From 9b82afb4a98ad2abdc586b40a261c9aad7183bab Mon Sep 17 00:00:00 2001 From: Logan Ford <110533855+Logannford@users.noreply.github.com> Date: Sun, 9 Mar 2025 10:15:17 +0000 Subject: [PATCH 01/18] feat: init league schema commit --- prisma/schema/leagues.prisma | 59 ++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 prisma/schema/leagues.prisma diff --git a/prisma/schema/leagues.prisma b/prisma/schema/leagues.prisma new file mode 100644 index 000000000..3a3255838 --- /dev/null +++ b/prisma/schema/leagues.prisma @@ -0,0 +1,59 @@ +enum LeagueName { + BRONZE + SILVER + GOLD + PLATINUM + DIAMOND +} + +enum LeagueColor { + CD7F32 + C0C0C0 + FFD700 + E5E4E2 + b9f2ff +} + +// schema for a league, no relation to a users as +// we have multiple instances of the same league +model IndividualLeagueData { + // standard prisma fields + uid String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // the name of the league + name LeagueName + + // the league's color + color LeagueColor + + // a brief description of the league (may or may not be used) + description String? + + // the amount of xp required to be in the league + xpRequirement Int + + // when the league resets + resetDate DateTime + + // leagues such as the bronze league cannot be relegated + // as it's the lowest league + canBeRelegated Boolean @default(false) + + // the league's icon + icon String? + + Leagues Leagues[] +} + +model Leagues { + // standard prisma fields + uid String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connect to the league data + leagueData IndividualLeagueData @relation(fields: [leagueDataUid], references: [uid]) + leagueDataUid String +} \ No newline at end of file From 8c7bbf944e0d0ff7c4b9d8800528a22bb7fd020c Mon Sep 17 00:00:00 2001 From: Logan Ford <110533855+Logannford@users.noreply.github.com> Date: Sun, 9 Mar 2025 10:25:53 +0000 Subject: [PATCH 02/18] feat: adding more fields to league data --- prisma/schema/leagues.prisma | 190 ++++++++++++++++++++++++++++++++++- 1 file changed, 189 insertions(+), 1 deletion(-) diff --git a/prisma/schema/leagues.prisma b/prisma/schema/leagues.prisma index 3a3255838..794d58ab9 100644 --- a/prisma/schema/leagues.prisma +++ b/prisma/schema/leagues.prisma @@ -12,6 +12,26 @@ enum LeagueColor { FFD700 E5E4E2 b9f2ff + FF4D4D +} + +enum LeagueAchievementType { + LEAGUE_WINNER + TOP_THREE + PROMOTION + PERFECT_WEEK + SURVIVAL + COMEBACK_KING + CONSISTENCY + SPEED_DEMON +} + +enum LeaguePowerUp { + DOUBLE_XP + SHIELD + STREAK_SAVER + TIME_FREEZE + BONUS_POINTS } // schema for a league, no relation to a users as @@ -44,7 +64,26 @@ model IndividualLeagueData { // the league's icon icon String? - Leagues Leagues[] + // Minimum XP required to avoid relegation + minXpToAvoidRelegation Int @default(100) + + // XP threshold for promotion + xpThresholdForPromotion Int? + + // League rules + inactivityThresholdDays Int @default(7) + minWeeksBeforePromotion Int @default(1) + + // Maximum number of power-ups allowed per week + maxPowerUpsPerWeek Int @default(3) + + // Bonus XP multiplier for this league + xpMultiplier Float @default(1.0) + + // Relations + leagues Leagues[] + achievements LeagueAchievement[] + history LeagueHistory[] } model Leagues { @@ -56,4 +95,153 @@ model Leagues { // connect to the league data leagueData IndividualLeagueData @relation(fields: [leagueDataUid], references: [uid]) leagueDataUid String + + // Maximum number of users in a league group + maxUsers Int @default(50) + + // Current number of users + currentUsers Int @default(0) + + // Whether the league is accepting new users + isOpen Boolean @default(true) + + // When this league group started + startDate DateTime @default(now()) + + // When this league group ends + endDate DateTime + + // Top users who will be promoted + promotionCount Int @default(3) + + // Bottom users who will be relegated + relegationCount Int @default(5) + + // Weekly challenges for bonus XP + weeklyChallenge String? + weeklyChallengeXP Int? + + // Relations + users UserLeague[] +} + +model UserLeague { + uid String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // The user in this league + user Users @relation(fields: [userUid], references: [uid]) + userUid String + + // The league instance they're in + league Leagues @relation(fields: [leagueUid], references: [uid]) + leagueUid String + + // Their position in the league + position Int? + + // Their weekly XP in this league + weeklyXp Int @default(0) + + // Whether they've been promoted/relegated + promoted Boolean @default(false) + relegated Boolean @default(false) + + // When they joined this league + joinedAt DateTime @default(now()) + + // Gamification features + currentStreak Int @default(0) + bestPosition Int? + powerUpsRemaining Int @default(3) + activePowerUps LeaguePowerUp[] + powerUpExpiryTime DateTime? + + // Weekly challenge progress + challengeProgress Int @default(0) + challengeCompleted Boolean @default(false) + + // Unique constraint + @@unique([userUid, leagueUid]) +} + +model LeagueAchievement { + uid String @id @default(uuid()) + createdAt DateTime @default(now()) + + // The user who earned it + user Users @relation(fields: [userUid], references: [uid]) + userUid String + + // The league it was earned in + league IndividualLeagueData @relation(fields: [leagueDataUid], references: [uid]) + leagueDataUid String + + // Type of achievement + type LeagueAchievementType + + // When it was earned + earnedAt DateTime @default(now()) + + // Additional data (e.g., position, score) + metadata Json? + + // XP bonus awarded + xpBonus Int @default(0) +} + +model LeagueHistory { + uid String @id @default(uuid()) + createdAt DateTime @default(now()) + + // The user + user Users @relation(fields: [userUid], references: [uid]) + userUid String + + // The league + league IndividualLeagueData @relation(fields: [leagueDataUid], references: [uid]) + leagueDataUid String + + // Their final position + finalPosition Int + + // Their final XP + finalXp Int + + // Whether they were promoted/relegated + wasPromoted Boolean + wasRelegated Boolean + + // The week this history is for + weekStartDate DateTime + weekEndDate DateTime + + // Performance stats + averageXpPerDay Float? + powerUpsUsed Int @default(0) + challengesCompleted Int @default(0) +} + +model Users { + // ... your existing fields ... + + // Current league membership + currentLeague UserLeague? + + // All league memberships + leagueMemberships UserLeague[] + + // League achievements + leagueAchievements LeagueAchievement[] + + // League history + leagueHistory LeagueHistory[] + + // Last league activity + lastLeagueActivity DateTime? + + // League related fields + totalLeagueWins Int @default(0) + highestLeagueReached LeagueName @default(BRONZE) } \ No newline at end of file From 677480c913487601979d2e55c91a9a60cbe107b6 Mon Sep 17 00:00:00 2001 From: Logan Ford <110533855+Logannford@users.noreply.github.com> Date: Sun, 9 Mar 2025 10:26:25 +0000 Subject: [PATCH 03/18] hotfix: user league relations --- prisma/schema/leagues.prisma | 23 ----------------------- prisma/schema/users.prisma | 6 ++++++ 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/prisma/schema/leagues.prisma b/prisma/schema/leagues.prisma index 794d58ab9..ef1b9694e 100644 --- a/prisma/schema/leagues.prisma +++ b/prisma/schema/leagues.prisma @@ -221,27 +221,4 @@ model LeagueHistory { averageXpPerDay Float? powerUpsUsed Int @default(0) challengesCompleted Int @default(0) -} - -model Users { - // ... your existing fields ... - - // Current league membership - currentLeague UserLeague? - - // All league memberships - leagueMemberships UserLeague[] - - // League achievements - leagueAchievements LeagueAchievement[] - - // League history - leagueHistory LeagueHistory[] - - // Last league activity - lastLeagueActivity DateTime? - - // League related fields - totalLeagueWins Int @default(0) - highestLeagueReached LeagueName @default(BRONZE) } \ No newline at end of file diff --git a/prisma/schema/users.prisma b/prisma/schema/users.prisma index 4e6771d38..72becd6de 100644 --- a/prisma/schema/users.prisma +++ b/prisma/schema/users.prisma @@ -73,6 +73,12 @@ model Users { roadmaps UserRoadmaps[] studyPathEnrollments UserStudyPath[] userMissions UserMission[] + + LeagueHistory LeagueHistory[] + + LeagueAchievement LeagueAchievement[] + + UserLeague UserLeague[] } model Streaks { From b6441ddce0a14b8011c529ec41bedec8b8cd7b4e Mon Sep 17 00:00:00 2001 From: Logan Ford <110533855+Logannford@users.noreply.github.com> Date: Sun, 9 Mar 2025 15:28:37 +0000 Subject: [PATCH 04/18] feat: creates league schema and applying to db --- .../migration.sql | 127 ++++++++++++++++++ prisma/schema/leagues.prisma | 76 +++++------ 2 files changed, 162 insertions(+), 41 deletions(-) create mode 100644 prisma/migrations/20250309152758_init_league_schema_commit/migration.sql diff --git a/prisma/migrations/20250309152758_init_league_schema_commit/migration.sql b/prisma/migrations/20250309152758_init_league_schema_commit/migration.sql new file mode 100644 index 000000000..26c8e8f79 --- /dev/null +++ b/prisma/migrations/20250309152758_init_league_schema_commit/migration.sql @@ -0,0 +1,127 @@ +-- CreateEnum +CREATE TYPE "LeagueName" AS ENUM ('BRONZE', 'SILVER', 'GOLD', 'PLATINUM', 'DIAMOND'); + +-- CreateEnum +CREATE TYPE "LeagueColor" AS ENUM ('CD7F32', 'C0C0C0', 'FFD700', 'E5E4E2', 'b9f2ff', 'FF4D4D'); + +-- CreateEnum +CREATE TYPE "LeagueAchievementType" AS ENUM ('LEAGUE_WINNER', 'TOP_THREE', 'PROMOTION', 'PERFECT_WEEK', 'SURVIVAL', 'COMEBACK_KING', 'CONSISTENCY', 'SPEED_DEMON'); + +-- CreateEnum +CREATE TYPE "LeaguePowerUp" AS ENUM ('DOUBLE_XP', 'SHIELD', 'STREAK_SAVER', 'TIME_FREEZE', 'BONUS_POINTS'); + +-- CreateTable +CREATE TABLE "IndividualLeagueData" ( + "uid" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "name" "LeagueName" NOT NULL, + "color" "LeagueColor" NOT NULL, + "description" TEXT, + "xpRequirement" INTEGER NOT NULL, + "resetDate" TIMESTAMP(3) NOT NULL, + "canBeRelegated" BOOLEAN NOT NULL DEFAULT false, + "icon" TEXT, + "inactivityThresholdDays" INTEGER DEFAULT 7, + "maxPowerUpsPerWeek" INTEGER NOT NULL DEFAULT 3, + "xpMultiplier" DOUBLE PRECISION NOT NULL DEFAULT 1.0, + + CONSTRAINT "IndividualLeagueData_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "Leagues" ( + "uid" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "leagueDataUid" TEXT NOT NULL, + "maxUsers" INTEGER NOT NULL DEFAULT 30, + "currentUsers" INTEGER NOT NULL DEFAULT 0, + "startDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "endDate" TIMESTAMP(3) NOT NULL, + "promotionCount" INTEGER NOT NULL DEFAULT 3, + "relegationCount" INTEGER NOT NULL DEFAULT 5, + "weeklyChallenge" TEXT, + "weeklyChallengeXP" INTEGER, + + CONSTRAINT "Leagues_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "UserLeague" ( + "uid" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "userUid" TEXT NOT NULL, + "leagueUid" TEXT NOT NULL, + "position" INTEGER, + "weeklyXp" INTEGER NOT NULL DEFAULT 0, + "promoted" BOOLEAN NOT NULL DEFAULT false, + "relegated" BOOLEAN NOT NULL DEFAULT false, + "joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "currentStreak" INTEGER NOT NULL DEFAULT 0, + "bestPosition" INTEGER, + "activePowerUps" "LeaguePowerUp"[], + "powerUpExpiryTime" TIMESTAMP(3), + "challengeProgress" INTEGER NOT NULL DEFAULT 0, + "challengeCompleted" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "UserLeague_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "LeagueAchievement" ( + "uid" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userUid" TEXT NOT NULL, + "leagueDataUid" TEXT NOT NULL, + "type" "LeagueAchievementType" NOT NULL, + "earnedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "metadata" JSONB, + "xpBonus" INTEGER NOT NULL DEFAULT 0, + + CONSTRAINT "LeagueAchievement_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "LeagueHistory" ( + "uid" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userUid" TEXT NOT NULL, + "leagueDataUid" TEXT NOT NULL, + "finalPosition" INTEGER NOT NULL, + "finalXp" INTEGER NOT NULL, + "wasPromoted" BOOLEAN NOT NULL, + "wasRelegated" BOOLEAN NOT NULL, + "weekStartDate" TIMESTAMP(3) NOT NULL, + "weekEndDate" TIMESTAMP(3) NOT NULL, + "averageXpPerDay" DOUBLE PRECISION, + "powerUpsUsed" INTEGER NOT NULL DEFAULT 0, + "challengesCompleted" INTEGER NOT NULL DEFAULT 0, + + CONSTRAINT "LeagueHistory_pkey" PRIMARY KEY ("uid") +); + +-- CreateIndex +CREATE UNIQUE INDEX "UserLeague_userUid_leagueUid_key" ON "UserLeague"("userUid", "leagueUid"); + +-- AddForeignKey +ALTER TABLE "Leagues" ADD CONSTRAINT "Leagues_leagueDataUid_fkey" FOREIGN KEY ("leagueDataUid") REFERENCES "IndividualLeagueData"("uid") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserLeague" ADD CONSTRAINT "UserLeague_userUid_fkey" FOREIGN KEY ("userUid") REFERENCES "Users"("uid") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserLeague" ADD CONSTRAINT "UserLeague_leagueUid_fkey" FOREIGN KEY ("leagueUid") REFERENCES "Leagues"("uid") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LeagueAchievement" ADD CONSTRAINT "LeagueAchievement_userUid_fkey" FOREIGN KEY ("userUid") REFERENCES "Users"("uid") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LeagueAchievement" ADD CONSTRAINT "LeagueAchievement_leagueDataUid_fkey" FOREIGN KEY ("leagueDataUid") REFERENCES "IndividualLeagueData"("uid") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LeagueHistory" ADD CONSTRAINT "LeagueHistory_userUid_fkey" FOREIGN KEY ("userUid") REFERENCES "Users"("uid") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LeagueHistory" ADD CONSTRAINT "LeagueHistory_leagueDataUid_fkey" FOREIGN KEY ("leagueDataUid") REFERENCES "IndividualLeagueData"("uid") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema/leagues.prisma b/prisma/schema/leagues.prisma index ef1b9694e..b929e098e 100644 --- a/prisma/schema/leagues.prisma +++ b/prisma/schema/leagues.prisma @@ -16,22 +16,22 @@ enum LeagueColor { } enum LeagueAchievementType { - LEAGUE_WINNER - TOP_THREE - PROMOTION - PERFECT_WEEK - SURVIVAL - COMEBACK_KING - CONSISTENCY - SPEED_DEMON + LEAGUE_WINNER // did the user win their league last week + TOP_THREE // did the user finish in the top 3 of their league last week + PROMOTION // did the user get promoted last week + PERFECT_WEEK // did the user get 100% of the bonus xp last week + SURVIVAL // did the user avoid relegation last week + COMEBACK_KING // did the user come back from the bottom of the league last week + CONSISTENCY // did the user finish in the top 3 of their league for 3 weeks in a row + SPEED_DEMON // did the user get to the top of the league in the shortest time possible } enum LeaguePowerUp { - DOUBLE_XP - SHIELD - STREAK_SAVER - TIME_FREEZE - BONUS_POINTS + DOUBLE_XP // double the users xp for the week + SHIELD // prevent the user= from being relegated for the week + STREAK_SAVER // prevent the user from being relegated for the next 3 weeks + TIME_FREEZE // freeze the users league for the week + BONUS_POINTS // give the user bonus xp for the week } // schema for a league, no relation to a users as @@ -45,7 +45,7 @@ model IndividualLeagueData { // the name of the league name LeagueName - // the league's color + // the league's color (used for styling) color LeagueColor // a brief description of the league (may or may not be used) @@ -54,7 +54,7 @@ model IndividualLeagueData { // the amount of xp required to be in the league xpRequirement Int - // when the league resets + // when the league resets (usually weekly) resetDate DateTime // leagues such as the bronze league cannot be relegated @@ -64,28 +64,24 @@ model IndividualLeagueData { // the league's icon icon String? - // Minimum XP required to avoid relegation - minXpToAvoidRelegation Int @default(100) - - // XP threshold for promotion - xpThresholdForPromotion Int? - - // League rules - inactivityThresholdDays Int @default(7) - minWeeksBeforePromotion Int @default(1) + // league rules (may or may not be used) + inactivityThresholdDays Int? @default(7) // Maximum number of power-ups allowed per week maxPowerUpsPerWeek Int @default(3) - // Bonus XP multiplier for this league + // bonus XP multiplier for this league xpMultiplier Float @default(1.0) - // Relations + // relations leagues Leagues[] achievements LeagueAchievement[] history LeagueHistory[] } +// we can have multiple instances of the same league +// e.g. bronze league, silver league, gold league, etc. +// this is so the same league can be used for different groups of users model Leagues { // standard prisma fields uid String @id @default(uuid()) @@ -96,35 +92,33 @@ model Leagues { leagueData IndividualLeagueData @relation(fields: [leagueDataUid], references: [uid]) leagueDataUid String - // Maximum number of users in a league group - maxUsers Int @default(50) + // maximum number of users in the league + maxUsers Int @default(30) - // Current number of users + // current number of users in the league currentUsers Int @default(0) - // Whether the league is accepting new users - isOpen Boolean @default(true) - - // When this league group started + // when this league group started startDate DateTime @default(now()) // When this league group ends endDate DateTime - // Top users who will be promoted + // how many users will be promoted promotionCount Int @default(3) - // Bottom users who will be relegated + // how many users will be relegated relegationCount Int @default(5) - // Weekly challenges for bonus XP + // weekly challenges for bonus xp weeklyChallenge String? weeklyChallengeXP Int? - // Relations + // relations users UserLeague[] } +// the data tied to the user in the league model UserLeague { uid String @id @default(uuid()) createdAt DateTime @default(now()) @@ -138,10 +132,10 @@ model UserLeague { league Leagues @relation(fields: [leagueUid], references: [uid]) leagueUid String - // Their position in the league + // users position in the league position Int? - // Their weekly XP in this league + // users weekly xp in this league weeklyXp Int @default(0) // Whether they've been promoted/relegated @@ -151,10 +145,8 @@ model UserLeague { // When they joined this league joinedAt DateTime @default(now()) - // Gamification features currentStreak Int @default(0) bestPosition Int? - powerUpsRemaining Int @default(3) activePowerUps LeaguePowerUp[] powerUpExpiryTime DateTime? @@ -166,6 +158,7 @@ model UserLeague { @@unique([userUid, leagueUid]) } +// FUTURE PROOFING - may not be used initially model LeagueAchievement { uid String @id @default(uuid()) createdAt DateTime @default(now()) @@ -191,6 +184,7 @@ model LeagueAchievement { xpBonus Int @default(0) } +// FUTURE PROOFING - may not be used initially model LeagueHistory { uid String @id @default(uuid()) createdAt DateTime @default(now()) From 1e48ebcaa5cacd19754d3654d4599ad98db9e69f Mon Sep 17 00:00:00 2001 From: Logan Ford <110533855+Logannford@users.noreply.github.com> Date: Sun, 9 Mar 2025 15:50:03 +0000 Subject: [PATCH 05/18] adds admin area w/ dummy data --- src/app/(app)/admin/layout.tsx | 30 ++++ .../(app)/admin/leagues/achievements/page.tsx | 145 ++++++++++++++++ src/app/(app)/admin/leagues/list/page.tsx | 160 ++++++++++++++++++ src/app/(app)/admin/leagues/page.tsx | 132 +++++++++++++++ src/components/ui/icons/icons/map.tsx | 32 ++++ 5 files changed, 499 insertions(+) create mode 100644 src/app/(app)/admin/leagues/achievements/page.tsx create mode 100644 src/app/(app)/admin/leagues/list/page.tsx create mode 100644 src/app/(app)/admin/leagues/page.tsx create mode 100644 src/components/ui/icons/icons/map.tsx diff --git a/src/app/(app)/admin/layout.tsx b/src/app/(app)/admin/layout.tsx index 972bfae9d..bfdac25b0 100644 --- a/src/app/(app)/admin/layout.tsx +++ b/src/app/(app)/admin/layout.tsx @@ -85,6 +85,36 @@ export default async function AdminLayout({ children }: { children: React.ReactN +
  • + + Leagues + +
    +
    + + Dashboard + + + All Leagues + + + Achievements + +
    +
    +
  • +
    +
    +

    League Achievements

    +
    + + Back to Dashboard + +
    +
    + +
    + {MOCK_ACHIEVEMENTS.map((achievement) => { + const Icon = achievement.icon; + return ( + +
    +
    + +
    +
    +
    +

    {achievement.name}

    + +{achievement.xpBonus} XP +
    +

    {achievement.description}

    +
    +
    +

    Times Awarded

    +

    {achievement.timesAwarded}

    +
    +
    +

    Type

    +

    {achievement.type}

    +
    +
    +
    +
    +
    + + View Details + +
    +
    + ); + })} +
    +
    + + ); +} diff --git a/src/app/(app)/admin/leagues/list/page.tsx b/src/app/(app)/admin/leagues/list/page.tsx new file mode 100644 index 000000000..68094f87f --- /dev/null +++ b/src/app/(app)/admin/leagues/list/page.tsx @@ -0,0 +1,160 @@ +import { Metadata } from 'next'; +import Link from 'next/link'; +import AdminContainer from '@/components/app/admin/admin-container'; +import { ArrowLeft, Users, Trophy } from 'lucide-react'; +import { Card } from '@/components/ui/card'; + +export const metadata: Metadata = { + title: 'TechBlitz | League List', + description: 'View and manage all leagues', +}; + +const MOCK_LEAGUES = [ + { + id: 'bronze', + name: 'Bronze League', + color: 'CD7F32', + description: 'Entry level league for new users', + xpRequirement: 0, + xpMultiplier: 1.0, + currentUsers: 50, + maxUsers: 100, + powerUpsPerWeek: 1, + weeklyChallenge: 'Complete 5 challenges', + weeklyChallengeXP: 100, + }, + { + id: 'silver', + name: 'Silver League', + color: 'C0C0C0', + description: 'Intermediate league for consistent users', + xpRequirement: 1000, + xpMultiplier: 1.2, + currentUsers: 40, + maxUsers: 75, + powerUpsPerWeek: 2, + weeklyChallenge: 'Maintain a 3-day streak', + weeklyChallengeXP: 200, + }, + { + id: 'gold', + name: 'Gold League', + color: 'FFD700', + description: 'Advanced league for dedicated users', + xpRequirement: 5000, + xpMultiplier: 1.5, + currentUsers: 30, + maxUsers: 50, + powerUpsPerWeek: 3, + weeklyChallenge: 'Complete 10 hard challenges', + weeklyChallengeXP: 500, + }, + { + id: 'platinum', + name: 'Platinum League', + color: 'E5E4E2', + description: 'Elite league for top performers', + xpRequirement: 10000, + xpMultiplier: 2.0, + currentUsers: 20, + maxUsers: 30, + powerUpsPerWeek: 4, + weeklyChallenge: 'Maintain a 7-day streak', + weeklyChallengeXP: 1000, + }, + { + id: 'diamond', + name: 'Diamond League', + color: 'b9f2ff', + description: 'Legendary league for the best of the best', + xpRequirement: 25000, + xpMultiplier: 3.0, + currentUsers: 10, + maxUsers: 15, + powerUpsPerWeek: 5, + weeklyChallenge: 'Complete all daily challenges', + weeklyChallengeXP: 2000, + }, +]; + +export default async function LeagueListPage() { + return ( + +
    +
    +

    League List

    +
    + + Back to Dashboard + +
    +
    + +
    + {MOCK_LEAGUES.map((league) => ( + +
    +
    +
    +
    +

    {league.name}

    +

    {league.description}

    +
    +
    +

    XP Requirement

    +

    {league.xpRequirement.toLocaleString()}

    +
    +
    +

    XP Multiplier

    +

    {league.xpMultiplier}x

    +
    +
    +

    Users

    +

    + {league.currentUsers} / {league.maxUsers} +

    +
    +
    +

    Power-ups/Week

    +

    {league.powerUpsPerWeek}

    +
    +
    + {league.weeklyChallenge && ( +
    +

    Weekly Challenge

    +

    + {league.weeklyChallenge} + {league.weeklyChallengeXP && ( + (+{league.weeklyChallengeXP} XP) + )} +

    +
    + )} +
    +
    +
    + + {league.currentUsers} +
    +
    +
    + + View Details + +
    + + ))} +
    +
    + + ); +} diff --git a/src/app/(app)/admin/leagues/page.tsx b/src/app/(app)/admin/leagues/page.tsx new file mode 100644 index 000000000..e9a804e79 --- /dev/null +++ b/src/app/(app)/admin/leagues/page.tsx @@ -0,0 +1,132 @@ +import { Metadata } from 'next'; +import Link from 'next/link'; +import AdminContainer from '@/components/app/admin/admin-container'; +import { Trophy, Users, Award, ArrowUpRight } from 'lucide-react'; +import { Card } from '@/components/ui/card'; + +export const metadata: Metadata = { + title: 'TechBlitz | League Management', + description: 'Manage leagues, achievements, and user rankings', +}; + +export default async function LeaguesAdminPage() { + return ( + +
    +
    +

    League Management

    +
    + + Back to Dashboard + +
    +
    + + {/* Statistics Cards */} +
    + +
    +
    +

    Total Leagues

    +

    5

    +
    + +
    +
    + + +
    +
    +

    Active Users

    +

    150

    +
    + +
    +
    + + +
    +
    +

    Achievements Earned

    +

    324

    +
    + +
    +
    +
    + + {/* Quick Actions */} +
    + +

    Quick Actions

    +
    + +
    + +
    +

    Manage Leagues

    +

    View and edit league settings

    +
    +
    + + + + +
    + +
    +

    Achievement Settings

    +

    Configure league achievements

    +
    +
    + + +
    +
    + + +

    Recent Activity

    +
    +
    +
    + +
    +

    New user promoted to Gold League

    +

    2 minutes ago

    +
    +
    +
    +
    +
    + +
    +

    Achievement unlocked: Perfect Week

    +

    15 minutes ago

    +
    +
    +
    +
    +
    + +
    +

    5 users joined Silver League

    +

    1 hour ago

    +
    +
    +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/components/ui/icons/icons/map.tsx b/src/components/ui/icons/icons/map.tsx new file mode 100644 index 000000000..7e6d35b66 --- /dev/null +++ b/src/components/ui/icons/icons/map.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +type iconProps = { + fill?: string, + secondaryfill?: string, + strokewidth?: number, + width?: string, + height?: string, + title?: string +} + +function Map(props: iconProps) { + const fill = props.fill || 'currentColor'; + const secondaryfill = props.secondaryfill || fill; + const strokewidth = props.strokewidth || 1; + const width = props.width || '1em'; + const height = props.height || '1em'; + const title = props.title || "map"; + + return ( + + {title} + + + + + + + ); +}; + +export default Map; \ No newline at end of file From 752e7bc8c8bc06742167a94f1e0fc604f9be19d0 Mon Sep 17 00:00:00 2001 From: Logan Ford <110533855+Logannford@users.noreply.github.com> Date: Sun, 9 Mar 2025 16:13:16 +0000 Subject: [PATCH 06/18] lint --- src/app/(app)/admin/leagues/achievements/page.tsx | 2 +- src/app/(app)/admin/leagues/list/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(app)/admin/leagues/achievements/page.tsx b/src/app/(app)/admin/leagues/achievements/page.tsx index 3c1899ff3..210246d67 100644 --- a/src/app/(app)/admin/leagues/achievements/page.tsx +++ b/src/app/(app)/admin/leagues/achievements/page.tsx @@ -1,7 +1,7 @@ import { Metadata } from 'next'; import Link from 'next/link'; import AdminContainer from '@/components/app/admin/admin-container'; -import { ArrowLeft, Award, Trophy, Users, Clock, Star, Shield, Zap } from 'lucide-react'; +import { ArrowLeft, Award, Trophy, Clock, Star, Shield, Zap } from 'lucide-react'; import { Card } from '@/components/ui/card'; export const metadata: Metadata = { diff --git a/src/app/(app)/admin/leagues/list/page.tsx b/src/app/(app)/admin/leagues/list/page.tsx index 68094f87f..545792c60 100644 --- a/src/app/(app)/admin/leagues/list/page.tsx +++ b/src/app/(app)/admin/leagues/list/page.tsx @@ -1,7 +1,7 @@ import { Metadata } from 'next'; import Link from 'next/link'; import AdminContainer from '@/components/app/admin/admin-container'; -import { ArrowLeft, Users, Trophy } from 'lucide-react'; +import { ArrowLeft, Users } from 'lucide-react'; import { Card } from '@/components/ui/card'; export const metadata: Metadata = { From 4fc18f5b95feeb89886e267a4c1f76fc1b7d96bd Mon Sep 17 00:00:00 2001 From: Logan Ford <110533855+Logannford@users.noreply.github.com> Date: Sun, 9 Mar 2025 18:46:10 +0000 Subject: [PATCH 07/18] feat: ability to create new leagues (needs refining) --- src/actions/leagues/create-league.ts | 44 +++ src/app/(app)/admin/leagues/create/page.tsx | 35 +++ src/app/(app)/admin/leagues/list/page.tsx | 109 +------ src/app/(app)/admin/page.tsx | 20 ++ .../app/admin/leagues/create-league-form.tsx | 292 ++++++++++++++++++ src/utils/data/leagues/get.ts | 6 + 6 files changed, 412 insertions(+), 94 deletions(-) create mode 100644 src/actions/leagues/create-league.ts create mode 100644 src/app/(app)/admin/leagues/create/page.tsx create mode 100644 src/components/app/admin/leagues/create-league-form.tsx create mode 100644 src/utils/data/leagues/get.ts diff --git a/src/actions/leagues/create-league.ts b/src/actions/leagues/create-league.ts new file mode 100644 index 000000000..052920d1e --- /dev/null +++ b/src/actions/leagues/create-league.ts @@ -0,0 +1,44 @@ +'use server'; + +import { z } from 'zod'; +import { prisma } from '@/lib/prisma'; +import { revalidatePath } from 'next/cache'; + +const leagueSchema = z.object({ + name: z.enum(['BRONZE', 'SILVER', 'GOLD', 'PLATINUM', 'DIAMOND']), + color: z.enum(['CD7F32', 'C0C0C0', 'FFD700', 'E5E4E2', 'b9f2ff']), + description: z.string().min(10).max(500), + xpRequirement: z.number().min(0), + resetDate: z.string(), + canBeRelegated: z.boolean(), + maxPowerUpsPerWeek: z.number().min(1).max(10), + xpMultiplier: z.number().min(1).max(5), + maxUsers: z.number().min(10).max(100), + promotionCount: z.number().min(1), + relegationCount: z.number().min(1), + weeklyChallenge: z.string().optional(), + weeklyChallengeXP: z.number().optional(), +}); + +export async function createLeague(data: z.infer) { + const validatedData = leagueSchema.parse(data); + + // Create the league data first + const leagueData = await prisma.individualLeagueData.create({ + data: { + name: validatedData.name, + color: validatedData.color, + description: validatedData.description, + xpRequirement: validatedData.xpRequirement, + resetDate: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000), + canBeRelegated: validatedData.canBeRelegated, + maxPowerUpsPerWeek: validatedData.maxPowerUpsPerWeek, + xpMultiplier: validatedData.xpMultiplier, + }, + }); + + revalidatePath('/admin/leagues'); + revalidatePath('/admin/leagues/list'); + + return { leagueData }; +} diff --git a/src/app/(app)/admin/leagues/create/page.tsx b/src/app/(app)/admin/leagues/create/page.tsx new file mode 100644 index 000000000..e1e5c338e --- /dev/null +++ b/src/app/(app)/admin/leagues/create/page.tsx @@ -0,0 +1,35 @@ +import { Metadata } from 'next'; +import Link from 'next/link'; +import AdminContainer from '@/components/app/admin/admin-container'; +import { ArrowLeft } from 'lucide-react'; +import { Card } from '@/components/ui/card'; +import CreateLeagueForm from '@/components/app/admin/leagues/create-league-form'; + +export const metadata: Metadata = { + title: 'TechBlitz | Create League', + description: 'Create a new league and configure its settings', +}; + +export default async function CreateLeaguePage() { + return ( + +
    +
    +

    Create New League

    +
    + + Back to Leagues + +
    +
    + + + + +
    +
    + ); +} diff --git a/src/app/(app)/admin/leagues/list/page.tsx b/src/app/(app)/admin/leagues/list/page.tsx index 545792c60..3206851b2 100644 --- a/src/app/(app)/admin/leagues/list/page.tsx +++ b/src/app/(app)/admin/leagues/list/page.tsx @@ -1,89 +1,30 @@ import { Metadata } from 'next'; import Link from 'next/link'; import AdminContainer from '@/components/app/admin/admin-container'; -import { ArrowLeft, Users } from 'lucide-react'; +import { ArrowLeft, Plus } from 'lucide-react'; import { Card } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { getLeagues } from '@/utils/data/leagues/get'; + export const metadata: Metadata = { title: 'TechBlitz | League List', description: 'View and manage all leagues', }; -const MOCK_LEAGUES = [ - { - id: 'bronze', - name: 'Bronze League', - color: 'CD7F32', - description: 'Entry level league for new users', - xpRequirement: 0, - xpMultiplier: 1.0, - currentUsers: 50, - maxUsers: 100, - powerUpsPerWeek: 1, - weeklyChallenge: 'Complete 5 challenges', - weeklyChallengeXP: 100, - }, - { - id: 'silver', - name: 'Silver League', - color: 'C0C0C0', - description: 'Intermediate league for consistent users', - xpRequirement: 1000, - xpMultiplier: 1.2, - currentUsers: 40, - maxUsers: 75, - powerUpsPerWeek: 2, - weeklyChallenge: 'Maintain a 3-day streak', - weeklyChallengeXP: 200, - }, - { - id: 'gold', - name: 'Gold League', - color: 'FFD700', - description: 'Advanced league for dedicated users', - xpRequirement: 5000, - xpMultiplier: 1.5, - currentUsers: 30, - maxUsers: 50, - powerUpsPerWeek: 3, - weeklyChallenge: 'Complete 10 hard challenges', - weeklyChallengeXP: 500, - }, - { - id: 'platinum', - name: 'Platinum League', - color: 'E5E4E2', - description: 'Elite league for top performers', - xpRequirement: 10000, - xpMultiplier: 2.0, - currentUsers: 20, - maxUsers: 30, - powerUpsPerWeek: 4, - weeklyChallenge: 'Maintain a 7-day streak', - weeklyChallengeXP: 1000, - }, - { - id: 'diamond', - name: 'Diamond League', - color: 'b9f2ff', - description: 'Legendary league for the best of the best', - xpRequirement: 25000, - xpMultiplier: 3.0, - currentUsers: 10, - maxUsers: 15, - powerUpsPerWeek: 5, - weeklyChallenge: 'Complete all daily challenges', - weeklyChallengeXP: 2000, - }, -]; - export default async function LeagueListPage() { + const leagues = await getLeagues(); return (

    League List

    + + +
    - {MOCK_LEAGUES.map((league) => ( - + {leagues.length === 0 &&
    No leagues found
    } + {leagues.map((league) => ( +
    XP Multiplier

    {league.xpMultiplier}x

    -
    -

    Users

    -

    - {league.currentUsers} / {league.maxUsers} -

    -

    Power-ups/Week

    -

    {league.powerUpsPerWeek}

    +

    {league.maxPowerUpsPerWeek}

    - {league.weeklyChallenge && ( -
    -

    Weekly Challenge

    -

    - {league.weeklyChallenge} - {league.weeklyChallengeXP && ( - (+{league.weeklyChallengeXP} XP) - )} -

    -
    - )}
    -
    - - {league.currentUsers} -
    View Details diff --git a/src/app/(app)/admin/page.tsx b/src/app/(app)/admin/page.tsx index ec56711c9..5ca0921b4 100644 --- a/src/app/(app)/admin/page.tsx +++ b/src/app/(app)/admin/page.tsx @@ -88,6 +88,26 @@ export default function AdminDashboardPage() {
    +
    +
    +

    Leagues

    +

    Create and manage leagues.

    +
    +
    + + Create New + + + View All + +
    +
    {/* Tools Section */} diff --git a/src/components/app/admin/leagues/create-league-form.tsx b/src/components/app/admin/leagues/create-league-form.tsx new file mode 100644 index 000000000..9e3ee98cb --- /dev/null +++ b/src/components/app/admin/leagues/create-league-form.tsx @@ -0,0 +1,292 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { createLeague } from '@/actions/leagues/create-league'; +import { toast } from 'sonner'; + +const leagueSchema = z.object({ + name: z.enum(['BRONZE', 'SILVER', 'GOLD', 'PLATINUM', 'DIAMOND']), + color: z.enum(['CD7F32', 'C0C0C0', 'FFD700', 'E5E4E2', 'b9f2ff']), + description: z.string().min(10).max(500), + xpRequirement: z.number().min(0), + resetDate: z.string(), + canBeRelegated: z.boolean(), + maxPowerUpsPerWeek: z.number().min(1).max(10), + xpMultiplier: z.number().min(1).max(5), + maxUsers: z.number().min(10).max(100), + promotionCount: z.number().min(1), + relegationCount: z.number().min(1), + weeklyChallenge: z.string().optional(), + weeklyChallengeXP: z.number().optional(), +}); + +type LeagueFormData = z.infer; + +export default function CreateLeagueForm() { + const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(leagueSchema), + defaultValues: { + canBeRelegated: true, + maxPowerUpsPerWeek: 3, + xpMultiplier: 1, + maxUsers: 30, + promotionCount: 3, + relegationCount: 5, + }, + }); + + const onSubmit = async (data: LeagueFormData) => { + try { + setIsSubmitting(true); + await createLeague(data); + toast.success('League created successfully'); + router.push('/admin/leagues/list'); + } catch (error) { + toast.error('Failed to create league'); + console.error('Failed to create league:', error); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
    +
    +
    + + + {errors.name &&

    {errors.name.message}

    } +
    + +
    + + + {errors.color &&

    {errors.color.message}

    } +
    +
    + +
    + +