Add select members page

This commit is contained in:
tonkaew131 2025-07-27 17:33:04 +07:00
parent 027ac0bc0c
commit bde97a3c42
12 changed files with 1493 additions and 7659 deletions

View File

@ -4,57 +4,109 @@
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 210 16.6667% 97.6471%;
--foreground: 0 0% 3.9%; --foreground: 240 41.4634% 8.0392%;
--card: 0 0% 100%; --card: 0 0% 100%;
--card-foreground: 0 0% 3.9%; --card-foreground: 240 41.4634% 8.0392%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%; --popover-foreground: 240 41.4634% 8.0392%;
--primary: 0 0% 9%; --primary: 312.9412 100% 50%;
--primary-foreground: 0 0% 98%; --primary-foreground: 0 0% 100%;
--secondary: 0 0% 96.1%; --secondary: 240 100% 97.0588%;
--secondary-foreground: 0 0% 9%; --secondary-foreground: 240 41.4634% 8.0392%;
--muted: 0 0% 96.1%; --muted: 240 100% 97.0588%;
--muted-foreground: 0 0% 45.1%; --muted-foreground: 240 41.4634% 8.0392%;
--accent: 0 0% 96.1%; --accent: 168 100% 50%;
--accent-foreground: 0 0% 9%; --accent-foreground: 240 41.4634% 8.0392%;
--destructive: 0 84.2% 60.2%; --destructive: 14.3529 100% 50%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 100%;
--border: 0 0% 89.8%; --border: 198 18.5185% 89.4118%;
--input: 0 0% 89.8%; --input: 198 18.5185% 89.4118%;
--ring: 0 0% 3.9%; --ring: 312.9412 100% 50%;
--chart-1: 12 76% 61%; --chart-1: 312.9412 100% 50%;
--chart-2: 173 58% 39%; --chart-2: 273.8824 100% 50%;
--chart-3: 197 37% 24%; --chart-3: 186.1176 100% 50%;
--chart-4: 43 74% 66%; --chart-4: 168 100% 50%;
--chart-5: 27 87% 67%; --chart-5: 54.1176 100% 50%;
--sidebar: 240 100% 97.0588%;
--sidebar-foreground: 240 41.4634% 8.0392%;
--sidebar-primary: 312.9412 100% 50%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 168 100% 50%;
--sidebar-accent-foreground: 240 41.4634% 8.0392%;
--sidebar-border: 198 18.5185% 89.4118%;
--sidebar-ring: 312.9412 100% 50%;
--font-sans: Outfit, sans-serif;
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: Fira Code, monospace;
--radius: 0.5rem; --radius: 0.5rem;
--shadow-2xs: 0px 4px 8px -2px hsl(0 0% 0% / 0.05);
--shadow-xs: 0px 4px 8px -2px hsl(0 0% 0% / 0.05);
--shadow-sm: 0px 4px 8px -2px hsl(0 0% 0% / 0.1),
0px 1px 2px -3px hsl(0 0% 0% / 0.1);
--shadow: 0px 4px 8px -2px hsl(0 0% 0% / 0.1),
0px 1px 2px -3px hsl(0 0% 0% / 0.1);
--shadow-md: 0px 4px 8px -2px hsl(0 0% 0% / 0.1),
0px 2px 4px -3px hsl(0 0% 0% / 0.1);
--shadow-lg: 0px 4px 8px -2px hsl(0 0% 0% / 0.1),
0px 4px 6px -3px hsl(0 0% 0% / 0.1);
--shadow-xl: 0px 4px 8px -2px hsl(0 0% 0% / 0.1),
0px 8px 10px -3px hsl(0 0% 0% / 0.1);
--shadow-2xl: 0px 4px 8px -2px hsl(0 0% 0% / 0.25);
--tracking-normal: 0em;
--spacing: 0.25rem;
} }
.dark { .dark {
--background: 0 0% 3.9%; --background: 240 41.4634% 8.0392%;
--foreground: 0 0% 98%; --foreground: 217.5 26.6667% 94.1176%;
--card: 0 0% 3.9%; --card: 240 35.4839% 18.2353%;
--card-foreground: 0 0% 98%; --card-foreground: 217.5 26.6667% 94.1176%;
--popover: 0 0% 3.9%; --popover: 240 35.4839% 18.2353%;
--popover-foreground: 0 0% 98%; --popover-foreground: 217.5 26.6667% 94.1176%;
--primary: 0 0% 98%; --primary: 312.9412 100% 50%;
--primary-foreground: 0 0% 9%; --primary-foreground: 0 0% 100%;
--secondary: 0 0% 14.9%; --secondary: 240 35.4839% 18.2353%;
--secondary-foreground: 0 0% 98%; --secondary-foreground: 217.5 26.6667% 94.1176%;
--muted: 0 0% 14.9%; --muted: 240 35.4839% 18.2353%;
--muted-foreground: 0 0% 63.9%; --muted-foreground: 232.1053 17.5926% 57.6471%;
--accent: 0 0% 14.9%; --accent: 168 100% 50%;
--accent-foreground: 0 0% 98%; --accent-foreground: 240 41.4634% 8.0392%;
--destructive: 0 62.8% 30.6%; --destructive: 14.3529 100% 50%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 100%;
--border: 0 0% 14.9%; --border: 240 34.2857% 27.451%;
--input: 0 0% 14.9%; --input: 240 34.2857% 27.451%;
--ring: 0 0% 83.1%; --ring: 312.9412 100% 50%;
--chart-1: 220 70% 50%; --chart-1: 312.9412 100% 50%;
--chart-2: 160 60% 45%; --chart-2: 273.8824 100% 50%;
--chart-3: 30 80% 55%; --chart-3: 186.1176 100% 50%;
--chart-4: 280 65% 60%; --chart-4: 168 100% 50%;
--chart-5: 340 75% 55%; --chart-5: 54.1176 100% 50%;
--sidebar: 240 41.4634% 8.0392%;
--sidebar-foreground: 217.5 26.6667% 94.1176%;
--sidebar-primary: 312.9412 100% 50%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 168 100% 50%;
--sidebar-accent-foreground: 240 41.4634% 8.0392%;
--sidebar-border: 240 34.2857% 27.451%;
--sidebar-ring: 312.9412 100% 50%;
--font-sans: Outfit, sans-serif;
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: Fira Code, monospace;
--radius: 0.5rem;
--shadow-2xs: 0px 4px 8px -2px hsl(0 0% 0% / 0.05);
--shadow-xs: 0px 4px 8px -2px hsl(0 0% 0% / 0.05);
--shadow-sm: 0px 4px 8px -2px hsl(0 0% 0% / 0.1),
0px 1px 2px -3px hsl(0 0% 0% / 0.1);
--shadow: 0px 4px 8px -2px hsl(0 0% 0% / 0.1),
0px 1px 2px -3px hsl(0 0% 0% / 0.1);
--shadow-md: 0px 4px 8px -2px hsl(0 0% 0% / 0.1),
0px 2px 4px -3px hsl(0 0% 0% / 0.1);
--shadow-lg: 0px 4px 8px -2px hsl(0 0% 0% / 0.1),
0px 4px 6px -3px hsl(0 0% 0% / 0.1);
--shadow-xl: 0px 4px 8px -2px hsl(0 0% 0% / 0.1),
0px 8px 10px -3px hsl(0 0% 0% / 0.1);
--shadow-2xl: 0px 4px 8px -2px hsl(0 0% 0% / 0.25);
} }
} }

View File

@ -1,5 +1,5 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist } from "next/font/google"; import { K2D } from "next/font/google";
import { ThemeProvider } from "next-themes"; import { ThemeProvider } from "next-themes";
import "./globals.css"; import "./globals.css";
@ -13,10 +13,10 @@ export const metadata: Metadata = {
description: "The fastest way to build apps with Next.js and Supabase", description: "The fastest way to build apps with Next.js and Supabase",
}; };
const geistSans = Geist({ const k2d = K2D({
variable: "--font-geist-sans", variable: "--font-k2d",
display: "swap", subsets: ["thai"],
subsets: ["latin"], weight: ["100", "200", "300", "400", "500", "600", "700", "800"],
}); });
export default function RootLayout({ export default function RootLayout({
@ -26,10 +26,10 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body className={`${geistSans.className} antialiased`}> <body className={`${k2d.className} antialiased`}>
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"
defaultTheme="system" defaultTheme="dark"
enableSystem enableSystem
disableTransitionOnChange disableTransitionOnChange
> >

View File

@ -1,51 +1,3 @@
import { DeployButton } from "@/components/deploy-button";
import { EnvVarWarning } from "@/components/env-var-warning";
import { AuthButton } from "@/components/auth-button";
import { Hero } from "@/components/hero";
import { ThemeSwitcher } from "@/components/theme-switcher";
import { ConnectSupabaseSteps } from "@/components/tutorial/connect-supabase-steps";
import { SignUpUserSteps } from "@/components/tutorial/sign-up-user-steps";
import { hasEnvVars } from "@/lib/utils";
import Link from "next/link";
export default function Home() { export default function Home() {
return ( return <main></main>;
<main className="min-h-screen flex flex-col items-center">
<div className="flex-1 w-full flex flex-col gap-20 items-center">
<nav className="w-full flex justify-center border-b border-b-foreground/10 h-16">
<div className="w-full max-w-5xl flex justify-between items-center p-3 px-5 text-sm">
<div className="flex gap-5 items-center font-semibold">
<Link href={"/"}>Next.js Supabase Starter</Link>
<div className="flex items-center gap-2">
<DeployButton />
</div>
</div>
{!hasEnvVars ? <EnvVarWarning /> : <AuthButton />}
</div>
</nav>
<div className="flex-1 flex flex-col gap-20 max-w-5xl p-5">
<Hero />
<main className="flex-1 flex flex-col gap-6 px-4">
<h2 className="font-medium text-xl mb-4">Next steps</h2>
{hasEnvVars ? <SignUpUserSteps /> : <ConnectSupabaseSteps />}
</main>
</div>
<footer className="w-full flex items-center justify-center border-t mx-auto text-center text-xs gap-8 py-16">
<p>
Powered by{" "}
<a
href="https://supabase.com/?utm_source=create-next-app&utm_medium=template&utm_term=nextjs"
target="_blank"
className="font-bold hover:underline"
rel="noreferrer"
>
Supabase
</a>
</p>
<ThemeSwitcher />
</footer>
</div>
</main>
);
} }

View File

@ -0,0 +1,48 @@
import { Button } from "@/components/ui/button";
import { createClient } from "@/lib/supabase/server";
import { ChevronLeftIcon } from "lucide-react";
import Link from "next/link";
interface PageProps {
params: Promise<{ id: string; memberId: string }>;
}
export default async function Page(props: PageProps) {
const { id, memberId } = await props.params;
const supabase = await createClient();
const { data: polls } = await supabase.from("polls").select().eq("id", id);
const { data: members } = await supabase
.from("poll_members")
.select()
.eq("poll_id", id)
.eq("id", parseInt(memberId));
if (!polls || polls.length === 0) {
return <div className="max-w-6xl mx-auto py-2 w-[95%]">Poll not found</div>;
}
if (!members || members.length === 0) {
return (
<div className="max-w-6xl mx-auto py-2 w-[95%]">Member not found</div>
);
}
const [poll] = polls;
const [member] = members;
return (
<main className="max-w-6xl mx-auto py-4 w-[95%] h-screen flex flex-col gap-4">
<div className="flex justify-between items-center relative">
<Button size="icon" variant="ghost" asChild>
<Link href={`/polls/${id}`}>
<ChevronLeftIcon />
</Link>
</Button>
<h1 className="absolute left-1/2 transform -translate-x-1/2">
{poll.name}
</h1>
<Button>{member.name}</Button>
</div>
</main>
);
}

42
app/polls/[id]/page.tsx Normal file
View File

@ -0,0 +1,42 @@
import { Button } from "@/components/ui/button";
import { createClient } from "@/lib/supabase/server";
import Link from "next/link";
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function Page(props: PageProps) {
const { id } = await props.params;
const supabase = await createClient();
const { data: polls } = await supabase.from("polls").select().eq("id", id);
const { data: members } = await supabase
.from("poll_members")
.select()
.order("id", { ascending: true })
.eq("poll_id", id);
if (!polls || polls.length === 0) {
return <div className="max-w-6xl mx-auto py-2 w-[95%]">Poll not found</div>;
}
const [poll] = polls;
return (
<main className="max-w-6xl mx-auto py-4 w-[95%] h-screen flex flex-col gap-4">
<h1 className="text-center text-xl font-semibold">{poll.name}</h1>
<div className="grid gap-4 grid-cols-2 h-full">
{members?.map((m) => (
<Button
key={m.id}
className="h-full bg-primary/50 border-primary text-xl"
variant="outline"
asChild
>
<Link href={`/polls/${id}/members/${m.id}`}>{m.name}</Link>
</Button>
))}
</div>
</main>
);
}

1078
bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,9 @@
import { createBrowserClient } from "@supabase/ssr"; import { createBrowserClient } from "@supabase/ssr";
import { type Database } from "./database.types";
export function createClient() { export function createClient() {
return createBrowserClient( return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!, process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!
); );
} }

View File

@ -0,0 +1,207 @@
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
export type Database = {
// Allows to automatically instanciate createClient with right options
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
__InternalSupabase: {
PostgrestVersion: "12.2.12 (cd3cf9e)"
}
public: {
Tables: {
poll_members: {
Row: {
created_at: string
id: number
name: string
poll_id: string
user_id: string | null
vote: boolean
}
Insert: {
created_at?: string
id?: number
name: string
poll_id: string
user_id?: string | null
vote?: boolean
}
Update: {
created_at?: string
id?: number
name?: string
poll_id?: string
user_id?: string | null
vote?: boolean
}
Relationships: [
{
foreignKeyName: "poll_members_poll_id_fkey"
columns: ["poll_id"]
isOneToOne: false
referencedRelation: "polls"
referencedColumns: ["id"]
},
]
}
polls: {
Row: {
created_at: string
id: string
name: string
}
Insert: {
created_at?: string
id?: string
name: string
}
Update: {
created_at?: string
id?: string
name?: string
}
Relationships: []
}
}
Views: {
[_ in never]: never
}
Functions: {
[_ in never]: never
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
}
type DatabaseWithoutInternals = Omit<Database, "__InternalSupabase">
type DefaultSchema = DatabaseWithoutInternals[Extract<keyof Database, "public">]
export type Tables<
DefaultSchemaTableNameOrOptions extends
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
Row: infer R
}
? R
: never
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
DefaultSchema["Views"])
? (DefaultSchema["Tables"] &
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
Row: infer R
}
? R
: never
: never
export type TablesInsert<
DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema["Tables"]
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Insert: infer I
}
? I
: never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
Insert: infer I
}
? I
: never
: never
export type TablesUpdate<
DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema["Tables"]
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Update: infer U
}
? U
: never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
Update: infer U
}
? U
: never
: never
export type Enums<
DefaultSchemaEnumNameOrOptions extends
| keyof DefaultSchema["Enums"]
| { schema: keyof DatabaseWithoutInternals },
EnumName extends DefaultSchemaEnumNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
: never = never,
> = DefaultSchemaEnumNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
: never
export type CompositeTypes<
PublicCompositeTypeNameOrOptions extends
| keyof DefaultSchema["CompositeTypes"]
| { schema: keyof DatabaseWithoutInternals },
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
: never = never,
> = PublicCompositeTypeNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
: never
export const Constants = {
public: {
Enums: {},
},
} as const

View File

@ -49,6 +49,7 @@ export async function updateSession(request: NextRequest) {
if ( if (
request.nextUrl.pathname !== "/" && request.nextUrl.pathname !== "/" &&
!request.nextUrl.pathname.startsWith("/polls") &&
!user && !user &&
!request.nextUrl.pathname.startsWith("/login") && !request.nextUrl.pathname.startsWith("/login") &&
!request.nextUrl.pathname.startsWith("/auth") !request.nextUrl.pathname.startsWith("/auth")

View File

@ -1,5 +1,6 @@
import { createServerClient } from "@supabase/ssr"; import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { type Database } from "./database.types";
/** /**
* Especially important if using Fluid compute: Don't put this client in a * Especially important if using Fluid compute: Don't put this client in a
@ -9,7 +10,7 @@ import { cookies } from "next/headers";
export async function createClient() { export async function createClient() {
const cookieStore = await cookies(); const cookieStore = await cookies();
return createServerClient( return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!, process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,
{ {
@ -20,7 +21,7 @@ export async function createClient() {
setAll(cookiesToSet) { setAll(cookiesToSet) {
try { try {
cookiesToSet.forEach(({ name, value, options }) => cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options), cookieStore.set(name, value, options)
); );
} catch { } catch {
// The `setAll` method was called from a Server Component. // The `setAll` method was called from a Server Component.
@ -29,6 +30,6 @@ export async function createClient() {
} }
}, },
}, },
}, }
); );
} }

7550
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,11 @@
{ {
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint",
"api:gen": "supabase gen types typescript --project-id vjbicmosxxcntznlpgqk > lib/supabase/database.types.ts"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-checkbox": "^1.3.1", "@radix-ui/react-checkbox": "^1.3.1",
@ -31,6 +32,7 @@
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.3.1", "eslint-config-next": "15.3.1",
"postcss": "^8", "postcss": "^8",
"supabase": "^2.31.8",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^5" "typescript": "^5"