mirror of
https://github.com/EdiFarcas/Giveaway-app.git
synced 2026-06-22 07:00:57 +03:00
Initial commit - project setup
This commit is contained in:
Generated
+1009
-19
File diff suppressed because it is too large
Load Diff
+13
-6
@@ -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;
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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
@@ -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
@@ -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>1–100 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>101–200 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>201–300 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">$1–5</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">$10–25</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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return <SessionProvider>{children}</SessionProvider>;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
export const db = new PrismaClient();
|
||||
Reference in New Issue
Block a user