diff options
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/web/app/dashboard/layout.tsx | 42 | ||||
| -rw-r--r-- | apps/web/app/settings/layout.tsx | 17 | ||||
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/LinkCard.tsx | 26 | ||||
| -rw-r--r-- | apps/web/components/settings/UserOptions.tsx | 102 | ||||
| -rw-r--r-- | apps/web/lib/i18n/locales/en/translation.json | 8 | ||||
| -rw-r--r-- | apps/web/lib/userSettings.tsx | 33 |
6 files changed, 195 insertions, 33 deletions
diff --git a/apps/web/app/dashboard/layout.tsx b/apps/web/app/dashboard/layout.tsx index 45b97653..c4a53e4b 100644 --- a/apps/web/app/dashboard/layout.tsx +++ b/apps/web/app/dashboard/layout.tsx @@ -4,6 +4,7 @@ import MobileSidebar from "@/components/shared/sidebar/MobileSidebar"; import Sidebar from "@/components/shared/sidebar/Sidebar"; import SidebarLayout from "@/components/shared/sidebar/SidebarLayout"; import { Separator } from "@/components/ui/separator"; +import { UserSettingsContextProvider } from "@/lib/userSettings"; import { api } from "@/server/api/client"; import { getServerAuthSession } from "@/server/auth"; import { TFunction } from "i18next"; @@ -30,7 +31,10 @@ export default async function Dashboard({ redirect("/"); } - const lists = await api.lists.list(); + const [lists, userSettings] = await Promise.all([ + api.lists.list(), + api.users.settings(), + ]); const items = (t: TFunction) => [ @@ -75,22 +79,24 @@ export default async function Dashboard({ ]; return ( - <SidebarLayout - sidebar={ - <Sidebar - items={items} - extraSections={ - <> - <Separator /> - <AllLists initialData={lists} /> - </> - } - /> - } - mobileSidebar={<MobileSidebar items={mobileSidebar} />} - modal={modal} - > - {children} - </SidebarLayout> + <UserSettingsContextProvider userSettings={userSettings}> + <SidebarLayout + sidebar={ + <Sidebar + items={items} + extraSections={ + <> + <Separator /> + <AllLists initialData={lists} /> + </> + } + /> + } + mobileSidebar={<MobileSidebar items={mobileSidebar} />} + modal={modal} + > + {children} + </SidebarLayout> + </UserSettingsContextProvider> ); } diff --git a/apps/web/app/settings/layout.tsx b/apps/web/app/settings/layout.tsx index 9bac783c..1f7c5c12 100644 --- a/apps/web/app/settings/layout.tsx +++ b/apps/web/app/settings/layout.tsx @@ -1,6 +1,8 @@ import MobileSidebar from "@/components/shared/sidebar/MobileSidebar"; import Sidebar from "@/components/shared/sidebar/Sidebar"; import SidebarLayout from "@/components/shared/sidebar/SidebarLayout"; +import { UserSettingsContextProvider } from "@/lib/userSettings"; +import { api } from "@/server/api/client"; import { TFunction } from "i18next"; import { ArrowLeft, @@ -79,12 +81,15 @@ export default async function SettingsLayout({ }: Readonly<{ children: React.ReactNode; }>) { + const userSettings = await api.users.settings(); return ( - <SidebarLayout - sidebar={<Sidebar items={settingsSidebarItems} />} - mobileSidebar={<MobileSidebar items={settingsSidebarItems} />} - > - {children} - </SidebarLayout> + <UserSettingsContextProvider userSettings={userSettings}> + <SidebarLayout + sidebar={<Sidebar items={settingsSidebarItems} />} + mobileSidebar={<MobileSidebar items={settingsSidebarItems} />} + > + {children} + </SidebarLayout> + </UserSettingsContextProvider> ); } diff --git a/apps/web/components/dashboard/bookmarks/LinkCard.tsx b/apps/web/components/dashboard/bookmarks/LinkCard.tsx index ec224ca6..2c91bd08 100644 --- a/apps/web/components/dashboard/bookmarks/LinkCard.tsx +++ b/apps/web/components/dashboard/bookmarks/LinkCard.tsx @@ -2,6 +2,7 @@ import Image from "next/image"; import Link from "next/link"; +import { useUserSettings } from "@/lib/userSettings"; import type { ZBookmarkTypeLink } from "@karakeep/shared/types/bookmarks"; import { @@ -14,11 +15,25 @@ import { import { BookmarkLayoutAdaptingCard } from "./BookmarkLayoutAdaptingCard"; import FooterLinkURL from "./FooterLinkURL"; +const useOnClickUrl = (bookmark: ZBookmarkTypeLink) => { + const userSettings = useUserSettings(); + return { + urlTarget: + userSettings.bookmarkClickAction === "open_original_link" + ? ("_blank" as const) + : ("_self" as const), + onClickUrl: + userSettings.bookmarkClickAction === "expand_bookmark_preview" + ? `/dashboard/preview/${bookmark.id}` + : bookmark.content.url, + }; +}; + function LinkTitle({ bookmark }: { bookmark: ZBookmarkTypeLink }) { - const link = bookmark.content; - const parsedUrl = new URL(link.url); + const { onClickUrl, urlTarget } = useOnClickUrl(bookmark); + const parsedUrl = new URL(bookmark.content.url); return ( - <Link href={link.url} target="_blank" rel="noreferrer"> + <Link href={onClickUrl} target={urlTarget} rel="noreferrer"> {getBookmarkTitle(bookmark) ?? parsedUrl.host} </Link> ); @@ -31,6 +46,7 @@ function LinkImage({ bookmark: ZBookmarkTypeLink; className?: string; }) { + const { onClickUrl, urlTarget } = useOnClickUrl(bookmark); const link = bookmark.content; const imgComponent = (url: string, unoptimized: boolean) => ( @@ -61,8 +77,8 @@ function LinkImage({ return ( <Link - href={link.url} - target="_blank" + href={onClickUrl} + target={urlTarget} rel="noreferrer" className={className} > diff --git a/apps/web/components/settings/UserOptions.tsx b/apps/web/components/settings/UserOptions.tsx index 33ffc46a..c8aa5e86 100644 --- a/apps/web/components/settings/UserOptions.tsx +++ b/apps/web/components/settings/UserOptions.tsx @@ -1,11 +1,23 @@ "use client"; +import { useEffect } from "react"; +import { useClientConfig } from "@/lib/clientConfig"; import { useTranslation } from "@/lib/i18n/client"; import { useInterfaceLang } from "@/lib/userLocalSettings/bookmarksLayout"; import { updateInterfaceLang } from "@/lib/userLocalSettings/userLocalSettings"; +import { useUserSettings } from "@/lib/userSettings"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { useUpdateUserSettings } from "@karakeep/shared-react/hooks/users"; import { langNameMappings } from "@karakeep/shared/langs"; +import { + ZUserSettings, + zUserSettingsSchema, +} from "@karakeep/shared/types/users"; +import { Form, FormField } from "../ui/form"; import { Label } from "../ui/label"; import { Select, @@ -14,6 +26,7 @@ import { SelectTrigger, SelectValue, } from "../ui/select"; +import { toast } from "../ui/use-toast"; const LanguageSelect = () => { const lang = useInterfaceLang(); @@ -38,6 +51,86 @@ const LanguageSelect = () => { ); }; +export default function UserSettings() { + const { t } = useTranslation(); + const clientConfig = useClientConfig(); + const data = useUserSettings(); + const { mutate } = useUpdateUserSettings({ + onSuccess: () => { + toast({ + description: t("settings.info.user_settings.user_settings_updated"), + }); + }, + onError: () => { + toast({ + description: t("common.something_went_wrong"), + variant: "destructive", + }); + }, + }); + + const bookmarkClickActionTranslation: Record< + ZUserSettings["bookmarkClickAction"], + string + > = { + open_original_link: t("settings.info.user_settings.open_external_url"), + expand_bookmark_preview: t( + "settings.info.user_settings.open_bookmark_details", + ), + }; + + const form = useForm<z.infer<typeof zUserSettingsSchema>>({ + resolver: zodResolver(zUserSettingsSchema), + defaultValues: data, + }); + + // When the actual user setting is loaded, reset the form to the current value + useEffect(() => { + form.reset(data); + }, [data]); + + 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.boomark_click_action")} + </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> + </div> + )} + /> + </Form> + ); +} + export function UserOptions() { const { t } = useTranslation(); @@ -46,9 +139,12 @@ export function UserOptions() { <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-2"> - <Label>{t("settings.info.interface_lang")}</Label> - <LanguageSelect /> + <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> ); diff --git a/apps/web/lib/i18n/locales/en/translation.json b/apps/web/lib/i18n/locales/en/translation.json index 1eef3ac4..48d32f37 100644 --- a/apps/web/lib/i18n/locales/en/translation.json +++ b/apps/web/lib/i18n/locales/en/translation.json @@ -98,7 +98,13 @@ "new_password": "New Password", "confirm_new_password": "Confirm New Password", "options": "Options", - "interface_lang": "Interface Language" + "interface_lang": "Interface Language", + "user_settings": { + "user_settings_updated": "User settings have been updated!", + "boomark_click_action": "Bookmark Click Action", + "open_external_url": "Open Original URL", + "open_bookmark_details": "Open Bookmark Details" + } }, "ai": { "ai_settings": "AI Settings", diff --git a/apps/web/lib/userSettings.tsx b/apps/web/lib/userSettings.tsx new file mode 100644 index 00000000..727c823e --- /dev/null +++ b/apps/web/lib/userSettings.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { createContext, useContext } from "react"; + +import { ZUserSettings } from "@karakeep/shared/types/users"; + +import { api } from "./trpc"; + +export const UserSettingsContext = createContext<ZUserSettings>({ + bookmarkClickAction: "open_original_link", +}); + +export function UserSettingsContextProvider({ + userSettings, + children, +}: { + userSettings: ZUserSettings; + children: React.ReactNode; +}) { + const { data } = api.users.settings.useQuery(undefined, { + initialData: userSettings, + }); + + return ( + <UserSettingsContext.Provider value={data}> + {children} + </UserSettingsContext.Provider> + ); +} + +export function useUserSettings() { + return useContext(UserSettingsContext); +} |
