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"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0"
|
||||||
"next": "15.3.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@tailwindcss/postcss": "^4",
|
|
||||||
"tailwindcss": "^4",
|
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.3.1",
|
"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 type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import AuthButtons from "@/components/AuthButtons";
|
||||||
|
import { Providers } from "@/components/Providers";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
const geistSans = Geist({
|
export const metadata = {
|
||||||
variable: "--font-geist-sans",
|
title: "Giveaway System",
|
||||||
subsets: ["latin"],
|
description: "Built with Next.js 15 and Tailwind",
|
||||||
});
|
|
||||||
|
|
||||||
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 default function RootLayout({
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
}>) {
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body
|
<body className="p-4">
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
<Providers>
|
||||||
>
|
<header className="flex justify-between items-center mb-6">
|
||||||
{children}
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
+115
-97
@@ -1,103 +1,121 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
export default function Home() {
|
export default function GiveawaySystem() {
|
||||||
return (
|
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)]">
|
<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)]">
|
||||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
<div className="max-w-4xl mx-auto">
|
||||||
<Image
|
{/* Header */}
|
||||||
className="dark:invert"
|
<div className="text-center mb-16">
|
||||||
src="/next.svg"
|
<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">
|
||||||
alt="Next.js logo"
|
🎁 Community Giveaway System
|
||||||
width={180}
|
</h1>
|
||||||
height={38}
|
<p className="text-lg text-gray-600">Revamped engagement-powered rewards system</p>
|
||||||
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>
|
</div>
|
||||||
</main>
|
|
||||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
{/* Coin System Section */}
|
||||||
<a
|
<div className="bg-white rounded-xl shadow-lg p-6 mb-8">
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
<div className="flex items-center mb-4">
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<span className="text-3xl mr-3">🪙</span>
|
||||||
target="_blank"
|
<h2 className="text-2xl font-bold">Coin System Overview</h2>
|
||||||
rel="noopener noreferrer"
|
</div>
|
||||||
>
|
<ul className="list-disc pl-6 space-y-3 text-gray-700">
|
||||||
<Image
|
<li>Earn coins by commenting on videos and other engagement</li>
|
||||||
aria-hidden
|
<li>Coins serve dual purpose:
|
||||||
src="/file.svg"
|
<ul className="list-circle pl-4 mt-2 space-y-2">
|
||||||
alt="File icon"
|
<li>Eligibility requirement for giveaways</li>
|
||||||
width={16}
|
<li>Weighted entries (1 coin = 1 ticket, with diminishing returns)</li>
|
||||||
height={16}
|
</ul>
|
||||||
/>
|
</li>
|
||||||
Learn
|
</ul>
|
||||||
</a>
|
</div>
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
{/* Weighted Lottery Section */}
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<div className="bg-white rounded-xl shadow-lg p-6 mb-8">
|
||||||
target="_blank"
|
<div className="flex items-center mb-4">
|
||||||
rel="noopener noreferrer"
|
<span className="text-3xl mr-3">🧮</span>
|
||||||
>
|
<h2 className="text-2xl font-bold">Weighted Lottery System</h2>
|
||||||
<Image
|
</div>
|
||||||
aria-hidden
|
|
||||||
src="/window.svg"
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
alt="Window icon"
|
<div className="space-y-4">
|
||||||
width={16}
|
<div className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
|
||||||
height={16}
|
<span>1–100 coins</span>
|
||||||
/>
|
<span className="font-bold text-purple-600">1x multiplier</span>
|
||||||
Examples
|
</div>
|
||||||
</a>
|
<div className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
|
||||||
<a
|
<span>101–200 coins</span>
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
<span className="font-bold text-blue-600">0.5x multiplier</span>
|
||||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
</div>
|
||||||
target="_blank"
|
<div className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
|
||||||
rel="noopener noreferrer"
|
<span>201–300 coins</span>
|
||||||
>
|
<span className="font-bold text-green-600">0.25x multiplier</span>
|
||||||
<Image
|
</div>
|
||||||
aria-hidden
|
<div className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
|
||||||
src="/globe.svg"
|
<span>301+ coins</span>
|
||||||
alt="Globe icon"
|
<span className="font-bold text-red-600">0.1x multiplier</span>
|
||||||
width={16}
|
</div>
|
||||||
height={16}
|
</div>
|
||||||
/>
|
|
||||||
Go to nextjs.org →
|
<div className="bg-purple-50 p-4 rounded-lg">
|
||||||
</a>
|
<h3 className="font-semibold mb-2">Example Calculation</h3>
|
||||||
</footer>
|
<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>
|
</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