diff options
| -rw-r--r-- | apps/web/app/settings/info/page.tsx | 4 | ||||
| -rw-r--r-- | apps/web/components/settings/ChangePassword.tsx | 225 | ||||
| -rw-r--r-- | apps/web/components/settings/UserDetails.tsx | 71 | ||||
| -rw-r--r-- | apps/web/components/settings/UserOptions.tsx | 192 |
4 files changed, 302 insertions, 190 deletions
diff --git a/apps/web/app/settings/info/page.tsx b/apps/web/app/settings/info/page.tsx index c7d8f808..52a6ce9d 100644 --- a/apps/web/app/settings/info/page.tsx +++ b/apps/web/app/settings/info/page.tsx @@ -1,10 +1,10 @@ import { ChangePassword } from "@/components/settings/ChangePassword"; import UserDetails from "@/components/settings/UserDetails"; -import { UserOptions } from "@/components/settings/UserOptions"; +import UserOptions from "@/components/settings/UserOptions"; export default async function InfoPage() { return ( - <div className="flex flex-col gap-8 rounded-md border bg-background p-4"> + <div className="flex flex-col gap-4"> <UserDetails /> <ChangePassword /> <UserOptions /> diff --git a/apps/web/components/settings/ChangePassword.tsx b/apps/web/components/settings/ChangePassword.tsx index f8c2b8dd..703b9c16 100644 --- a/apps/web/components/settings/ChangePassword.tsx +++ b/apps/web/components/settings/ChangePassword.tsx @@ -1,6 +1,7 @@ "use client"; import type { z } from "zod"; +import { useState } from "react"; import { ActionButton } from "@/components/ui/action-button"; import { Form, @@ -15,12 +16,19 @@ import { toast } from "@/components/ui/use-toast"; import { useTranslation } from "@/lib/i18n/client"; import { api } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { Eye, EyeOff, Lock } from "lucide-react"; import { useForm } from "react-hook-form"; import { zChangePasswordSchema } from "@karakeep/shared/types/users"; +import { Button } from "../ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; + export function ChangePassword() { const { t } = useTranslation(); + const [showCurrentPassword, setShowCurrentPassword] = useState(false); + const [showNewPassword, setShowNewPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); const form = useForm<z.infer<typeof zChangePasswordSchema>>({ resolver: zodResolver(zChangePasswordSchema), defaultValues: { @@ -55,83 +63,150 @@ export function ChangePassword() { } return ( - <div className="flex flex-col sm:flex-row"> - <div className="mb-4 w-full text-lg font-medium sm:w-1/3"> - {t("settings.info.change_password")} - </div> - <Form {...form}> - <form - onSubmit={form.handleSubmit(onSubmit)} - className="flex w-full flex-col gap-2" - > - <FormField - control={form.control} - name="currentPassword" - render={({ field }) => { - return ( - <FormItem className="flex-1"> - <FormLabel>{t("settings.info.current_password")}</FormLabel> - <FormControl> - <Input - type="password" - placeholder={t("settings.info.current_password")} - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - <FormField - control={form.control} - name="newPassword" - render={({ field }) => { - return ( - <FormItem className="flex-1"> - <FormLabel>{t("settings.info.new_password")}</FormLabel> - <FormControl> - <Input - type="password" - placeholder={t("settings.info.new_password")} - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - <FormField - control={form.control} - name="newPasswordConfirm" - render={({ field }) => { - return ( - <FormItem className="flex-1"> - <FormLabel> - {t("settings.info.confirm_new_password")} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Lock className="h-5 w-5" /> + Security + </CardTitle> + </CardHeader> + <CardContent className="space-y-6"> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> + <FormField + control={form.control} + name="currentPassword" + render={({ field }) => ( + <FormItem className="space-y-2"> + <FormLabel + htmlFor="current-password" + className="text-sm font-medium" + > + {t("settings.info.current_password")} </FormLabel> - <FormControl> - <Input - type="Password" - placeholder={t("settings.info.confirm_new_password")} - {...field} - /> - </FormControl> + <div className="relative"> + <FormControl> + <Input + id="current-password" + type={showCurrentPassword ? "text" : "password"} + placeholder={t("settings.info.current_password")} + className="h-11 pr-10" + {...field} + /> + </FormControl> + <Button + type="button" + variant="ghost" + size="sm" + className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent" + onClick={() => + setShowCurrentPassword(!showCurrentPassword) + } + > + {showCurrentPassword ? ( + <EyeOff className="h-4 w-4" /> + ) : ( + <Eye className="h-4 w-4" /> + )} + </Button> + </div> <FormMessage /> </FormItem> - ); - }} - /> - <ActionButton - className="mt-4 h-10 w-max px-8" - type="submit" - loading={mutator.isPending} - > - {t("actions.save")} - </ActionButton> - </form> - </Form> - </div> + )} + /> + + <div className="grid gap-4 md:grid-cols-2"> + <FormField + control={form.control} + name="newPassword" + render={({ field }) => ( + <FormItem className="space-y-2"> + <FormLabel + htmlFor="new-password" + className="text-sm font-medium" + > + {t("settings.info.new_password")} + </FormLabel> + <div className="relative"> + <FormControl> + <Input + id="new-password" + type={showNewPassword ? "text" : "password"} + placeholder={t("settings.info.new_password")} + className="h-11 pr-10" + {...field} + /> + </FormControl> + <Button + type="button" + variant="ghost" + size="sm" + className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent" + onClick={() => setShowNewPassword(!showNewPassword)} + > + {showNewPassword ? ( + <EyeOff className="h-4 w-4" /> + ) : ( + <Eye className="h-4 w-4" /> + )} + </Button> + </div> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="newPasswordConfirm" + render={({ field }) => ( + <FormItem className="space-y-2"> + <FormLabel + htmlFor="confirm-password" + className="text-sm font-medium" + > + {t("settings.info.confirm_new_password")} + </FormLabel> + <div className="relative"> + <FormControl> + <Input + id="confirm-password" + type={showConfirmPassword ? "text" : "password"} + placeholder={t("settings.info.confirm_new_password")} + className="h-11 pr-10" + {...field} + /> + </FormControl> + <Button + type="button" + variant="ghost" + size="sm" + className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent" + onClick={() => + setShowConfirmPassword(!showConfirmPassword) + } + > + {showConfirmPassword ? ( + <EyeOff className="h-4 w-4" /> + ) : ( + <Eye className="h-4 w-4" /> + )} + </Button> + </div> + <FormMessage /> + </FormItem> + )} + /> + </div> + + <div className="flex justify-end"> + <ActionButton type="submit" loading={mutator.isPending}> + {t("actions.save")} + </ActionButton> + </div> + </form> + </Form> + </CardContent> + </Card> ); } diff --git a/apps/web/components/settings/UserDetails.tsx b/apps/web/components/settings/UserDetails.tsx index af6698ad..6135df47 100644 --- a/apps/web/components/settings/UserDetails.tsx +++ b/apps/web/components/settings/UserDetails.tsx @@ -1,35 +1,60 @@ import { Input } from "@/components/ui/input"; import { useTranslation } from "@/lib/i18n/server"; import { api } from "@/server/api/client"; +import { Mail, User } from "lucide-react"; + +import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; +import { Label } from "../ui/label"; export default async function UserDetails() { const { t } = await useTranslation(); const whoami = await api.users.whoami(); - const details = [ - { - label: t("common.name"), - value: whoami.name ?? undefined, - }, - { - label: t("common.email"), - value: whoami.email ?? undefined, - }, - ]; - return ( - <div className="flex w-full flex-col sm:flex-row"> - <div className="mb-4 w-full text-lg font-medium sm:w-1/3"> - {t("settings.info.basic_details")} - </div> - <div className="w-full"> - {details.map(({ label, value }) => ( - <div className="mb-2" key={label}> - <div className="mb-2 text-sm font-medium">{label}</div> - <Input value={value} disabled /> + <Card> + <CardHeader> + <div className="flex items-center space-x-4"> + <div className="space-y-1"> + <CardTitle className="flex items-center gap-2"> + <User className="h-5 w-5" /> + {t("settings.info.basic_details")} + </CardTitle> + </div> + </div> + </CardHeader> + <CardContent className="space-y-6"> + <div className="grid gap-6 md:grid-cols-2"> + <div className="space-y-2"> + <Label htmlFor="name" className="text-sm font-medium"> + {t("common.name")} + </Label> + <Input + id="name" + defaultValue={whoami.name ?? ""} + className="h-11" + disabled + /> + </div> + <div className="space-y-2"> + <Label + htmlFor="email" + className="flex items-center gap-2 text-sm font-medium" + > + <Mail className="h-4 w-4" /> + {t("common.email")} + </Label> + <div className="relative"> + <Input + id="email" + type="email" + defaultValue={whoami.email ?? ""} + className="h-11" + disabled + /> + </div> </div> - ))} - </div> - </div> + </div> + </CardContent> + </Card> ); } diff --git a/apps/web/components/settings/UserOptions.tsx b/apps/web/components/settings/UserOptions.tsx index 3918ceed..483c3f2b 100644 --- a/apps/web/components/settings/UserOptions.tsx +++ b/apps/web/components/settings/UserOptions.tsx @@ -7,6 +7,7 @@ import { useInterfaceLang } from "@/lib/userLocalSettings/bookmarksLayout"; import { updateInterfaceLang } from "@/lib/userLocalSettings/userLocalSettings"; import { useUserSettings } from "@/lib/userSettings"; import { zodResolver } from "@hookform/resolvers/zod"; +import { Archive, Bookmark, Globe } from "lucide-react"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -17,6 +18,7 @@ import { zUserSettingsSchema, } from "@karakeep/shared/types/users"; +import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; import { Form, FormField } from "../ui/form"; import { Label } from "../ui/label"; import { @@ -26,6 +28,7 @@ import { SelectTrigger, SelectValue, } from "../ui/select"; +import { Separator } from "../ui/separator"; import { toast } from "../ui/use-toast"; const LanguageSelect = () => { @@ -37,7 +40,7 @@ const LanguageSelect = () => { await updateInterfaceLang(val); }} > - <SelectTrigger> + <SelectTrigger className="h-11"> <SelectValue /> </SelectTrigger> <SelectContent> @@ -51,7 +54,7 @@ const LanguageSelect = () => { ); }; -export default function UserSettings() { +export default function UserOptions() { const { t } = useTranslation(); const clientConfig = useClientConfig(); const data = useUserSettings(); @@ -101,97 +104,106 @@ export default function UserSettings() { return ( <Form {...form}> - <FormField - control={form.control} - name="bookmarkClickAction" - render={({ field }) => ( - <div className="flex w-full flex-col gap-2"> - <Label> - {t("settings.info.user_settings.bookmark_click_action.title")} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Globe className="h-5 w-5" /> + {t("settings.info.options")} + </CardTitle> + </CardHeader> + <CardContent className="space-y-6"> + <div className="space-y-2"> + <Label className="text-sm font-medium"> + {t("settings.info.interface_lang")} </Label> - <Select - disabled={!!clientConfig.demoMode} - value={field.value} - onValueChange={(value) => { - mutate({ - bookmarkClickAction: - value as ZUserSettings["bookmarkClickAction"], - }); - }} - > - <SelectTrigger> - <SelectValue> - {bookmarkClickActionTranslation[field.value]} - </SelectValue> - </SelectTrigger> - <SelectContent> - {Object.entries(bookmarkClickActionTranslation).map( - ([value, label]) => ( - <SelectItem key={value} value={value}> - {label} - </SelectItem> - ), - )} - </SelectContent> - </Select> + <LanguageSelect /> </div> - )} - /> - <FormField - control={form.control} - name="archiveDisplayBehaviour" - render={({ field }) => ( - <div className="flex w-full flex-col gap-2"> - <Label> - {t("settings.info.user_settings.archive_display_behaviour.title")} - </Label> - <Select - disabled={!!clientConfig.demoMode} - value={field.value} - onValueChange={(value) => { - mutate({ - archiveDisplayBehaviour: - value as ZUserSettings["archiveDisplayBehaviour"], - }); - }} - > - <SelectTrigger> - <SelectValue> - {archiveDisplayBehaviourTranslation[field.value]} - </SelectValue> - </SelectTrigger> - <SelectContent> - {Object.entries(archiveDisplayBehaviourTranslation).map( - ([value, label]) => ( - <SelectItem key={value} value={value}> - {label} - </SelectItem> - ), - )} - </SelectContent> - </Select> - </div> - )} - /> - </Form> - ); -} -export function UserOptions() { - const { t } = useTranslation(); + <Separator /> - return ( - <div className="flex flex-col sm:flex-row"> - <div className="mb-4 w-full text-lg font-medium sm:w-1/3"> - {t("settings.info.options")} - </div> - <div className="flex w-full flex-col gap-3"> - <div className="flex w-full flex-col gap-2"> - <Label>{t("settings.info.interface_lang")}</Label> - <LanguageSelect /> - </div> - <UserSettings /> - </div> - </div> + <div className="grid gap-6 md:grid-cols-2"> + <FormField + control={form.control} + name="bookmarkClickAction" + render={({ field }) => ( + <div className="space-y-2"> + <Label className="flex items-center gap-2 text-sm font-medium"> + <Bookmark className="h-4 w-4" /> + {t( + "settings.info.user_settings.bookmark_click_action.title", + )} + </Label> + <Select + disabled={!!clientConfig.demoMode} + value={field.value} + onValueChange={(value) => { + mutate({ + bookmarkClickAction: + value as ZUserSettings["bookmarkClickAction"], + }); + }} + > + <SelectTrigger className="h-11"> + <SelectValue> + {bookmarkClickActionTranslation[field.value]} + </SelectValue> + </SelectTrigger> + <SelectContent> + {Object.entries(bookmarkClickActionTranslation).map( + ([value, label]) => ( + <SelectItem key={value} value={value}> + {label} + </SelectItem> + ), + )} + </SelectContent> + </Select> + </div> + )} + /> + + <FormField + control={form.control} + name="archiveDisplayBehaviour" + render={({ field }) => ( + <div className="space-y-2"> + <Label className="flex items-center gap-2 text-sm font-medium"> + <Archive className="h-4 w-4" /> + {t( + "settings.info.user_settings.archive_display_behaviour.title", + )} + </Label> + <Select + disabled={!!clientConfig.demoMode} + value={field.value} + onValueChange={(value) => { + mutate({ + archiveDisplayBehaviour: + value as ZUserSettings["archiveDisplayBehaviour"], + }); + }} + > + <SelectTrigger className="h-11"> + <SelectValue> + {archiveDisplayBehaviourTranslation[field.value]} + </SelectValue> + </SelectTrigger> + <SelectContent> + {Object.entries(archiveDisplayBehaviourTranslation).map( + ([value, label]) => ( + <SelectItem key={value} value={value}> + {label} + </SelectItem> + ), + )} + </SelectContent> + </Select> + </div> + )} + /> + </div> + </CardContent> + </Card> + </Form> ); } |
