fix: update Dockerfile and remove .next from git

This commit is contained in:
2026-01-14 18:49:57 +00:00
parent 91fc911523
commit ed2c303d6f
252 changed files with 1537 additions and 10866 deletions

View File

@@ -0,0 +1,16 @@
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
export default function MarketingLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<>
<Navbar />
{children}
<Footer />
</>
);
}

View File

@@ -1,21 +1,48 @@
import { NextResponse } from 'next/server';
import { createClient } from '@/utils/supabase/server'
import { redirect } from 'next/navigation'
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
try {
// valid JSON body check (optional, but good for parsing)
const body = await request.json();
const supabase = await createClient()
// Log the request for now - placeholder for actual logic
console.log('Delete account request received:', body);
// Check if user is authenticated
const {
data: { user },
} = await supabase.auth.getUser()
return NextResponse.json(
{ message: 'Request received. Account deletion processed.' },
{ status: 200 }
);
} catch (error) {
return NextResponse.json(
{ error: 'Invalid request body' },
{ status: 400 }
);
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Perform soft deletes
const now = new Date().toISOString()
// 1. Soft delete usage data (card_progress)
await supabase
.from('card_progress')
.update({ deleted_at: now })
.eq('user_id', user.id)
// 2. Soft delete content (decks)
await supabase
.from('decks')
.update({ deleted_at: now })
.eq('creator_id', user.id)
// 3. Soft delete organization (folders)
await supabase
.from('folders')
.update({ deleted_at: now })
.eq('user_id', user.id)
// 4. Soft delete profile
await supabase
.from('profiles')
.update({ deleted_at: now })
.eq('id', user.id)
// 5. Sign out
await supabase.auth.signOut()
return NextResponse.redirect(new URL('/', request.url))
}

29
app/app/layout.tsx Normal file
View File

@@ -0,0 +1,29 @@
import { createClient } from '@/utils/supabase/server'
import { redirect } from 'next/navigation'
import AppNavbar from '@/components/AppNavbar'
export default async function AppLayout({
children,
}: {
children: React.ReactNode
}) {
const supabase = await createClient()
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) {
return redirect('/login')
}
return (
<div className="min-h-screen bg-background">
<AppNavbar user={user} />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{children}
</main>
</div>
)
}

34
app/app/page.tsx Normal file
View File

@@ -0,0 +1,34 @@
import { createClient } from '@/utils/supabase/server'
import DeckList from '@/components/DeckList'
import { redirect } from 'next/navigation'
export default async function Dashboard() {
const supabase = await createClient()
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) {
return redirect('/login')
}
const { data: decks } = await supabase
.from('decks')
.select('*')
.eq('creator_id', user.id)
.order('created_at', { ascending: false })
return (
<div>
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold text-white">Your Decks</h1>
<p className="text-gray-400 mt-1">Manage and study your flashcard decks.</p>
</div>
</div>
<DeckList decks={decks || []} />
</div>
)
}

40
app/app/settings/page.tsx Normal file
View File

@@ -0,0 +1,40 @@
import { createClient } from '@/utils/supabase/server';
import { redirect } from 'next/navigation';
import DeleteAccountSection from '@/components/DeleteAccountSection';
export default async function SettingsPage() {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
redirect('/login');
}
return (
<div className="space-y-6">
<div className="border-b border-gray-800 pb-5">
<h1 className="text-2xl font-bold text-white">Settings</h1>
<p className="mt-2 text-sm text-gray-400">
Manage your account settings and preferences.
</p>
</div>
<div className="bg-[#0B0F17] border border-gray-800 rounded-xl p-6">
<h2 className="text-lg font-semibold text-white mb-4">Account Information</h2>
<div className="grid gap-4 max-w-xl">
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Email Address</label>
<input
type="email"
value={user.email}
disabled
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-gray-300 focus:outline-none focus:border-primary cursor-not-allowed opacity-75"
/>
</div>
</div>
</div>
<DeleteAccountSection />
</div>
);
}

View File

@@ -0,0 +1,27 @@
import { NextResponse } from 'next/server'
import { createClient } from '@/utils/supabase/server'
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
const next = searchParams.get('next') ?? '/app'
const error = searchParams.get('error')
const error_description = searchParams.get('error_description')
if (code) {
const supabase = await createClient()
const { error: supabaseError } = await supabase.auth.exchangeCodeForSession(code)
if (!supabaseError) {
await supabase.rpc('reactivate_profile')
return NextResponse.redirect(`${origin}${next}`)
}
}
// If there's an error from Supabase or from the URL, forward it
if (error) {
return NextResponse.redirect(`${origin}/login?error=${globalThis.encodeURIComponent(error_description || error)}`)
}
// return the user to an error page with instructions
return NextResponse.redirect(`${origin}/login?code_error=true`)
}

11
app/auth/signout/route.ts Normal file
View File

@@ -0,0 +1,11 @@
import { createClient } from '@/utils/supabase/server'
import { redirect } from 'next/navigation'
export async function POST(request: Request) {
const requestUrl = new URL(request.url)
const supabase = await createClient()
await supabase.auth.signOut()
return redirect(`${requestUrl.origin}/login`)
}

View File

@@ -0,0 +1,98 @@
import Link from 'next/link'
import Image from 'next/image'
import logo from '@/assets/images/icon.png'
import Footer from '@/components/Footer'
export default function DeleteAccountInfo() {
return (
<div className="min-h-screen bg-background flex flex-col">
<header className="border-b border-gray-800 bg-background-dark/80 backdrop-blur-md sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<Link href="/" className="flex items-center gap-2">
<Image
src={logo}
alt="Nemia Logo"
width={32}
height={32}
className="rounded-lg object-contain"
/>
<span className="font-display font-bold text-xl tracking-tight text-white">
Nemia
</span>
</Link>
<Link
href="/login"
className="text-sm font-semibold text-gray-300 hover:text-white transition-colors"
>
Log In
</Link>
</div>
</header>
<main className="flex-grow max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<h1 className="text-4xl font-bold text-white mb-6">
Request Account Deletion
</h1>
<div className="prose prose-invert max-w-none space-y-8 text-gray-300">
<section>
<p className="text-lg leading-relaxed">
We respect your privacy and your right to control your personal
data. If you wish to delete your Nemia account and all associated
data, you can do so directly within the application.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">
How to delete your account
</h2>
<ol className="list-decimal pl-5 space-y-2 marker:text-primary">
<li>
<Link href="/login" className="text-primary hover:underline">
Log in
</Link>{' '}
to your account on the Nemia web app.
</li>
<li>
Navigate to the <strong>Dashboard</strong>.
</li>
<li>
In the top navigation bar, locate the user menu (or your email).
</li>
<li>
Click the <strong>"Delete Account"</strong> button.
</li>
<li>
Confirm the deletion in the modal that appears by typing "DELETE".
</li>
</ol>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">
What happens to your data?
</h2>
<p>
When you confirm account deletion, the following actions occur
immediately:
</p>
<ul className="list-disc pl-5 space-y-2 marker:text-gray-500">
<li>
<strong>Profile Deactivation:</strong> Your user profile is deactivated and you are signed out.
</li>
<li>
<strong>Content Removal:</strong> All flashcards, decks, folders, and study progress created by you are explicitly marked as deleted and become inaccessible.
</li>
<li>
<strong>Data Retention:</strong> Your data is "soft deleted" in our database to allow for immediate service application but is effectively removed from all user-facing systems. If you wish for a complete permanent purge of your data record, please contact support at <a href="mailto:im@mati.ss" className="text-primary hover:underline">im@mati.ss</a>.
</li>
</ul>
</section>
</div>
</main>
<Footer />
</div>
)
}

View File

@@ -1,21 +1,24 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@variant dark (&:where(.dark, .dark *));
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
@theme {
--color-primary: #00E0B8;
--color-secondary: #6B7280;
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}
--color-background-dark: #0F111A;
--color-surface-dark: #1E202E;
--color-surface-accent: #2D3042;
--color-text-light: #E5E7EB;
--color-text-muted: #9CA3AF;
--font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
--font-display: var(--font-poppins), ui-sans-serif, system-ui, sans-serif;
--radius-DEFAULT: 0.75rem;
--radius-xl: 1rem;
--radius-2xl: 1.5rem;
--shadow-glow: 0 0 20px rgba(0, 224, 184, 0.15);
}

View File

@@ -1,8 +1,6 @@
import type { Metadata } from "next";
import { Inter, Poppins } from "next/font/google";
import "./globals.css";
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
const poppins = Poppins({
@@ -26,9 +24,7 @@ export default function RootLayout({
<body
className={`${inter.variable} ${poppins.variable} bg-background-dark text-text-light font-sans min-h-screen selection:bg-primary selection:text-background-dark`}
>
<Navbar />
{children}
<Footer />
</body>
</html>
);

45
app/login/actions.ts Normal file
View File

@@ -0,0 +1,45 @@
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { createClient } from '@/utils/supabase/server'
export async function login(formData: FormData) {
const supabase = await createClient()
const email = formData.get('email') as string
const password = formData.get('password') as string
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) {
redirect('/login?error=Could not authenticate user')
}
revalidatePath('/', 'layout')
await supabase.rpc('reactivate_profile')
redirect('/app')
}
export async function signup(formData: FormData) {
const supabase = await createClient()
const email = formData.get('email') as string
const password = formData.get('password') as string
const { error } = await supabase.auth.signUp({
email,
password,
})
if (error) {
redirect('/login?error=Could not authenticate user')
}
revalidatePath('/', 'layout')
redirect('/app')
}

111
app/login/page.tsx Normal file
View File

@@ -0,0 +1,111 @@
import { login, signup } from './actions'
import Link from 'next/link'
import { use } from 'react'
import GoogleSignInButton from '@/components/GoogleSignInButton'
export default function LoginPage({
searchParams,
}: {
searchParams: Promise<{ message: string, error: string }>
}) {
const params = use(searchParams)
return (
<div className="flex-1 flex flex-col w-full px-8 sm:max-w-md justify-center gap-2 mx-auto min-h-screen">
<Link
href="/"
className="absolute left-8 top-8 py-2 px-4 rounded-md no-underline text-foreground bg-btn-background hover:bg-btn-background-hover flex items-center group text-sm text-zinc-400 hover:text-white transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4 transition-transform group-hover:-translate-x-1"
>
<polyline points="15 18 9 12 15 6" />
</svg>
Back
</Link>
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2 text-center">
<h1 className="text-2xl font-bold text-white">Welcome back</h1>
<p className="text-zinc-400">Sign in to your account to continue</p>
</div>
<div className="w-full">
<GoogleSignInButton />
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-zinc-800" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-zinc-500">
Or continue with
</span>
</div>
</div>
<form className="animate-in flex-1 flex flex-col w-full justify-center gap-4 text-foreground">
<div className="grid gap-2">
<label className="text-md text-zinc-300" htmlFor="email">
Email
</label>
<input
className="rounded-md px-4 py-2 bg-zinc-900 border border-zinc-800 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
name="email"
placeholder="you@example.com"
required
/>
</div>
<div className="grid gap-2">
<label className="text-md text-zinc-300" htmlFor="password">
Password
</label>
<input
className="rounded-md px-4 py-2 bg-zinc-900 border border-zinc-800 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
type="password"
name="password"
placeholder="••••••••"
required
/>
</div>
<button
formAction={login}
className="bg-indigo-600 rounded-md px-4 py-2 text-white hover:bg-indigo-700 active:scale-95 transition-all mt-2"
>
Sign In
</button>
<button
formAction={signup}
className="border border-zinc-800 rounded-md px-4 py-2 text-zinc-300 hover:bg-zinc-900 hover:text-white transition-all"
>
Sign Up
</button>
{params?.message && (
<p className="mt-4 p-4 bg-foreground/10 text-foreground text-center bg-zinc-900 rounded-md">
{params.message}
</p>
)}
{params?.error && (
<p className="mt-4 p-4 text-red-400 text-center bg-red-900/10 border border-red-900/20 rounded-md">
{params.error}
</p>
)}
</form>
</div>
</div>
)
}

27
app/not-found.tsx Normal file
View File

@@ -0,0 +1,27 @@
import Link from 'next/link'
export default function NotFound() {
return (
<div className="min-h-screen bg-background flex flex-col items-center justify-center p-4">
<div className="text-center space-y-6 max-w-md">
<h1 className="text-9xl font-bold text-primary opacity-20 select-none">404</h1>
<div className="-mt-16 space-y-4">
<h2 className="text-3xl font-bold text-white">Page not found</h2>
<p className="text-gray-400">
Sorry, we couldn't find the page you're looking for. It might have been moved or deleted.
</p>
</div>
<div className="pt-8">
<Link
href="/"
className="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-full shadow-glow text-gray-900 bg-primary hover:brightness-110 transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
Return Home
</Link>
</div>
</div>
</div>
)
}