mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-02 20:15:24 +02:00
Feature/user photo storage (#877)
* add default assets for user profile * add recipe avatar * change user_id to UUID * add profile image upload * setup image cache keys * cleanup tests and add image tests * purge user data on delete * new user repository tests * add user_id validator for int -> UUID conversion * delete depreciated route * force set content type * refactor tests to use temp directory * validate parent exists before createing * set user_id to correct type * update instruction id * reset primary key on migration
This commit is contained in:
parent
a2f8f27193
commit
ea7c4771ee
64 changed files with 433 additions and 181 deletions
|
@ -9,9 +9,8 @@
|
|||
<v-divider class="mx-2"></v-divider>
|
||||
<div class="d-flex flex-column">
|
||||
<div class="d-flex mt-3" style="gap: 10px">
|
||||
<v-avatar size="40">
|
||||
<img alt="user" src="https://cdn.pixabay.com/photo/2020/06/24/19/12/cabbage-5337431_1280.jpg" />
|
||||
</v-avatar>
|
||||
<UserAvatar size="40" :user-id="$auth.user.id" />
|
||||
|
||||
<v-textarea
|
||||
v-model="comment"
|
||||
hide-details=""
|
||||
|
@ -32,9 +31,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div v-for="comment in comments" :key="comment.id" class="d-flex my-2" style="gap: 10px">
|
||||
<v-avatar size="40">
|
||||
<img alt="user" src="https://cdn.pixabay.com/photo/2020/06/24/19/12/cabbage-5337431_1280.jpg" />
|
||||
</v-avatar>
|
||||
<UserAvatar size="40" :user-id="comment.userId" />
|
||||
<v-card outlined class="flex-grow-1">
|
||||
<v-card-text class="pa-3 pb-0">
|
||||
<p class="">{{ comment.user.username }} • {{ $d(Date.parse(comment.createdAt), "medium") }}</p>
|
||||
|
@ -60,8 +57,12 @@
|
|||
import { defineComponent, ref, toRefs, onMounted, reactive } from "@nuxtjs/composition-api";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { RecipeComment } from "~/api/class-interfaces/recipes/types";
|
||||
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
UserAvatar,
|
||||
},
|
||||
props: {
|
||||
slug: {
|
||||
type: String,
|
||||
|
|
46
frontend/components/Domain/User/UserAvatar.vue
Normal file
46
frontend/components/Domain/User/UserAvatar.vue
Normal file
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<v-list-item-avatar v-if="list && userId">
|
||||
<v-img :src="imageURL" :alt="userId" @load="error = false" @error="error = true"> </v-img>
|
||||
</v-list-item-avatar>
|
||||
<v-avatar v-else-if="userId" :size="size">
|
||||
<v-img :src="imageURL" :alt="userId" @load="error = false" @error="error = true"> </v-img>
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, toRefs, reactive, useContext, computed } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
list: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "42",
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const state = reactive({
|
||||
error: false,
|
||||
});
|
||||
|
||||
const { $auth } = useContext();
|
||||
|
||||
const imageURL = computed(() => {
|
||||
const key = $auth?.user?.cacheKey || "";
|
||||
return `/api/media/users/${props.userId}/profile.webp?cacheKey=${key}`;
|
||||
});
|
||||
|
||||
return {
|
||||
imageURL,
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -3,9 +3,7 @@
|
|||
<!-- User Profile -->
|
||||
<template v-if="$auth.user">
|
||||
<v-list-item two-line to="/user/profile" exact>
|
||||
<v-list-item-avatar color="accent" class="white--text">
|
||||
<v-img :src="require(`~/static/account.png`)" />
|
||||
</v-list-item-avatar>
|
||||
<UserAvatar list :user-id="$auth.user.id" />
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title> {{ $auth.user.fullName }}</v-list-item-title>
|
||||
|
@ -130,8 +128,12 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { SidebarLinks } from "~/types/application-types";
|
||||
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
UserAvatar,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
|
|
|
@ -17,10 +17,8 @@
|
|||
hide-default-footer
|
||||
disable-pagination
|
||||
>
|
||||
<template #item.avatar="">
|
||||
<v-avatar>
|
||||
<img src="https://i.pravatar.cc/300" alt="John" />
|
||||
</v-avatar>
|
||||
<template #item.avatar="{ item }">
|
||||
<UserAvatar :user-id="item.id" />
|
||||
</template>
|
||||
<template #item.admin="{ item }">
|
||||
{{ item.admin ? "Admin" : "User" }}
|
||||
|
@ -66,8 +64,12 @@
|
|||
import { defineComponent, ref, onMounted, useContext } from "@nuxtjs/composition-api";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { UserOut } from "~/types/api-types/user";
|
||||
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
UserAvatar,
|
||||
},
|
||||
setup() {
|
||||
const api = useUserApi();
|
||||
|
||||
|
|
|
@ -2,12 +2,21 @@
|
|||
<v-container class="narrow-container">
|
||||
<BasePageTitle divider>
|
||||
<template #header>
|
||||
<v-img max-height="200" max-width="200" class="mb-2" :src="require('~/static/svgs/manage-profile.svg')"></v-img>
|
||||
<div class="d-flex flex-column align-center justify-center">
|
||||
<UserAvatar size="96" :user-id="$auth.user.id" />
|
||||
<AppButtonUpload
|
||||
class="my-1"
|
||||
file-name="profile"
|
||||
accept="image/*"
|
||||
:url="`/api/users/${$auth.user.id}/image`"
|
||||
@uploaded="$auth.fetchUser()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #title> Your Profile Settings </template>
|
||||
</BasePageTitle>
|
||||
|
||||
<section>
|
||||
<section class="mt-5">
|
||||
<ToggleState tag="article">
|
||||
<template #activator="{ toggle, state }">
|
||||
<v-btn v-if="!state" color="info" class="mt-2 mb-n3" @click="toggle">
|
||||
|
@ -105,8 +114,12 @@
|
|||
<script lang="ts">
|
||||
import { ref, reactive, defineComponent, computed, useContext, watch } from "@nuxtjs/composition-api";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
UserAvatar,
|
||||
},
|
||||
setup() {
|
||||
const nuxtContext = useContext();
|
||||
const user = computed(() => nuxtContext.$auth.user);
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<template>
|
||||
<v-container v-if="user">
|
||||
<section class="d-flex flex-column align-center">
|
||||
<v-avatar color="primary" size="75" class="mb-2">
|
||||
<v-img :src="require(`~/static/account.png`)" />
|
||||
</v-avatar>
|
||||
<UserAvatar size="84" :user-id="$auth.user.id" />
|
||||
|
||||
<h2 class="headline">👋 Welcome, {{ user.fullName }}</h2>
|
||||
<p class="subtitle-1 mb-0">
|
||||
Manage your profile, recipes, and group settings.
|
||||
|
@ -137,10 +136,13 @@ import UserProfileLinkCard from "@/components/Domain/User/UserProfileLinkCard.vu
|
|||
import { useUserApi } from "~/composables/api";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import UserAvatar from "@/components/Domain/User/UserAvatar.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "UserProfile",
|
||||
components: {
|
||||
UserProfileLinkCard,
|
||||
UserAvatar,
|
||||
},
|
||||
scrollToTop: true,
|
||||
setup() {
|
||||
|
|
BIN
frontend/static/fallback-profile.webp
Normal file
BIN
frontend/static/fallback-profile.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Loading…
Add table
Add a link
Reference in a new issue