diff options
| author | Fuwn <[email protected]> | 2026-02-03 22:58:11 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-03 23:25:45 -0800 |
| commit | fab884c6fabf8deb548b3bd2adb2a4947f2db790 (patch) | |
| tree | e8afbc46076433d2a0232cbc098204f336c2ef33 /packages | |
| parent | test(sdk): Add Supabase store integration tests (diff) | |
| download | archived-imemio-fab884c6fabf8deb548b3bd2adb2a4947f2db790.tar.xz archived-imemio-fab884c6fabf8deb548b3bd2adb2a4947f2db790.zip | |
feat(web): Add memory dashboard
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/web/package.json | 1 | ||||
| -rw-r--r-- | packages/web/src/app/_components/post.tsx | 19 | ||||
| -rw-r--r-- | packages/web/src/app/auth/sign-in/page.tsx | 50 | ||||
| -rw-r--r-- | packages/web/src/app/auth/sign-up/page.tsx | 65 | ||||
| -rw-r--r-- | packages/web/src/app/dashboard/dashboard-content.tsx | 122 | ||||
| -rw-r--r-- | packages/web/src/app/dashboard/page.tsx | 13 | ||||
| -rw-r--r-- | packages/web/src/app/layout.tsx | 14 | ||||
| -rw-r--r-- | packages/web/src/app/page.tsx | 88 | ||||
| -rw-r--r-- | packages/web/src/server/api/root.ts | 2 | ||||
| -rw-r--r-- | packages/web/src/server/api/routers/memory.ts | 80 | ||||
| -rw-r--r-- | packages/web/src/styles/globals.css | 35 |
11 files changed, 368 insertions, 121 deletions
diff --git a/packages/web/package.json b/packages/web/package.json index b140111..293ee85 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -18,6 +18,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@imemio/sdk": "workspace:*", "@supabase/ssr": "^0.8.0", "@supabase/supabase-js": "^2.94.0", "@t3-oss/env-nextjs": "^0.12.0", diff --git a/packages/web/src/app/_components/post.tsx b/packages/web/src/app/_components/post.tsx index 9a1251f..5fb8ba8 100644 --- a/packages/web/src/app/_components/post.tsx +++ b/packages/web/src/app/_components/post.tsx @@ -15,34 +15,37 @@ export function LatestPost() { }); return ( - <div className="w-full max-w-xs"> + <div className="w-full max-w-sm"> {latestPost ? ( - <p className="truncate">Your most recent post: {latestPost.name}</p> + <p className="truncate text-[#999999]"> + your most recent post:{" "} + <span className="text-white">{latestPost.name}</span> + </p> ) : ( - <p>You have no posts yet.</p> + <p className="text-[#666666]">you have no posts yet.</p> )} <form - className="flex flex-col gap-2" + className="mt-3 flex flex-col gap-3" onSubmit={(formSubmitEvent) => { formSubmitEvent.preventDefault(); createPost.mutate({ name }); }} > <input - className="w-full rounded-full bg-white/10 px-4 py-2 text-white" + className="w-full border border-[#2a2a2a] bg-[#0f0f0f] px-3 py-2 text-white placeholder:text-[#666666] focus:border-[#666666]" onChange={(inputChangeEvent) => setName(inputChangeEvent.target.value) } - placeholder="Title" + placeholder="title" type="text" value={name} /> <button - className="rounded-full bg-white/10 px-10 py-3 font-semibold transition hover:bg-white/20" + className="w-full border border-[#2a2a2a] bg-[#0f0f0f] px-4 py-2 text-white transition-colors duration-100 hover:border-[#666666] disabled:text-[#666666] disabled:hover:border-[#2a2a2a]" disabled={createPost.isPending} type="submit" > - {createPost.isPending ? "Submitting..." : "Submit"} + {createPost.isPending ? "submitting ..." : "submit"} </button> </form> </div> diff --git a/packages/web/src/app/auth/sign-in/page.tsx b/packages/web/src/app/auth/sign-in/page.tsx index 3c4c10a..2bf6767 100644 --- a/packages/web/src/app/auth/sign-in/page.tsx +++ b/packages/web/src/app/auth/sign-in/page.tsx @@ -1,5 +1,6 @@ "use client"; +import Link from "next/link"; import { useState } from "react"; import { createClient } from "~/lib/supabase/client"; @@ -32,57 +33,64 @@ export default function SignInPage() { } return ( - <div className="flex min-h-screen items-center justify-center"> - <div className="w-full max-w-md space-y-8 p-8"> - <h1 className="text-center text-2xl font-bold">Sign In</h1> + <main className="flex min-h-screen flex-col items-center justify-center bg-[#070707]"> + <div className="w-full max-w-sm px-4"> + <h1 className="mb-8 text-center text-2xl tracking-tight text-white"> + <span className="text-[#999999]">></span> sign in + </h1> - <form className="space-y-6" onSubmit={handleSubmit}> - <div> - <label className="block text-sm font-medium" htmlFor="email"> - Email + <form className="flex flex-col gap-4" onSubmit={handleSubmit}> + <div className="flex flex-col gap-2"> + <label className="text-sm text-[#666666]" htmlFor="email"> + email </label> <input - className="mt-1 block w-full rounded-md border px-3 py-2" + className="w-full border border-[#2a2a2a] bg-[#0f0f0f] px-3 py-2 text-white placeholder:text-[#666666] focus:border-[#666666]" id="email" onChange={(event) => setEmail(event.target.value)} + placeholder="[email protected]" required type="email" value={email} /> </div> - <div> - <label className="block text-sm font-medium" htmlFor="password"> - Password + <div className="flex flex-col gap-2"> + <label className="text-sm text-[#666666]" htmlFor="password"> + password </label> <input - className="mt-1 block w-full rounded-md border px-3 py-2" + className="w-full border border-[#2a2a2a] bg-[#0f0f0f] px-3 py-2 text-white placeholder:text-[#666666] focus:border-[#666666]" id="password" onChange={(event) => setPassword(event.target.value)} + placeholder="••••••••" required type="password" value={password} /> </div> - {error && <p className="text-sm text-red-500">{error}</p>} + {error && <p className="text-sm text-[#999999]">{error}</p>} <button - className="w-full rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50" + className="mt-2 w-full border border-[#2a2a2a] bg-[#0f0f0f] px-4 py-2 text-white transition-colors duration-100 hover:border-[#666666] disabled:text-[#666666] disabled:hover:border-[#2a2a2a]" disabled={loading} type="submit" > - {loading ? "Signing in ..." : "Sign In"} + {loading ? "signing in ..." : "sign in"} </button> </form> - <p className="text-center text-sm"> - Don't have an account?{" "} - <a className="text-blue-600 hover:underline" href="/auth/sign-up"> - Sign Up - </a> + <p className="mt-6 text-center text-sm text-[#666666]"> + don't have an account?{" "} + <Link + className="text-[#999999] underline underline-offset-2 transition-colors duration-100 hover:text-white" + href="/auth/sign-up" + > + sign up + </Link> </p> </div> - </div> + </main> ); } diff --git a/packages/web/src/app/auth/sign-up/page.tsx b/packages/web/src/app/auth/sign-up/page.tsx index 94a501c..981692e 100644 --- a/packages/web/src/app/auth/sign-up/page.tsx +++ b/packages/web/src/app/auth/sign-up/page.tsx @@ -1,5 +1,6 @@ "use client"; +import Link from "next/link"; import { useState } from "react"; import { createClient } from "~/lib/supabase/client"; @@ -37,68 +38,80 @@ export default function SignUpPage() { if (success) { return ( - <div className="flex min-h-screen items-center justify-center"> - <div className="w-full max-w-md space-y-8 p-8 text-center"> - <h1 className="text-2xl font-bold">Check your email</h1> - <p>We've sent you a confirmation link to {email}</p> + <main className="flex min-h-screen flex-col items-center justify-center bg-[#070707]"> + <div className="w-full max-w-sm px-4 text-center"> + <h1 className="mb-4 text-2xl tracking-tight text-white"> + <span className="text-[#999999]">></span> check your email + </h1> + <p className="text-[#666666]"> + we've sent a confirmation link to{" "} + <span className="text-[#999999]">{email}</span> + </p> </div> - </div> + </main> ); } return ( - <div className="flex min-h-screen items-center justify-center"> - <div className="w-full max-w-md space-y-8 p-8"> - <h1 className="text-center text-2xl font-bold">Sign Up</h1> + <main className="flex min-h-screen flex-col items-center justify-center bg-[#070707]"> + <div className="w-full max-w-sm px-4"> + <h1 className="mb-8 text-center text-2xl tracking-tight text-white"> + <span className="text-[#999999]">></span> sign up + </h1> - <form className="space-y-6" onSubmit={handleSubmit}> - <div> - <label className="block text-sm font-medium" htmlFor="email"> - Email + <form className="flex flex-col gap-4" onSubmit={handleSubmit}> + <div className="flex flex-col gap-2"> + <label className="text-sm text-[#666666]" htmlFor="email"> + email </label> <input - className="mt-1 block w-full rounded-md border px-3 py-2" + className="w-full border border-[#2a2a2a] bg-[#0f0f0f] px-3 py-2 text-white placeholder:text-[#666666] focus:border-[#666666]" id="email" onChange={(event) => setEmail(event.target.value)} + placeholder="[email protected]" required type="email" value={email} /> </div> - <div> - <label className="block text-sm font-medium" htmlFor="password"> - Password + <div className="flex flex-col gap-2"> + <label className="text-sm text-[#666666]" htmlFor="password"> + password </label> <input - className="mt-1 block w-full rounded-md border px-3 py-2" + className="w-full border border-[#2a2a2a] bg-[#0f0f0f] px-3 py-2 text-white placeholder:text-[#666666] focus:border-[#666666]" id="password" minLength={6} onChange={(event) => setPassword(event.target.value)} + placeholder="••••••••" required type="password" value={password} /> </div> - {error && <p className="text-sm text-red-500">{error}</p>} + {error && <p className="text-sm text-[#999999]">{error}</p>} <button - className="w-full rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50" + className="mt-2 w-full border border-[#2a2a2a] bg-[#0f0f0f] px-4 py-2 text-white transition-colors duration-100 hover:border-[#666666] disabled:text-[#666666] disabled:hover:border-[#2a2a2a]" disabled={loading} type="submit" > - {loading ? "Creating account ..." : "Sign Up"} + {loading ? "creating account ..." : "sign up"} </button> </form> - <p className="text-center text-sm"> - Already have an account?{" "} - <a className="text-blue-600 hover:underline" href="/auth/sign-in"> - Sign In - </a> + <p className="mt-6 text-center text-sm text-[#666666]"> + already have an account?{" "} + <Link + className="text-[#999999] underline underline-offset-2 transition-colors duration-100 hover:text-white" + href="/auth/sign-in" + > + sign in + </Link> </p> </div> - </div> + </main> ); } diff --git a/packages/web/src/app/dashboard/dashboard-content.tsx b/packages/web/src/app/dashboard/dashboard-content.tsx new file mode 100644 index 0000000..da681b8 --- /dev/null +++ b/packages/web/src/app/dashboard/dashboard-content.tsx @@ -0,0 +1,122 @@ +"use client"; + +import Link from "next/link"; +import { useState } from "react"; +import { api } from "~/trpc/react"; + +function MemoryList() { + const [memories] = api.memory.list.useSuspenseQuery(); + const trpcUtilities = api.useUtils(); + const deleteMemory = api.memory.delete.useMutation({ + onSuccess: async () => { + await trpcUtilities.memory.invalidate(); + }, + }); + + if (memories.length === 0) { + return ( + <div className="border border-[#2a2a2a] bg-[#0f0f0f] p-4 text-center"> + <p className="text-[#666666]"> + no memories yet. create your first one below. + </p> + </div> + ); + } + + return ( + <div className="flex w-full flex-col gap-2"> + {memories.map((memory) => ( + <div + className="flex items-start justify-between gap-4 border border-[#2a2a2a] bg-[#0f0f0f] p-3" + key={memory.id} + > + <div className="flex-1"> + <p className="text-white">{memory.content}</p> + <p className="mt-1 text-xs text-[#666666]"> + {new Date(memory.createdAt).toLocaleString()} + </p> + </div> + <button + className="border border-[#2a2a2a] bg-[#0f0f0f] px-3 py-1 text-sm text-[#999999] transition hover:border-[#666666] hover:text-white" + disabled={deleteMemory.isPending} + onClick={() => deleteMemory.mutate({ id: memory.id })} + type="button" + > + {deleteMemory.isPending ? "deleting ..." : "delete"} + </button> + </div> + ))} + </div> + ); +} + +function CreateMemoryForm() { + const [content, setContent] = useState(""); + const trpcUtilities = api.useUtils(); + const createMemory = api.memory.create.useMutation({ + onSuccess: async () => { + await trpcUtilities.memory.invalidate(); + setContent(""); + }, + }); + + return ( + <form + className="flex w-full flex-col gap-3" + onSubmit={(formSubmitEvent) => { + formSubmitEvent.preventDefault(); + + if (content.trim()) { + createMemory.mutate({ content }); + } + }} + > + <textarea + className="w-full border border-[#2a2a2a] bg-[#0f0f0f] px-3 py-2 text-white placeholder:text-[#666666] focus:border-[#666666] focus:outline-none" + onChange={(textareaChangeEvent) => + setContent(textareaChangeEvent.target.value) + } + placeholder="enter your memory content ..." + rows={3} + value={content} + /> + <button + className="border border-[#2a2a2a] bg-[#0f0f0f] px-6 py-2 text-white transition hover:border-[#666666] disabled:text-[#666666] disabled:hover:border-[#2a2a2a]" + disabled={createMemory.isPending || !content.trim()} + type="submit" + > + {createMemory.isPending ? "creating ..." : "create memory"} + </button> + </form> + ); +} + +export function DashboardContent() { + return ( + <main className="flex min-h-screen flex-col items-center bg-[#070707]"> + <div className="container flex max-w-2xl flex-col items-center gap-6 px-4 py-12"> + <div className="flex w-full items-center justify-between"> + <h1 className="text-2xl tracking-tight text-white"> + <span className="text-[#999999]">></span> memory dashboard + </h1> + <Link + className="border border-[#2a2a2a] bg-[#0f0f0f] px-4 py-1 text-[#999999] transition hover:border-[#666666] hover:text-white" + href="/" + > + home + </Link> + </div> + + <section className="w-full"> + <h2 className="mb-3 text-sm text-[#666666]">your memories</h2> + <MemoryList /> + </section> + + <section className="w-full"> + <h2 className="mb-3 text-sm text-[#666666]">create new memory</h2> + <CreateMemoryForm /> + </section> + </div> + </main> + ); +} diff --git a/packages/web/src/app/dashboard/page.tsx b/packages/web/src/app/dashboard/page.tsx new file mode 100644 index 0000000..f1e0f5d --- /dev/null +++ b/packages/web/src/app/dashboard/page.tsx @@ -0,0 +1,13 @@ +import { redirect } from "next/navigation"; +import { getUser } from "~/server/auth"; +import { DashboardContent } from "./dashboard-content"; + +export default async function DashboardPage() { + const user = await getUser(); + + if (!user) { + redirect("/auth/sign-in"); + } + + return <DashboardContent />; +} diff --git a/packages/web/src/app/layout.tsx b/packages/web/src/app/layout.tsx index 163dbef..bb542b8 100644 --- a/packages/web/src/app/layout.tsx +++ b/packages/web/src/app/layout.tsx @@ -1,24 +1,24 @@ import "~/styles/globals.css"; import type { Metadata } from "next"; -import { Geist } from "next/font/google"; +import { JetBrains_Mono } from "next/font/google"; import { TRPCReactProvider } from "~/trpc/react"; export const metadata: Metadata = { - title: "Create T3 App", - description: "Generated by create-t3-app", + title: "imemio", + description: "Memory management dashboard", icons: [{ rel: "icon", url: "/favicon.ico" }], }; -const geist = Geist({ +const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"], - variable: "--font-geist-sans", + variable: "--font-mono", }); export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { return ( - <html className={`${geist.variable}`} lang="en"> - <body> + <html className={`${jetbrainsMono.variable}`} lang="en"> + <body className="bg-[#070707] font-mono text-white antialiased"> <TRPCReactProvider>{children}</TRPCReactProvider> </body> </html> diff --git a/packages/web/src/app/page.tsx b/packages/web/src/app/page.tsx index 2539f3a..efb6912 100644 --- a/packages/web/src/app/page.tsx +++ b/packages/web/src/app/page.tsx @@ -1,68 +1,38 @@ import Link from "next/link"; -import { LatestPost } from "~/app/_components/post"; -import { auth } from "~/server/auth"; -import { api, HydrateClient } from "~/trpc/server"; +import { getSession } from "~/server/auth"; export default async function Home() { - const hello = await api.post.hello({ text: "from tRPC" }); - const session = await auth(); - - if (session?.user) { - void api.post.getLatest.prefetch(); - } + const session = await getSession(); return ( - <HydrateClient> - <main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white"> - <div className="container flex flex-col items-center justify-center gap-12 px-4 py-16"> - <h1 className="font-extrabold text-5xl tracking-tight sm:text-[5rem]"> - Create <span className="text-[hsl(280,100%,70%)]">T3</span> App - </h1> - <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-8"> - <Link - className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20" - href="https://create.t3.gg/en/usage/first-steps" - target="_blank" - > - <h3 className="font-bold text-2xl">First Steps →</h3> - <div className="text-lg"> - Just the basics - Everything you need to know to set up your - database and authentication. - </div> - </Link> - <Link - className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20" - href="https://create.t3.gg/en/introduction" - target="_blank" - > - <h3 className="font-bold text-2xl">Documentation →</h3> - <div className="text-lg"> - Learn more about Create T3 App, the libraries it uses, and how - to deploy it. - </div> - </Link> - </div> - <div className="flex flex-col items-center gap-2"> - <p className="text-2xl text-white"> - {hello ? hello.greeting : "Loading tRPC query..."} - </p> - - <div className="flex flex-col items-center justify-center gap-4"> - <p className="text-center text-2xl text-white"> - {session && <span>Logged in as {session.user?.name}</span>} - </p> - <Link - className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20" - href={session ? "/api/auth/signout" : "/api/auth/signin"} - > - {session ? "Sign out" : "Sign in"} - </Link> - </div> + <main className="flex min-h-screen flex-col items-center justify-center bg-[#070707]"> + <div className="container flex flex-col items-center justify-center gap-8 px-4 py-12"> + <h1 className="text-4xl tracking-tight text-white sm:text-5xl"> + <span className="text-[#999999]">></span> imemio + </h1> + <p className="text-[#666666]">memory management system</p> + <Link + className="flex max-w-xs flex-col gap-2 border border-[#2a2a2a] bg-[#0f0f0f] p-4 transition hover:border-[#666666]" + href="/dashboard" + > + <h3 className="text-lg text-white">dashboard</h3> + <div className="text-sm text-[#999999]"> + View and manage your memories. Create new memories and organise your + thoughts. </div> - - {session?.user && <LatestPost />} + </Link> + <div className="flex flex-col items-center gap-3"> + <p className="text-center text-[#666666]"> + {session && <span>logged in as {session.user?.email}</span>} + </p> + <Link + className="border border-[#2a2a2a] bg-[#0f0f0f] px-6 py-2 text-white transition hover:border-[#666666]" + href={session ? "/api/auth/signout" : "/auth/sign-in"} + > + {session ? "sign out" : "sign in"} + </Link> </div> - </main> - </HydrateClient> + </div> + </main> ); } diff --git a/packages/web/src/server/api/root.ts b/packages/web/src/server/api/root.ts index 374285c..b0fd7ba 100644 --- a/packages/web/src/server/api/root.ts +++ b/packages/web/src/server/api/root.ts @@ -1,3 +1,4 @@ +import { memoryRouter } from "~/server/api/routers/memory"; import { postRouter } from "~/server/api/routers/post"; import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc"; @@ -7,6 +8,7 @@ import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc"; * All routers added in /api/routers should be manually added here. */ export const appRouter = createTRPCRouter({ + memory: memoryRouter, post: postRouter, }); diff --git a/packages/web/src/server/api/routers/memory.ts b/packages/web/src/server/api/routers/memory.ts new file mode 100644 index 0000000..d1bb849 --- /dev/null +++ b/packages/web/src/server/api/routers/memory.ts @@ -0,0 +1,80 @@ +import { SupabaseProjectStore, SupabaseStore } from "@imemio/sdk"; +import type { SupabaseClient } from "@supabase/supabase-js"; +import { z } from "zod"; +import { createClient } from "~/lib/supabase/server"; +import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; + +const defaultProjectName = "default"; + +async function getOrCreateDefaultProject( + supabaseClient: SupabaseClient, + userId: string, +): Promise<string> { + const projectStore = new SupabaseProjectStore(supabaseClient, userId); + const projectsResult = await projectStore.list(); + + if (projectsResult.success && projectsResult.value.length > 0) { + const existingDefaultProject = projectsResult.value.find( + (project) => project.name === defaultProjectName, + ); + + if (existingDefaultProject) { + return existingDefaultProject.id; + } + + const firstProject = projectsResult.value[0]; + + if (firstProject) { + return firstProject.id; + } + } + + const createResult = await projectStore.create({ name: defaultProjectName }); + + if (!createResult.success) { + throw new Error("Failed to create default project"); + } + + return createResult.value.id; +} + +export const memoryRouter = createTRPCRouter({ + list: protectedProcedure.query(async ({ ctx }) => { + const supabaseClient = await createClient(); + const memoryStore = new SupabaseStore(supabaseClient, ctx.user.id); + const memories = await memoryStore.list(); + + return memories; + }), + + create: protectedProcedure + .input(z.object({ content: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + const supabaseClient = await createClient(); + const projectId = await getOrCreateDefaultProject( + supabaseClient, + ctx.user.id, + ); + const memoryStore = new SupabaseStore(supabaseClient, ctx.user.id); + const memory = await memoryStore.create({ + content: input.content, + projectId, + }); + + return memory; + }), + + delete: protectedProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ ctx, input }) => { + const supabaseClient = await createClient(); + const memoryStore = new SupabaseStore(supabaseClient, ctx.user.id); + const deleteResult = await memoryStore.delete(input.id); + + if (!deleteResult.success) { + throw new Error(`Memory not found: ${input.id}`); + } + + return { success: true }; + }), +}); diff --git a/packages/web/src/styles/globals.css b/packages/web/src/styles/globals.css index 7fdd803..38f76a5 100644 --- a/packages/web/src/styles/globals.css +++ b/packages/web/src/styles/globals.css @@ -4,4 +4,39 @@ --font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + + --color-bg-primary: #070707; + --color-bg-secondary: #0f0f0f; + --color-bg-tertiary: #1a1a1a; + --color-border: #2a2a2a; + --color-text-primary: #ffffff; + --color-text-secondary: #999999; + --color-text-tertiary: #666666; + --color-text-dim: #444444; +} + +* { + border-radius: 0 !important; +} + +::selection { + background-color: #2a2a2a; + color: #ffffff; +} + +input, +textarea, +button { + font-family: inherit; +} + +input::placeholder, +textarea::placeholder { + color: var(--color-text-tertiary); +} + +input:focus, +textarea:focus { + outline: none; + border-color: var(--color-text-tertiary); } |