From e3f187bdc0d4e76e504634a3af4f85e2403bbd7f Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sat, 11 Jan 2025 14:58:22 +1100 Subject: [PATCH] Update settings page --- app/(dashboard)/[tenant]/settings/actions.ts | 12 ++++ app/(dashboard)/[tenant]/settings/page.tsx | 68 ++++++++++++++---- components/core/editable-text.tsx | 69 +++++++++++++++++++ components/project/events/events-calendar.tsx | 2 +- lib/ops/auth.ts | 18 ++++- 5 files changed, 153 insertions(+), 16 deletions(-) create mode 100644 components/core/editable-text.tsx diff --git a/app/(dashboard)/[tenant]/settings/actions.ts b/app/(dashboard)/[tenant]/settings/actions.ts index 1448a3f..779a4a9 100644 --- a/app/(dashboard)/[tenant]/settings/actions.ts +++ b/app/(dashboard)/[tenant]/settings/actions.ts @@ -2,6 +2,7 @@ import { updateUser } from "@/lib/ops/auth"; import { getOwner } from "@/lib/utils/useOwner"; +import { revalidatePath } from "next/cache"; import { cookies } from "next/headers"; export async function saveUserTimezone(timezone: string) { @@ -15,3 +16,14 @@ export async function saveUserTimezone(timezone: string) { const { userId } = await getOwner(); await updateUser(userId, { customData: { timezone } }); } + +export async function updateUserData(payload: FormData) { + const { userId, orgSlug } = await getOwner(); + const key = payload.get("key") as string; + const value = payload.get(key) as string; + await updateUser(userId, { + [key]: value, + }); + revalidatePath(`/${orgSlug}/settings`); + return { success: true }; +} diff --git a/app/(dashboard)/[tenant]/settings/page.tsx b/app/(dashboard)/[tenant]/settings/page.tsx index 0b4aa75..1079675 100644 --- a/app/(dashboard)/[tenant]/settings/page.tsx +++ b/app/(dashboard)/[tenant]/settings/page.tsx @@ -1,16 +1,19 @@ import { logtoConfig } from "@/app/logto"; +import { EditableValue } from "@/components/core/editable-text"; import PageSection from "@/components/core/section"; import PageTitle from "@/components/layout/page-title"; import { blob } from "@/drizzle/schema"; import { bytesToMegabytes } from "@/lib/blobStore"; +import { getUser } from "@/lib/ops/auth"; import { database } from "@/lib/utils/useDatabase"; import { getLogtoContext } from "@logto/next/server-actions"; import { sql } from "drizzle-orm"; import { HardDrive, User2 } from "lucide-react"; import { notFound } from "next/navigation"; +import { updateUserData } from "./actions"; export default async function Settings() { - const { claims, userInfo } = await getLogtoContext(logtoConfig, { + const { claims } = await getLogtoContext(logtoConfig, { fetchUserInfo: true, }); @@ -20,7 +23,7 @@ export default async function Settings() { const db = await database(); - const [storage] = [ + const [storage, user] = await Promise.all([ db .select({ count: sql`count(*)`, @@ -28,7 +31,8 @@ export default async function Settings() { }) .from(blob) .get(), - ]; + getUser(claims.sub), + ]); return ( <> @@ -56,27 +60,63 @@ export default async function Settings() { - -

- - Profile ({userInfo?.username}) -

+ {user ? ( + +

+ + Profile ({user.username}) +

+ +
+
+
+ Name +
+
+
+ +
+
+
-
- {userInfo?.email ? (
Email address
- {userInfo.email} +
- ) : null} -
- + + {user.customData?.timezone ? ( +
+
+ Timezone +
+
+
+ {user.customData?.timezone} +
+
+
+ ) : null} +
+
+ ) : null} ); } diff --git a/components/core/editable-text.tsx b/components/core/editable-text.tsx new file mode 100644 index 0000000..fe47560 --- /dev/null +++ b/components/core/editable-text.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { useState } from "react"; +import { notifyError, notifySuccess } from "../core/toast"; +import { ActionButton } from "../form/button"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; + +export type ResultWithOptionalError = { error?: string; success: boolean }; + +export function EditableValue({ + id, + name, + value, + action, + type, +}: { + id: string | number; + name: string; + value: string | number; + type: "text" | "number"; + action: (data: FormData) => Promise; +}) { + const [isEditing, setIsEditing] = useState(false); + const [localValue, setLocalValue] = useState(value); + + return ( +
{ + try { + const result = await action(formData); + if (result?.error) { + notifyError(result.error); + } else { + notifySuccess("Updated successfully"); + } + } finally { + setIsEditing(false); + } + }} + > + + + {isEditing ? ( +
+ setLocalValue(e.target.value)} + className="w-auto max-w-[160px]" + /> + +
+ ) : ( +
+

{value}

+ +
+ )} +
+ ); +} diff --git a/components/project/events/events-calendar.tsx b/components/project/events/events-calendar.tsx index ab8b515..eed6425 100644 --- a/components/project/events/events-calendar.tsx +++ b/components/project/events/events-calendar.tsx @@ -28,7 +28,7 @@ export default function EventsCalendar({ return (
{ diff --git a/lib/ops/auth.ts b/lib/ops/auth.ts index 2c5324e..770b1b5 100644 --- a/lib/ops/auth.ts +++ b/lib/ops/auth.ts @@ -9,6 +9,22 @@ const tenantId = "default"; * https://openapi.logto.io */ +export interface User { + id: string; + username: string; + primaryEmail: string | null; + name: string | null; + avatar: string | null; + customData: { + timezone: string; + }; + lastSignInAt: number; + createdAt: number; + updatedAt: number; + isSuspended: boolean; + hasPassword: boolean; +} + export interface Organization { tenantId: string; id: string; @@ -47,7 +63,7 @@ export const fetchAccessToken = async () => { }); }; -export const getUser = async (userId: string) => { +export const getUser = async (userId: string): Promise => { const { access_token } = await fetchAccessToken().then((res) => res.json()); if (!access_token) { throw new Error("Access token not found");