aboutsummaryrefslogtreecommitdiff
path: root/packages/web/src/app
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-03 22:58:11 -0800
committerFuwn <[email protected]>2026-02-03 23:25:45 -0800
commitfab884c6fabf8deb548b3bd2adb2a4947f2db790 (patch)
treee8afbc46076433d2a0232cbc098204f336c2ef33 /packages/web/src/app
parenttest(sdk): Add Supabase store integration tests (diff)
downloadarchived-imemio-fab884c6fabf8deb548b3bd2adb2a4947f2db790.tar.xz
archived-imemio-fab884c6fabf8deb548b3bd2adb2a4947f2db790.zip
feat(web): Add memory dashboard
Diffstat (limited to 'packages/web/src/app')
-rw-r--r--packages/web/src/app/_components/post.tsx19
-rw-r--r--packages/web/src/app/auth/sign-in/page.tsx50
-rw-r--r--packages/web/src/app/auth/sign-up/page.tsx65
-rw-r--r--packages/web/src/app/dashboard/dashboard-content.tsx122
-rw-r--r--packages/web/src/app/dashboard/page.tsx13
-rw-r--r--packages/web/src/app/layout.tsx14
-rw-r--r--packages/web/src/app/page.tsx88
7 files changed, 250 insertions, 121 deletions
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]">&gt;</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&apos;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&apos;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&apos;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]">&gt;</span> check your email
+ </h1>
+ <p className="text-[#666666]">
+ we&apos;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]">&gt;</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]">&gt;</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]">&gt;</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>
);
}