Initial commit - project setup

This commit is contained in:
Alexandru Eduard Farcas
2025-04-27 17:13:38 +03:00
parent 5be661f41a
commit d8da8c839f
22 changed files with 1775 additions and 144 deletions
+1009 -19
View File
File diff suppressed because it is too large Load Diff
+13 -6
View File
@@ -9,19 +9,26 @@
"lint": "next lint"
},
"dependencies": {
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^6.6.0",
"autoprefixer": "^10.4.21",
"axios": "^1.9.0",
"next": "15.3.1",
"next-auth": "^4.24.11",
"postcss": "^8.5.3",
"prisma": "^6.6.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.3.1"
"react-dom": "^19.0.0"
},
"devDependencies": {
"typescript": "^5",
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9",
"eslint-config-next": "15.3.1",
"@eslint/eslintrc": "^3"
"tailwindcss": "^4.1.4",
"typescript": "^5"
}
}
@@ -0,0 +1,45 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT,
"image" TEXT,
"youtubeId" TEXT,
"coins" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Giveaway" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"value" INTEGER NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Giveaway_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Entry" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"giveawayId" TEXT NOT NULL,
"weight" DOUBLE PRECISION NOT NULL,
CONSTRAINT "Entry_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "User_youtubeId_key" ON "User"("youtubeId");
-- AddForeignKey
ALTER TABLE "Entry" ADD CONSTRAINT "Entry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Entry" ADD CONSTRAINT "Entry_giveawayId_fkey" FOREIGN KEY ("giveawayId") REFERENCES "Giveaway"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,56 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "emailVerified" TIMESTAMP(3),
ALTER COLUMN "email" DROP NOT NULL;
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,10 @@
/*
Warnings:
- You are about to drop the column `value` on the `Giveaway` table. All the data in the column will be lost.
- Added the required column `prize` to the `Giveaway` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Giveaway" DROP COLUMN "value",
ADD COLUMN "prize" TEXT NOT NULL;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Giveaway" ADD COLUMN "value" INTEGER NOT NULL DEFAULT 0;
+3
View File
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
+77
View File
@@ -0,0 +1,77 @@
datasource db {
provider = "postgresql" // Or your database provider
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
youtubeId String? @unique
coins Int @default(0)
entries Entry[]
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? // Optional
access_token String? // Optional
expires_at Int? // Optional
token_type String? // Optional
scope String? // Optional
id_token String? // Optional
session_state String? // Optional
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
model Giveaway {
id String @id @default(cuid())
title String
description String
value Int @default(0)
prize String
active Boolean @default(true)
createdAt DateTime @default(now())
entries Entry[]
}
model Entry {
id String @id @default(cuid())
user User @relation(fields: [userId], references: [id])
userId String
giveaway Giveaway @relation(fields: [giveawayId], references: [id])
giveawayId String
weight Float
}
+114
View File
@@ -0,0 +1,114 @@
"use client";
import { useState } from "react";
interface AdminClientProps {
email: string;
}
export default function AdminClient({ email }: AdminClientProps) {
const [giveaway, setGiveaway] = useState({ title: "", description: "", value: 0, prize: "" });
const [userCoins, setUserCoins] = useState({ userId: "", coins: 0 });
const handleCreateGiveaway = async () => {
const response = await fetch("/api/admin/create-giveaway", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(giveaway),
});
if (response.ok) {
alert("Giveaway created successfully!");
setGiveaway({ title: "", description: "", value:0, prize: "" }); // Reset form
} else {
const error = await response.json();
alert(`Failed to create giveaway: ${error.error}`);
}
};
const handleUpdateCoins = async () => {
const response = await fetch("/api/admin/update-coins", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(userCoins),
});
if (response.ok) {
alert("User coins updated successfully!");
setUserCoins({ userId: "", coins: 0 }); // Reset form
} else {
const error = await response.json();
alert(`Failed to update user coins: ${error.error}`);
}
};
return (
<div className="min-h-screen bg-white text-black py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto space-y-8">
<h1 className="text-3xl font-bold">Admin Page</h1>
<p>Welcome, {email}!</p>
{/* Create Giveaway Form */}
<div className="p-4 text-white bg-gray-800 shadow rounded">
<h2 className="text-xl font-semibold">Create Giveaway</h2>
<input
type="text"
placeholder="Title"
value={giveaway.title}
onChange={(e) => setGiveaway({ ...giveaway, title: e.target.value })}
className="block w-full mt-2 p-2 border rounded bg-gray-700 text-gray-200"
/>
<textarea
placeholder="Description"
value={giveaway.description}
onChange={(e) => setGiveaway({ ...giveaway, description: e.target.value })}
className="block w-full mt-2 p-2 border rounded bg-gray-700 text-gray-200"
/>
<input
type="text"
placeholder="Prize"
value={giveaway.prize}
onChange={(e) => setGiveaway({ ...giveaway, prize: e.target.value })}
className="block w-full mt-2 p-2 border rounded bg-gray-700 text-gray-200"
/>
<input
type="number"
placeholder="Value"
value={giveaway.value}
onChange={(e) => setGiveaway({ ...giveaway, value: Number(e.target.value) })}
className="block w-full mt-2 p-2 border rounded bg-gray-700 text-gray-200"
/>
<button
onClick={handleCreateGiveaway}
className="mt-4 bg-blue-600 text-white px-4 py-2 rounded"
>
Create Giveaway
</button>
</div>
{/* Update User Coins Form */}
<div className="p-4 text-white bg-gray-800 shadow rounded">
<h2 className="text-xl font-semibold">Update User Coins</h2>
<input
type="text"
placeholder="User ID"
value={userCoins.userId}
onChange={(e) => setUserCoins({ ...userCoins, userId: e.target.value })}
className="block w-full mt-2 p-2 border rounded bg-gray-700 text-gray-200"
/>
<input
type="number"
placeholder="Coins"
value={userCoins.coins}
onChange={(e) => setUserCoins({ ...userCoins, coins: Number(e.target.value) })}
className="block w-full mt-2 p-2 border rounded bg-gray-700 text-gray-200"
/>
<button
onClick={handleUpdateCoins}
className="mt-4 bg-green-600 text-white px-4 py-2 rounded"
>
Update Coins
</button>
</div>
</div>
</div>
);
}
+19
View File
@@ -0,0 +1,19 @@
import { getServerSession } from "next-auth";
import { authOptions } from "../api/auth/[...nextauth]/route";
import { redirect } from "next/navigation";
import AdminClient from "./AdminClient"; // Adjust the import path as necessary
const allowedEmails = ["farcas.edi@gmail.com"]; // Add allowed Gmail accounts here
export default async function AdminPage() {
const session = await getServerSession(authOptions);
if (!session || !allowedEmails.includes(session.user?.email || "")) {
redirect("/");
return null;
}
return (
<AdminClient email={session.user?.email || ""} />
);
}
@@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
export async function POST(req: Request) {
const body = await req.json();
const { title, description, value, prize } = body;
if (!title || !description || !prize || !value) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
try {
const giveaway = await db.giveaway.create({
data: {
title,
description,
prize,
value,
},
});
return NextResponse.json(giveaway);
} catch (error) {
return NextResponse.json({ error: "Failed to create giveaway" }, { status: 500 });
}
}
+21
View File
@@ -0,0 +1,21 @@
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
export async function POST(req: Request) {
const body = await req.json();
const { userId, coins } = body;
if (!userId || coins === undefined) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
try {
const user = await db.user.update({
where: { id: userId },
data: { coins },
});
return NextResponse.json(user);
} catch (error) {
return NextResponse.json({ error: "Failed to update user coins" }, { status: 500 });
}
}
+40
View File
@@ -0,0 +1,40 @@
import NextAuth, { SessionStrategy } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { db } from "@/lib/db"; // Ensure this is the correct path to your Prisma client
if (!db) {
throw new Error("Prisma client is not initialized. Check your database configuration.");
}
export const authOptions = {
adapter: PrismaAdapter(db),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
secret: process.env.NEXTAUTH_SECRET,
session: {
strategy: "database" as SessionStrategy,
},
callbacks: {
async signIn({ user, account, profile }: { user: any; account: any; profile?: any }) {
if (!user || !account || !profile) {
console.error("Sign-in failed: Missing user, account, or profile data.");
return false;
}
return true;
},
async session({ session, user }: { session: any; user: { id: string } }) {
session.user.id = user.id; // Attach user ID to the session
return session;
},
},
debug: true, // Enable debug mode for detailed error messages
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

+25
View File
@@ -0,0 +1,25 @@
import { getServerSession } from "next-auth";
import { authOptions } from "../api/auth/[...nextauth]/route";
import { db } from "@/lib/db";
import { redirect } from "next/navigation";
export default async function ProfilePage() {
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto">
<div className="bg-black rounded-2xl shadow-xl overflow-hidden">
{/* Profile Header */}
<div className="bg-gradient-to-r from-purple-600 to-blue-500 p-6">
<div className="flex items-center space-x-4">
<div className="h-16 w-16 bg-white rounded-full flex items-center justify-center">
</div>
<div>
<h1 className="text-2xl font-bold text-white">{"Giveaway page"}</h1>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
+37 -22
View File
@@ -1,33 +1,48 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import AuthButtons from "@/components/AuthButtons";
import { Providers } from "@/components/Providers";
import Link from "next/link";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
export const metadata = {
title: "Giveaway System",
description: "Built with Next.js 15 and Tailwind",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<body className="p-4">
<Providers>
<header className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Giveaway System</h1>
<div className="flex items-center gap-4">
<Link
href="/"
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
>
Home
</Link>
<Link
href="/giveaways"
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
>
Giveaways
</Link>
<Link
href="/profile"
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
>
Profile
</Link>
<AuthButtons />
</div>
</header>
<main>{children}</main>
</Providers>
</body>
</html>
);
+115 -97
View File
@@ -1,103 +1,121 @@
import Image from "next/image";
export default function Home() {
export default function GiveawaySystem() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8 font-[family-name:var(--font-geist-sans)]">
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="text-center mb-16">
<h1 className="text-4xl sm:text-5xl font-bold mb-4 bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent">
🎁 Community Giveaway System
</h1>
<p className="text-lg text-gray-600">Revamped engagement-powered rewards system</p>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
{/* Coin System Section */}
<div className="bg-white rounded-xl shadow-lg p-6 mb-8">
<div className="flex items-center mb-4">
<span className="text-3xl mr-3">🪙</span>
<h2 className="text-2xl font-bold">Coin System Overview</h2>
</div>
<ul className="list-disc pl-6 space-y-3 text-gray-700">
<li>Earn coins by commenting on videos and other engagement</li>
<li>Coins serve dual purpose:
<ul className="list-circle pl-4 mt-2 space-y-2">
<li>Eligibility requirement for giveaways</li>
<li>Weighted entries (1 coin = 1 ticket, with diminishing returns)</li>
</ul>
</li>
</ul>
</div>
{/* Weighted Lottery Section */}
<div className="bg-white rounded-xl shadow-lg p-6 mb-8">
<div className="flex items-center mb-4">
<span className="text-3xl mr-3">🧮</span>
<h2 className="text-2xl font-bold">Weighted Lottery System</h2>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-4">
<div className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
<span>1100 coins</span>
<span className="font-bold text-purple-600">1x multiplier</span>
</div>
<div className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
<span>101200 coins</span>
<span className="font-bold text-blue-600">0.5x multiplier</span>
</div>
<div className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
<span>201300 coins</span>
<span className="font-bold text-green-600">0.25x multiplier</span>
</div>
<div className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
<span>301+ coins</span>
<span className="font-bold text-red-600">0.1x multiplier</span>
</div>
</div>
<div className="bg-purple-50 p-4 rounded-lg">
<h3 className="font-semibold mb-2">Example Calculation</h3>
<p className="text-sm text-gray-600">
User with 450 coins:<br />
100 × 1 + 100 × 0.5 + 100 × 0.25 + 50 × 0.1 =<br />
<span className="font-bold">100 + 50 + 25 + 5 = 180 tickets</span>
</p>
</div>
</div>
</div>
{/* Entry Requirements */}
<div className="bg-white rounded-xl shadow-lg p-6">
<div className="flex items-center mb-4">
<span className="text-3xl mr-3">🔐</span>
<h2 className="text-2xl font-bold">Entry Requirements</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="text-left py-3 px-4">Giveaway Value</th>
<th className="text-left py-3 px-4">Minimum Coins</th>
<th className="text-left py-3 px-4">Coins Burned</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="py-3 px-4">$15</td>
<td className="py-3 px-4 font-medium">500</td>
<td className="py-3 px-4 text-gray-500">None</td>
</tr>
<tr className="border-b">
<td className="py-3 px-4">$1025</td>
<td className="py-3 px-4 font-medium">1,000</td>
<td className="py-3 px-4 text-gray-500">None</td>
</tr>
<tr>
<td className="py-3 px-4">$50+</td>
<td className="py-3 px-4 font-medium">2,000+</td>
<td className="py-3 px-4 text-gray-500">Optional (e.g., 50 coins)</td>
</tr>
</tbody>
</table>
</div>
<div className="mt-6 p-4 bg-blue-50 rounded-lg">
<p className="text-sm text-blue-800">
💡 Note: Coins are not spent to enter - minimum balance acts as eligibility requirement.
Optional burns for higher tiers provide bonus entry weight.
</p>
</div>
</div>
{/* Disclaimer */}
<div className="mt-8 text-center text-sm text-gray-500">
<p>System subject to change. See full rules for complete details.</p>
</div>
</div>
</div>
);
}
}
+105
View File
@@ -0,0 +1,105 @@
import { getServerSession } from "next-auth";
import { authOptions } from "../api/auth/[...nextauth]/route";
import { db } from "@/lib/db";
import { redirect } from "next/navigation";
export default async function ProfilePage() {
const session = await getServerSession(authOptions);
if (!session) {
redirect("/api/auth/signin");
}
const user = await db.user.findUnique({
where: { email: session.user?.email! },
});
if (!user) {
redirect("/api/auth/signin");
}
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto">
<div className="bg-black rounded-2xl shadow-xl overflow-hidden">
{/* Profile Header */}
<div className="bg-gradient-to-r from-purple-600 to-blue-500 p-6">
<div className="flex items-center space-x-4">
<div className="h-16 w-16 bg-white rounded-full flex items-center justify-center">
<span className="text-2xl font-bold bg-gradient-to-r from-purple-600 to-blue-500 bg-clip-text text-transparent">
{user.name?.[0]?.toUpperCase() || 'U'}
</span>
</div>
<div>
<h1 className="text-2xl font-bold text-white">{user.name}</h1>
<p className="text-purple-100">🌟 Premium Member</p>
</div>
</div>
</div>
{/* Profile Content */}
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Personal Info */}
<div className="space-y-4">
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<span className="text-lg">👤</span>
<div>
<p className="text-sm text-black">Full Name</p>
<p className="font-medium text-gray-500">{user.name}</p>
</div>
</div>
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<span className="text-lg">📧</span>
<div>
<p className="text-sm text-black">Email Address</p>
<p className="font-medium text-gray-500">{user.email}</p>
</div>
</div>
</div>
{/* Coins Section */}
<div className="bg-purple-50 rounded-xl p-5">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-purple-600 mb-1">Available Balance</p>
<div className="flex items-baseline space-x-2">
<span className="text-lg">💰</span>
<span className="text-2xl font-bold text-black">{user.coins}</span>
</div>
</div>
<button className="bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors">
Add Coins
</button> {/* Add functionality to add coins here */}
</div>
</div>
</div>
{/* Achievement Section */}
<div className="mt-8 border-t border-gray-100 pt-6">
<h3 className="text-lg font-semibold mb-4 flex items-center">
<span className="text-xl mr-2"></span>
Achievements
</h3>
<div className="grid grid-cols-3 gap-4">
<div className="text-center p-4 bg-gray-50 rounded-lg">
<div className="text-2xl font-bold text-purple-600">5</div>
<div className="text-sm text-gray-500">Completed Tasks</div>
</div>
<div className="text-center p-4 bg-gray-50 rounded-lg">
<div className="text-2xl font-bold text-purple-600">12</div>
<div className="text-sm text-gray-500">Active Days</div>
</div>
<div className="text-center p-4 bg-gray-50 rounded-lg">
<div className="text-2xl font-bold text-purple-600">3</div>
<div className="text-sm text-gray-500">Badges Earned</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
+30
View File
@@ -0,0 +1,30 @@
"use client";
import { signIn, signOut, useSession } from "next-auth/react";
export default function AuthButtons() {
const { data: session } = useSession();
if (session) {
return (
<div className="flex items-center gap-4">
{/* <p>Hi, {session.user?.name}!</p> */}
<button
onClick={() => signOut()}
className="bg-red-500 text-white px-4 py-2 rounded"
>
Sign Out
</button>
</div>
);
}
return (
<button
onClick={() => signIn("google")}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Sign In with Google
</button>
);
}
+19
View File
@@ -0,0 +1,19 @@
import React from 'react';
interface GiveawayCardProps {
title: string;
description: string;
imageUrl: string;
}
const GiveawayCard: React.FC<GiveawayCardProps> = ({ title, description, imageUrl }) => {
return (
<div className="card">
<img src={imageUrl} alt={title} className="card-image" />
<h3>{title}</h3>
<p>{description}</p>
</div>
);
};
export default GiveawayCard;
+7
View File
@@ -0,0 +1,7 @@
"use client";
import { SessionProvider } from "next-auth/react";
export function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}
+3
View File
@@ -0,0 +1,3 @@
import { PrismaClient } from "@prisma/client";
export const db = new PrismaClient();