From ea22e272d7ae52ca417040b1bfe186135fda054a Mon Sep 17 00:00:00 2001 From: zobweyt Date: Sat, 14 Sep 2024 15:26:13 +0300 Subject: [PATCH] Add better avatar edit --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 13 +++ frontend/src/components/avatar-edit.tsx | 72 ++++++++++++++ frontend/src/components/index.tsx | 1 + .../session-expiration-observer.tsx | 4 +- .../src/components/steps/new-password.tsx | 8 +- .../src/components/steps/verification/otp.tsx | 6 +- .../src/components/ui/avatar/avatar.props.tsx | 9 ++ .../components/ui/avatar/avatar.styles.tsx | 17 ++++ frontend/src/components/ui/avatar/avatar.tsx | 42 ++++++++ frontend/src/components/ui/avatar/index.tsx | 5 + frontend/src/components/ui/index.tsx | 2 + frontend/src/components/ui/sidebar/item.tsx | 6 +- frontend/src/components/ui/sidebar/root.tsx | 48 ++++----- frontend/src/lib/api/auth/actions.ts | 13 ++- frontend/src/lib/api/auth/service.ts | 6 +- frontend/src/lib/api/otp/cache.ts | 6 +- frontend/src/lib/api/otp/service.ts | 2 +- frontend/src/lib/api/users/me/actions.ts | 6 +- frontend/src/lib/api/users/me/service.ts | 2 +- frontend/src/lib/i18n/locales/en.ts | 30 +++--- frontend/src/lib/i18n/locales/ru.ts | 36 +++---- frontend/src/lib/i18n/types.ts | 30 +++--- frontend/src/lib/utils/css/merge.ts | 6 +- .../(authenticated)/settings/account.tsx | 97 +++++++------------ .../settings/(settings).tsx => settings.tsx} | 42 ++------ .../src/routes/(unauthenticated)/login.tsx | 19 ++-- frontend/tailwind.config.cjs | 3 + 28 files changed, 306 insertions(+), 226 deletions(-) create mode 100644 frontend/src/components/avatar-edit.tsx create mode 100644 frontend/src/components/ui/avatar/avatar.props.tsx create mode 100644 frontend/src/components/ui/avatar/avatar.styles.tsx create mode 100644 frontend/src/components/ui/avatar/avatar.tsx create mode 100644 frontend/src/components/ui/avatar/index.tsx rename frontend/src/routes/(sidebar)/{(authenticated)/settings/(settings).tsx => settings.tsx} (72%) diff --git a/frontend/package.json b/frontend/package.json index fde6517..50be51f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "@solid-primitives/i18n": "^2.1.1", "@solid-primitives/intersection-observer": "^2.1.6", "@solid-primitives/storage": "^4.2.1", + "@solid-primitives/upload": "^0.0.117", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.14.3", "@solidjs/start": "^1.0.6", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index a65bd1d..bc4e7f7 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@solid-primitives/storage': specifier: ^4.2.1 version: 4.2.1(solid-js@1.8.22) + '@solid-primitives/upload': + specifier: ^0.0.117 + version: 0.0.117(solid-js@1.8.22) '@solidjs/meta': specifier: ^0.29.4 version: 0.29.4(solid-js@1.8.22) @@ -934,6 +937,11 @@ packages: peerDependencies: solid-js: ^1.6.12 + '@solid-primitives/upload@0.0.117': + resolution: {integrity: sha512-szDksm4u67JgiMtkpX8RPccxqfid4OCQ/zpJ1yB1PMFmenLjz8YKldGIIt+Yn3bEcfHBbdkPa2uu/y2FAdPeSw==} + peerDependencies: + solid-js: ^1.6.12 + '@solid-primitives/utils@6.2.3': resolution: {integrity: sha512-CqAwKb2T5Vi72+rhebSsqNZ9o67buYRdEJrIFzRXz3U59QqezuuxPsyzTSVCacwS5Pf109VRsgCJQoxKRoECZQ==} peerDependencies: @@ -3723,6 +3731,11 @@ snapshots: '@solid-primitives/utils': 6.2.3(solid-js@1.8.22) solid-js: 1.8.22 + '@solid-primitives/upload@0.0.117(solid-js@1.8.22)': + dependencies: + '@solid-primitives/utils': 6.2.3(solid-js@1.8.22) + solid-js: 1.8.22 + '@solid-primitives/utils@6.2.3(solid-js@1.8.22)': dependencies: solid-js: 1.8.22 diff --git a/frontend/src/components/avatar-edit.tsx b/frontend/src/components/avatar-edit.tsx new file mode 100644 index 0000000..f14c6e5 --- /dev/null +++ b/frontend/src/components/avatar-edit.tsx @@ -0,0 +1,72 @@ +import { createFileUploader } from "@solid-primitives/upload"; +import { useAction, useSubmission } from "@solidjs/router"; +import { Icon } from "solid-heroicons"; +import { arrowTopRightOnSquare, arrowUpTray, camera, trash } from "solid-heroicons/solid-mini"; +import { Component, Show } from "solid-js"; +import { toast } from "solid-sonner"; +import { Avatar, Dropdown, Separator } from "~/components"; +import { components } from "~/lib/api/schema"; +import { formatResourceURL } from "~/lib/api/uploads"; +import { removeCurrentUserAvatar, updateCurrentUserAvatar } from "~/lib/api/users/me"; +import { useI18n } from "~/lib/i18n"; + +export type AvatarEditProps = { + user: components["schemas"]["CurrentUserResponse"]; +}; + +export const AvatarEdit: Component = (props) => { + const i18n = useI18n(); + + const updateAvatar = useAction(updateCurrentUserAvatar); + const updatingAvatar = useSubmission(updateCurrentUserAvatar); + const uploader = createFileUploader({ accept: "image/*" }); + + const removeAvatar = useAction(removeCurrentUserAvatar); + const removingAvatar = useSubmission(removeCurrentUserAvatar); + + const selectAvatar = () => { + uploader.selectFiles(async (uploads) => { + const result = await updateAvatar(uploads[0].file); + if (result?.error) { + toast.error(result.error.detail?.toString()); + } + }); + }; + + return ( + + + +
+ + {i18n.t.components.avatarEdit.update()} +
+
+ + + {(avatar_url) => ( + <> + window.open(formatResourceURL(avatar_url()), "_blank")}> + + {i18n.t.components.avatarEdit.open()} + + + + )} + + + + {i18n.t.components.avatarEdit.select()} + + + + {i18n.t.components.avatarEdit.remove()} + + +
+ ); +}; diff --git a/frontend/src/components/index.tsx b/frontend/src/components/index.tsx index a1fa1f4..d86d705 100644 --- a/frontend/src/components/index.tsx +++ b/frontend/src/components/index.tsx @@ -1,2 +1,3 @@ export * from "./ui"; export * from "./session-expiration-observer"; +export * from "./avatar-edit"; diff --git a/frontend/src/components/session-expiration-observer.tsx b/frontend/src/components/session-expiration-observer.tsx index b8ec993..705f00b 100644 --- a/frontend/src/components/session-expiration-observer.tsx +++ b/frontend/src/components/session-expiration-observer.tsx @@ -40,10 +40,10 @@ export const SessionExpirationObserver: ParentComponent = (props) => { revalidate([getIsLoggedIn.key, getSessionExpirationDate.key, getCurrentUser.key]); }; - // BUG: When logging out, toast infinitely shows up on iOS until logged in. + // BUG: When logging out, toast infinitely shows up on mobile until logged in. const toastSessionExpired = (): void => { if (isLoggedIn() === false) { - toast.info(i18n.t.sessionExpired()); + toast.info(i18n.t.components.sessionExpirationObserver.expired()); } }; diff --git a/frontend/src/components/steps/new-password.tsx b/frontend/src/components/steps/new-password.tsx index b9c99e8..78676ff 100644 --- a/frontend/src/components/steps/new-password.tsx +++ b/frontend/src/components/steps/new-password.tsx @@ -26,12 +26,12 @@ export function NewPasswordStep() { return; } - const changed = await reset({ email: context.store.email, code: context.store.otp, password: form.password }); + const result = await reset({ email: context.store.email, code: context.store.otp, password: form.password }); - if (changed) { - toast.success(i18n.t.steps.resetPassword.password.form.success()); + if (result?.error) { + toast.error(result.error.detail?.toString()); } else { - toast.error(i18n.t.steps.resetPassword.password.form.errors.unknown()); + toast.success(i18n.t.steps.resetPassword.password.form.success()); } }; diff --git a/frontend/src/components/steps/verification/otp.tsx b/frontend/src/components/steps/verification/otp.tsx index c17bb00..dd69d7d 100644 --- a/frontend/src/components/steps/verification/otp.tsx +++ b/frontend/src/components/steps/verification/otp.tsx @@ -34,10 +34,10 @@ export const OtpStep = () => { const onSubmit = async (form: OtpForm) => { const otp = Number(form.otp); + const result = await isCorrectOtp({ email: email(), code: otp }); - if (!(await isCorrectOtp({ email: email(), code: otp }))) { - toast.error(i18n.t.steps.verification.otp.incorrect()); - + if (result.error) { + toast.error(result.error.detail?.toString()); reset(); } else { context.setStore("otp", otp); diff --git a/frontend/src/components/ui/avatar/avatar.props.tsx b/frontend/src/components/ui/avatar/avatar.props.tsx new file mode 100644 index 0000000..1dc588b --- /dev/null +++ b/frontend/src/components/ui/avatar/avatar.props.tsx @@ -0,0 +1,9 @@ +import type { JSX, ValidComponent } from "solid-js"; +import type { ImageRootProps, ImageImgProps } from "@kobalte/core/image"; + +export type AvatarRootProps = ImageRootProps & JSX.StylableSVGAttributes; +export type AvatarImgProps = Omit, "src"> & + JSX.StylableSVGAttributes & { + alt: string; + src: string | null | undefined; + }; diff --git a/frontend/src/components/ui/avatar/avatar.styles.tsx b/frontend/src/components/ui/avatar/avatar.styles.tsx new file mode 100644 index 0000000..834a54b --- /dev/null +++ b/frontend/src/components/ui/avatar/avatar.styles.tsx @@ -0,0 +1,17 @@ +import { tv } from "tailwind-variants"; + +export const styles = tv({ + slots: { + root: [ + "group relative inline-flex size-24 items-center justify-center container-size", + "select-none overflow-hidden rounded-full text-fg-soft ring-1 ring-bg-tertiary", + ], + img: ["group-aria-busy:cursor-progress group-aria-busy:animate-pulse"], + alt: [ + "flex size-full items-center justify-center", + "bg-bg-default text-cqi-50 font-medium uppercase leading-none", + "group-aria-busy:cursor-progress group-aria-busy:animate-pulse", + ], + fallback: ["size-full animate-pulse bg-bg-default"], + }, +}); diff --git a/frontend/src/components/ui/avatar/avatar.tsx b/frontend/src/components/ui/avatar/avatar.tsx new file mode 100644 index 0000000..39ed62d --- /dev/null +++ b/frontend/src/components/ui/avatar/avatar.tsx @@ -0,0 +1,42 @@ +import { type ValidComponent, Show, splitProps } from "solid-js"; +import { Transition } from "solid-transition-group"; + +import { Image } from "@kobalte/core/image"; +import type { PolymorphicProps } from "@kobalte/core/polymorphic"; + +import { formatResourceURL } from "~/lib/api/uploads"; +import { merge } from "~/lib/utils/css/merge"; + +import type { AvatarImgProps, AvatarRootProps } from "./avatar.props"; +import { styles } from "./avatar.styles"; + +export const AvatarRoot = (props: PolymorphicProps>) => { + const [local, others] = splitProps(props as AvatarRootProps, ["class"]); + + return ; +}; + +export const AvatarImg = (props: PolymorphicProps>) => { + const [local, others] = splitProps(props as AvatarImgProps, ["class", "src"]); + + return ( + + {props.alt.slice(0, 2)}}> + {(src) => ( + + + + + )} + + + ); +}; diff --git a/frontend/src/components/ui/avatar/index.tsx b/frontend/src/components/ui/avatar/index.tsx new file mode 100644 index 0000000..5e76f2d --- /dev/null +++ b/frontend/src/components/ui/avatar/index.tsx @@ -0,0 +1,5 @@ +import { AvatarImg, AvatarRoot } from "./avatar"; + +export const Avatar = Object.assign(AvatarRoot, { + Img: AvatarImg, +}); diff --git a/frontend/src/components/ui/index.tsx b/frontend/src/components/ui/index.tsx index f02804d..1cd2155 100644 --- a/frontend/src/components/ui/index.tsx +++ b/frontend/src/components/ui/index.tsx @@ -1,3 +1,4 @@ +export * from "./avatar"; export * from "./button"; export * from "./cursor"; export * from "./dropdown"; @@ -9,6 +10,7 @@ export * from "./motion"; export * from "./otp-field"; export * from "./select"; export * from "./separator"; +export * from "./settings"; export * from "./sidebar"; export * from "./stepper"; export * from "./text-field"; diff --git a/frontend/src/components/ui/sidebar/item.tsx b/frontend/src/components/ui/sidebar/item.tsx index c10ff6c..4d90d60 100644 --- a/frontend/src/components/ui/sidebar/item.tsx +++ b/frontend/src/components/ui/sidebar/item.tsx @@ -9,15 +9,15 @@ export const SidebarItemRoot: ParentComponent = (props) => { return ( { const i18n = useI18n(); @@ -28,31 +29,24 @@ export const Sidebar: Component = () => { Library - - - {i18n.t.routes.login.title()} - - } - > - {(user) => ( - - }> - {(avatar_url) => ( - - )} - + + }> + {(user) => ( + { + const [local, rest] = splitProps(props, ["class"]); + return ( + + + + ); + }} + /> + )} + - {i18n.t.routes.settings.heading()} - - )} - + {i18n.t.routes.settings.heading()} + ); diff --git a/frontend/src/lib/api/auth/actions.ts b/frontend/src/lib/api/auth/actions.ts index d6fca15..c93a957 100644 --- a/frontend/src/lib/api/auth/actions.ts +++ b/frontend/src/lib/api/auth/actions.ts @@ -9,13 +9,12 @@ import { $authenticate, $resetPassword } from "./service"; export const authenticate = action(async (form: LoginForm) => { "use server"; - const { data, error, status } = await $authenticate(form.email, form.password); + const { data, error } = await $authenticate(form.email, form.password); if (data) { await updateSession(data); - } - if (error) { - return { error, code: status }; + } else if (error) { + return { error }; } throw redirect(form.redirect || "/"); @@ -32,11 +31,11 @@ export const unauthenticate = action(async () => { export const resetPassword = action(async (body: components["schemas"]["UserPasswordReset"]) => { "use server"; - const { data } = await $resetPassword(body); + const { data, error } = await $resetPassword(body); if (data) { await updateSession(data); - return true; + } else if (error) { + return { error }; } - return false; }); diff --git a/frontend/src/lib/api/auth/service.ts b/frontend/src/lib/api/auth/service.ts index b37eb6b..4630d85 100644 --- a/frontend/src/lib/api/auth/service.ts +++ b/frontend/src/lib/api/auth/service.ts @@ -13,7 +13,8 @@ export const $authenticate = async (username: string, password: string) => { }, bodySerializer: formDataSerializer, }); - return { data: result.data, status: result.response.status, error: result.error }; + + return { data: result.data, error: result.error }; }; export const $resetPassword = async (body: components["schemas"]["UserPasswordReset"]) => { @@ -22,5 +23,6 @@ export const $resetPassword = async (body: components["schemas"]["UserPasswordRe const result = await client.PATCH("/api/v1/auth/reset-password", { body: body, }); - return { data: result.data }; + + return { data: result.data, error: result.error }; }; diff --git a/frontend/src/lib/api/otp/cache.ts b/frontend/src/lib/api/otp/cache.ts index f2013f6..4287314 100644 --- a/frontend/src/lib/api/otp/cache.ts +++ b/frontend/src/lib/api/otp/cache.ts @@ -6,10 +6,8 @@ import { $verifyOtp, $sendOtp } from "./service"; export const isCorrectOtp = cache(async (body: components["schemas"]["CodeVerify"]) => { "use server"; - const { status } = await $verifyOtp(body); - const isCorrect = status === 202; - - return isCorrect; + const { error } = await $verifyOtp(body); + return { error }; }, "isCorrectOtp"); export const sendOtp = cache(async (email: string) => { diff --git a/frontend/src/lib/api/otp/service.ts b/frontend/src/lib/api/otp/service.ts index e28ff67..69970ff 100644 --- a/frontend/src/lib/api/otp/service.ts +++ b/frontend/src/lib/api/otp/service.ts @@ -18,5 +18,5 @@ export const $verifyOtp = async (body: components["schemas"]["CodeVerify"]) => { const result = await client.POST("/api/v1/verification/verify", { body: body, }); - return { data: result.data, status: result.response.status }; + return { data: result.data, error: result.error }; }; diff --git a/frontend/src/lib/api/users/me/actions.ts b/frontend/src/lib/api/users/me/actions.ts index 7971f8b..3967dc7 100644 --- a/frontend/src/lib/api/users/me/actions.ts +++ b/frontend/src/lib/api/users/me/actions.ts @@ -11,5 +11,9 @@ export const removeCurrentUserAvatar = action(async () => { export const updateCurrentUserAvatar = action(async (file: File) => { "use server"; - await $updateCurrentUserAvatar(file); + const { error, status } = await $updateCurrentUserAvatar(file); + + if (error) { + return { error, status }; + } }); diff --git a/frontend/src/lib/api/users/me/service.ts b/frontend/src/lib/api/users/me/service.ts index bfbaf02..2c86316 100644 --- a/frontend/src/lib/api/users/me/service.ts +++ b/frontend/src/lib/api/users/me/service.ts @@ -24,5 +24,5 @@ export const $updateCurrentUserAvatar = async (file: File) => { }, bodySerializer: formDataSerializer, }); - return { data: result.data }; + return { data: result.data, error: result.error, status: result.response.status }; }; diff --git a/frontend/src/lib/i18n/locales/en.ts b/frontend/src/lib/i18n/locales/en.ts index 69dfe80..9f388b8 100644 --- a/frontend/src/lib/i18n/locales/en.ts +++ b/frontend/src/lib/i18n/locales/en.ts @@ -30,10 +30,6 @@ export const dict: DictionaryMap = { minLength: "Your password must have 8 characters or more.", }, }, - errors: { - 401: "Incorrect password", - 404: "User with such email not found", - }, submit: "Log in", }, footer: "Don't have an account? ", @@ -89,19 +85,6 @@ export const dict: DictionaryMap = { account: { heading: "Account", sections: { - avatar: { - heading: "Avatar", - cards: { - change: { - heading: "Upload new avatar", - description: "Change the current profile picture.", - }, - remove: { - heading: "Remove avatar", - description: "Delete the current uploaded avatar.", - }, - }, - }, security: { heading: "Security", cards: { @@ -147,7 +130,6 @@ export const dict: DictionaryMap = { }, errors: { mismatch: "The entered passwords don't match!", - unknown: "Something went wrong. Try all the steps again.", }, submit: "Update my password", success: "Password changed successfully.", @@ -175,7 +157,17 @@ export const dict: DictionaryMap = { }, }, }, - sessionExpired: "Your session has expired!", + components: { + sessionExpirationObserver: { + expired: "Your session has expired!", + }, + avatarEdit: { + open: "Open original", + update: "Update", + select: "Select new avatar", + remove: "Remove this avatar", + }, + }, }; export default dict; diff --git a/frontend/src/lib/i18n/locales/ru.ts b/frontend/src/lib/i18n/locales/ru.ts index a3bcdb8..b9c3655 100644 --- a/frontend/src/lib/i18n/locales/ru.ts +++ b/frontend/src/lib/i18n/locales/ru.ts @@ -30,10 +30,6 @@ export const dict: DictionaryMap = { minLength: "Ваш пароль должен содержать не менее {{ length }} символов.", }, }, - errors: { - 401: "Неверный пароль", - 404: "Пользователь с таким адресом электронной почты не зарегистрирован", - }, submit: "Войти", }, footer: "Нет аккаунта? ", @@ -46,10 +42,10 @@ export const dict: DictionaryMap = { heading: "Настройки", sections: { account: { - heading: "Учетная запись", + heading: "Аккаунт", cards: { account: { - heading: "Личный", + heading: "Личное", description: "Аватар, адрес электронной почты и пароль, управление учетной записью.", }, signout: { @@ -87,21 +83,8 @@ export const dict: DictionaryMap = { }, }, account: { - heading: "Учетная запись", + heading: "Аккаунт", sections: { - avatar: { - heading: "Аватар", - cards: { - change: { - heading: "Загрузить новый аватар", - description: "Измените текущее изображение профиля.", - }, - remove: { - heading: "Удалить аватар", - description: "Удалить текущий загруженный аватар.", - }, - }, - }, security: { heading: "Безопасность", cards: { @@ -168,7 +151,6 @@ export const dict: DictionaryMap = { }, errors: { mismatch: "Введённые пароли не совпадают!", - unknown: "Что-то пошло не так. Попробуйте пройти все шаги снова.", }, submit: "Обновить мой пароль", success: "Пароль успешно изменён.", @@ -176,7 +158,17 @@ export const dict: DictionaryMap = { }, }, }, - sessionExpired: "Ваша сессия истекла!", + components: { + sessionExpirationObserver: { + expired: "Ваша сессия истекла!", + }, + avatarEdit: { + open: "Открыть оригинал", + update: "Изменить", + select: "Выбрать новый аватар", + remove: "Удалить этот аватар", + }, + }, }; export default dict; diff --git a/frontend/src/lib/i18n/types.ts b/frontend/src/lib/i18n/types.ts index dc9f7d8..3406e14 100644 --- a/frontend/src/lib/i18n/types.ts +++ b/frontend/src/lib/i18n/types.ts @@ -34,10 +34,6 @@ export type DictionaryMap = { minLength: string; }; }; - errors: { - 401: string; - 404: string; - }; submit: string; }; footer: string; @@ -88,19 +84,6 @@ export type DictionaryMap = { account: { heading: string; sections: { - avatar: { - heading: string; - cards: { - change: { - heading: string; - description: string; - }; - remove: { - heading: string; - description: string; - }; - }; - }; security: { heading: string; cards: { @@ -165,7 +148,6 @@ export type DictionaryMap = { }; errors: { mismatch: string; - unknown: string; }; submit: string; success: string; @@ -173,7 +155,17 @@ export type DictionaryMap = { }; }; }; - sessionExpired: string; + components: { + sessionExpirationObserver: { + expired: string; + }; + avatarEdit: { + open: string; + update: string; + select: string; + remove: string; + }; + }; }; export type LocalizedDictionary = i18n.Flatten; diff --git a/frontend/src/lib/utils/css/merge.ts b/frontend/src/lib/utils/css/merge.ts index dfb4e31..0a13ed5 100644 --- a/frontend/src/lib/utils/css/merge.ts +++ b/frontend/src/lib/utils/css/merge.ts @@ -1,6 +1,6 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; export function merge(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } diff --git a/frontend/src/routes/(sidebar)/(authenticated)/settings/account.tsx b/frontend/src/routes/(sidebar)/(authenticated)/settings/account.tsx index 994bfb5..c7d861b 100644 --- a/frontend/src/routes/(sidebar)/(authenticated)/settings/account.tsx +++ b/frontend/src/routes/(sidebar)/(authenticated)/settings/account.tsx @@ -1,19 +1,18 @@ import { Breadcrumbs } from "@kobalte/core/breadcrumbs"; -import { A, createAsync, useAction } from "@solidjs/router"; +import { A, createAsync, useSubmission } from "@solidjs/router"; import { Icon } from "solid-heroicons"; -import { arrowUpTray, chevronRight, envelopeOpen, key, userCircle, xMark } from "solid-heroicons/solid-mini"; +import { arrowRightOnRectangle, chevronRight, envelopeOpen, key } from "solid-heroicons/solid-mini"; import { Show } from "solid-js"; -import { Heading, Title } from "~/components"; -import { SettingsCard } from "~/components/ui/settings/card"; -import { SettingsGroup } from "~/components/ui/settings/group"; -import { formatResourceURL } from "~/lib/api/uploads"; -import { getCurrentUser, removeCurrentUserAvatar, updateCurrentUserAvatar } from "~/lib/api/users/me"; +import { AvatarEdit, Heading, SettingsCard, SettingsGroup, Title } from "~/components"; +import { unauthenticate } from "~/lib/api/auth"; +import { getCurrentUser } from "~/lib/api/users/me"; import { useI18n } from "~/lib/i18n"; export default function Account() { const i18n = useI18n(); const currentUser = createAsync(() => getCurrentUser()); - const updateAvatar = useAction(updateCurrentUserAvatar); + + const unauthenticating = useSubmission(unauthenticate); return ( <> @@ -29,7 +28,7 @@ export default function Account() { {i18n.t.routes.settings.heading()} - +
  • {i18n.t.routes.settings.account.heading()}
  • @@ -37,15 +36,7 @@ export default function Account() {
    - }> - {(avatar_url) => ( - {i18n.t.routes.settings.account.sections.avatar.heading()} - )} - +
    {user().email} @@ -55,51 +46,12 @@ export default function Account() {
    - {i18n.t.routes.settings.account.sections.avatar.heading()} - - - - await updateAvatar(e.currentTarget.files?.[0]!)} - /> - - - - {i18n.t.routes.settings.account.sections.avatar.cards.change.heading()} - - - {i18n.t.routes.settings.account.sections.avatar.cards.change.description()} - - - - - - - - - - - - {i18n.t.routes.settings.account.sections.avatar.cards.remove.heading()} - - - {i18n.t.routes.settings.account.sections.avatar.cards.remove.description()} - - - - - -
    - -
    - {i18n.t.routes.settings.account.sections.security.heading()} + + {i18n.t.routes.settings.account.sections.security.heading()} + + @@ -113,6 +65,7 @@ export default function Account() { {user().email} + @@ -125,6 +78,28 @@ export default function Account() { + + + + + + + + + {i18n.t.routes.settings.sections.account.cards.signout.heading()} + + + {i18n.t.routes.settings.sections.account.cards.signout.description()} + + + +
    diff --git a/frontend/src/routes/(sidebar)/(authenticated)/settings/(settings).tsx b/frontend/src/routes/(sidebar)/settings.tsx similarity index 72% rename from frontend/src/routes/(sidebar)/(authenticated)/settings/(settings).tsx rename to frontend/src/routes/(sidebar)/settings.tsx index d5b4eed..13adaf2 100644 --- a/frontend/src/routes/(sidebar)/(authenticated)/settings/(settings).tsx +++ b/frontend/src/routes/(sidebar)/settings.tsx @@ -1,29 +1,20 @@ -import { A, useSubmission } from "@solidjs/router"; -import { - arrowRightOnRectangle, - chevronRight, - globeAlt, - informationCircle, - paintBrush, - userCircle, -} from "solid-heroicons/solid-mini"; +import { A } from "@solidjs/router"; +import { chevronRight, globeAlt, informationCircle, paintBrush, userCircle } from "solid-heroicons/solid-mini"; import { Heading, LocaleSwitcher, + SettingsCard, + SettingsExpander, + SettingsGroup, ThemeSwitcher, Title, } from "~/components"; -import { SettingsCard } from "~/components/ui/settings/card"; -import { SettingsExpander } from "~/components/ui/settings/expander"; -import { SettingsGroup } from "~/components/ui/settings/group"; -import { unauthenticate } from "~/lib/api/auth"; import { useI18n } from "~/lib/i18n"; import { useTheme } from "~/lib/theme"; export default function Settings() { const i18n = useI18n(); const theme = useTheme(); - const unauthenticating = useSubmission(unauthenticate); return (
    @@ -49,27 +40,6 @@ export default function Settings() { - - - - - - - {i18n.t.routes.settings.sections.account.cards.signout.heading()} - - - {i18n.t.routes.settings.sections.account.cards.signout.description()} - - - - -
    @@ -79,7 +49,7 @@ export default function Settings() { - + diff --git a/frontend/src/routes/(unauthenticated)/login.tsx b/frontend/src/routes/(unauthenticated)/login.tsx index c24ec4e..9918a2c 100644 --- a/frontend/src/routes/(unauthenticated)/login.tsx +++ b/frontend/src/routes/(unauthenticated)/login.tsx @@ -1,5 +1,5 @@ import { A, useAction, useSearchParams, useSubmission } from "@solidjs/router"; -import { createEffect, JSX, on, Show } from "solid-js"; +import { JSX, Show } from "solid-js"; import { createForm, email, minLength, required } from "@modular-forms/solid"; import { toast } from "solid-sonner"; @@ -16,16 +16,13 @@ export default function Login() { const [, Login] = createForm(); const action = useAction(authenticate); const submission = useSubmission(authenticate); - const result = () => submission.result; - createEffect( - on(result, () => { - const code = result()?.code; - if (code === 401 || code === 404) { - toast.error(i18n.t.routes.login.form.errors[code]()); - } - }), - ); + const submit = async (form: LoginForm) => { + const result = await action(form); + if (result.error) { + toast.error(result.error.detail?.toString()) + } + } return (
    @@ -40,7 +37,7 @@ export default function Login() {
    - +