Merge pull request #6 from orus-dev/supabase
Supabase and backend structure #5
This commit is contained in:
Generated
+152
-2
@@ -35,6 +35,8 @@
|
||||
"@radix-ui/react-toggle": "^1.1.9",
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@supabase/ssr": "^0.8.0",
|
||||
"@supabase/supabase-js": "^2.94.1",
|
||||
"@uiw/codemirror-theme-vscode": "^4.25.4",
|
||||
"@uiw/react-codemirror": "^4.25.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -2744,6 +2746,98 @@
|
||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@supabase/auth-js": {
|
||||
"version": "2.94.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.94.1.tgz",
|
||||
"integrity": "sha512-Wt/SdmAtNNiqrcBbPlzWojLcE1bQ9OYb8PTaYF6QccFX5JeXZI0sZ01MLNE+E83UK6cK0lw4YznX0D2g08UQng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
"version": "2.94.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.94.1.tgz",
|
||||
"integrity": "sha512-A7Bx0gnclDNZ4m8+mnO2IEEzMxtUSg7cpPEBF6Ek1LpjIQkC7vvoidiV/RuntnKX43IiVcWV1f2FsAppMagEmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/postgrest-js": {
|
||||
"version": "2.94.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.94.1.tgz",
|
||||
"integrity": "sha512-N6MTghjHnMZddT48rAj8dIFgedCU97cc1ahQM74Tc+DF4UH7y2+iEfdYV3unJsylpaiWlu92Fy8Lj14Jbrmxog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js": {
|
||||
"version": "2.94.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.94.1.tgz",
|
||||
"integrity": "sha512-Wq8olpCAGmN4y2DH2kUdlcakdzNHRCde72BFS8zK5ub46bBeSUoE9DqrfeNFWKaF2gCE/cmK8aTUTorZD9jdtQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/phoenix": "^1.6.6",
|
||||
"@types/ws": "^8.18.1",
|
||||
"tslib": "2.8.1",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/ssr": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.8.0.tgz",
|
||||
"integrity": "sha512-/PKk8kNFSs8QvvJ2vOww1mF5/c5W8y42duYtXvkOSe+yZKRgTTZywYG2l41pjhNomqESZCpZtXuWmYjFRMV+dw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@supabase/supabase-js": "^2.76.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/storage-js": {
|
||||
"version": "2.94.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.94.1.tgz",
|
||||
"integrity": "sha512-/Mi18LGyrugPwtfqETfAqEGcBQotY/7IMsTGYgEFdqr8cQq280BVQWjN2wI9KibWtshPp0Ryvil5Uzd5YfM7kA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iceberg-js": "^0.8.1",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "2.94.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.94.1.tgz",
|
||||
"integrity": "sha512-87vOY8n3WHB3m+a/KeySj07djOQVuRA5qgX5E7db1eDkaZ1of5M+3t/tv6eYYy4BfqxuHMZuCe5uVrO/oyvoow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/auth-js": "2.94.1",
|
||||
"@supabase/functions-js": "2.94.1",
|
||||
"@supabase/postgrest-js": "2.94.1",
|
||||
"@supabase/realtime-js": "2.94.1",
|
||||
"@supabase/storage-js": "2.94.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
@@ -3091,12 +3185,17 @@
|
||||
"version": "20.19.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
|
||||
"integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/phoenix": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
|
||||
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz",
|
||||
@@ -3117,6 +3216,15 @@
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/codemirror-extensions-basic-setup": {
|
||||
"version": "4.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.4.tgz",
|
||||
@@ -3300,6 +3408,19 @@
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
@@ -3554,6 +3675,15 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/iceberg-js": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
|
||||
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
@@ -4613,7 +4743,6 @@
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/use-callback-ref": {
|
||||
@@ -4695,6 +4824,27 @@
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
"@radix-ui/react-toggle": "^1.1.9",
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@supabase/ssr": "^0.8.0",
|
||||
"@supabase/supabase-js": "^2.94.1",
|
||||
"@uiw/codemirror-theme-vscode": "^4.25.4",
|
||||
"@uiw/react-codemirror": "^4.25.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
||||
@@ -25,158 +25,19 @@ import {
|
||||
|
||||
import LeaderboardRow from "@/app/leaderboards/components/LeaderboardRow";
|
||||
import StatCard from "@/components/app/StatCard";
|
||||
|
||||
const globalLeaderboard = [
|
||||
{
|
||||
rank: 1,
|
||||
username: "speedmaster_x",
|
||||
pb: "2:34.21",
|
||||
runsCount: 2341,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 2,
|
||||
username: "algo_ninja",
|
||||
pb: "2:38.54",
|
||||
runsCount: 1876,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 3,
|
||||
username: "clockwork",
|
||||
pb: "2:41.02",
|
||||
runsCount: 3102,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 4,
|
||||
username: "fastfingers99",
|
||||
pb: "2:45.33",
|
||||
runsCount: 987,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 5,
|
||||
username: "recursion_queen",
|
||||
pb: "2:47.89",
|
||||
runsCount: 1543,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 6,
|
||||
username: "bitshifter",
|
||||
pb: "2:52.11",
|
||||
runsCount: 2109,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 7,
|
||||
username: "nullpointer",
|
||||
pb: "2:54.67",
|
||||
runsCount: 876,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 8,
|
||||
username: "stackattack",
|
||||
pb: "2:58.23",
|
||||
runsCount: 1234,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 9,
|
||||
username: "looplegend",
|
||||
pb: "3:01.45",
|
||||
runsCount: 654,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 10,
|
||||
username: "coderunner",
|
||||
pb: "3:05.78",
|
||||
runsCount: 1098,
|
||||
category: "Any%",
|
||||
},
|
||||
];
|
||||
const weeklyLeaderboard = [
|
||||
{
|
||||
rank: 1,
|
||||
username: "weekly_warrior",
|
||||
pb: "2:51.33",
|
||||
runsCount: 156,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 2,
|
||||
username: "speedmaster_x",
|
||||
pb: "2:53.12",
|
||||
runsCount: 89,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 3,
|
||||
username: "newbie_grinder",
|
||||
pb: "2:58.67",
|
||||
runsCount: 234,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 4,
|
||||
username: "algo_ninja",
|
||||
pb: "3:02.45",
|
||||
runsCount: 67,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 5,
|
||||
username: "fresh_runner",
|
||||
pb: "3:05.89",
|
||||
runsCount: 45,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 6,
|
||||
username: "practice_makes",
|
||||
pb: "3:08.12",
|
||||
runsCount: 112,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 7,
|
||||
username: "splitmaster",
|
||||
pb: "3:12.34",
|
||||
runsCount: 78,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 8,
|
||||
username: "grindtime",
|
||||
pb: "3:15.56",
|
||||
runsCount: 92,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 9,
|
||||
username: "pb_hunter",
|
||||
pb: "3:18.90",
|
||||
runsCount: 56,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 10,
|
||||
username: "reset_king",
|
||||
pb: "3:22.11",
|
||||
runsCount: 187,
|
||||
category: "Any%",
|
||||
},
|
||||
];
|
||||
import useAction from "@/hook/use-action";
|
||||
import {
|
||||
getGlobalLeaderboard,
|
||||
getWeeklyLeaderboard,
|
||||
} from "@/modules/leaderboard/actions";
|
||||
|
||||
export default function Leaderboards() {
|
||||
const [global, globalLoaded, globalError] = useAction(getGlobalLeaderboard);
|
||||
const [weekly, weeklyLoaded, weeklyError] = useAction(getWeeklyLeaderboard);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<"global" | "weekly">("global");
|
||||
|
||||
const categories = ["Any%", "100%", "Easy Only", "No DP", "Blind"];
|
||||
|
||||
const data = activeTab === "global" ? globalLeaderboard : weeklyLeaderboard;
|
||||
const data = (activeTab === "global" ? global : weekly) || [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pt-24 pb-16">
|
||||
|
||||
@@ -8,8 +8,12 @@ import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import useAction from "@/hook/use-action";
|
||||
import { getProblems } from "@/modules/problems/actions";
|
||||
|
||||
export default function Problems() {
|
||||
const [problems, problemsLoaded, problemsError] = useAction(getProblems);
|
||||
|
||||
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
|
||||
|
||||
const categories = ["All", "Arrays", "Trees", "Graphs", "DP", "Strings"];
|
||||
@@ -83,100 +87,12 @@ export default function Problems() {
|
||||
: "grid-cols-1"
|
||||
}`}
|
||||
>
|
||||
{problems.map((problem, i) => (
|
||||
<ProblemCard key={problem.name} {...problem} index={i} />
|
||||
))}
|
||||
{problems &&
|
||||
problems.map((problem, i) => (
|
||||
<ProblemCard key={problem.name} {...problem} index={i} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- data ---------------- */
|
||||
|
||||
const problems = [
|
||||
{
|
||||
name: "Two Sum Dash",
|
||||
difficulty: "easy" as const,
|
||||
tags: ["arrays", "hash-map"],
|
||||
bestTime: "0:42",
|
||||
attempts: 12453,
|
||||
},
|
||||
{
|
||||
name: "Binary Sprint",
|
||||
difficulty: "easy" as const,
|
||||
tags: ["binary-search"],
|
||||
bestTime: "0:38",
|
||||
attempts: 8921,
|
||||
},
|
||||
{
|
||||
name: "Linked List Rush",
|
||||
difficulty: "medium" as const,
|
||||
tags: ["linked-list", "pointers"],
|
||||
bestTime: "1:24",
|
||||
attempts: 6234,
|
||||
},
|
||||
{
|
||||
name: "Tree Traverse Blitz",
|
||||
difficulty: "medium" as const,
|
||||
tags: ["trees", "recursion"],
|
||||
bestTime: "2:01",
|
||||
attempts: 5102,
|
||||
},
|
||||
{
|
||||
name: "DP Gauntlet",
|
||||
difficulty: "hard" as const,
|
||||
tags: ["dynamic-programming"],
|
||||
bestTime: "4:32",
|
||||
attempts: 2341,
|
||||
},
|
||||
{
|
||||
name: "Graph Navigator",
|
||||
difficulty: "hard" as const,
|
||||
tags: ["graphs", "bfs", "dfs"],
|
||||
bestTime: "3:45",
|
||||
attempts: 3456,
|
||||
},
|
||||
{
|
||||
name: "String Scramble",
|
||||
difficulty: "easy" as const,
|
||||
tags: ["strings", "manipulation"],
|
||||
bestTime: "0:55",
|
||||
attempts: 9876,
|
||||
},
|
||||
{
|
||||
name: "Stack Overflow",
|
||||
difficulty: "medium" as const,
|
||||
tags: ["stacks", "queues"],
|
||||
bestTime: "1:42",
|
||||
attempts: 4567,
|
||||
},
|
||||
{
|
||||
name: "Heap Hero",
|
||||
difficulty: "hard" as const,
|
||||
tags: ["heaps", "priority-queue"],
|
||||
bestTime: "3:21",
|
||||
attempts: 1890,
|
||||
},
|
||||
{
|
||||
name: "Sliding Window Slam",
|
||||
difficulty: "medium" as const,
|
||||
tags: ["sliding-window", "arrays"],
|
||||
bestTime: "1:56",
|
||||
attempts: 5678,
|
||||
},
|
||||
{
|
||||
name: "Trie Triumph",
|
||||
difficulty: "hard" as const,
|
||||
tags: ["trie", "strings"],
|
||||
bestTime: "4:12",
|
||||
attempts: 1234,
|
||||
},
|
||||
{
|
||||
name: "Backtrack Blaster",
|
||||
difficulty: "hard" as const,
|
||||
tags: ["backtracking", "recursion"],
|
||||
bestTime: "5:01",
|
||||
attempts: 987,
|
||||
},
|
||||
];
|
||||
|
||||
+5
-44
@@ -16,53 +16,14 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import LiveRunRow from "./components/LiveRunNow";
|
||||
import StatCard from "@/components/app/StatCard";
|
||||
|
||||
/* ---------------- mock data ---------------- */
|
||||
|
||||
const liveRuns = [
|
||||
{
|
||||
id: 1,
|
||||
username: "speedmaster_x",
|
||||
problem: "Two Sum Dash",
|
||||
category: "Any%",
|
||||
time: "1:52.34",
|
||||
pace: "-0:03",
|
||||
status: "pb",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: "algo_ninja",
|
||||
problem: "Binary Sprint",
|
||||
category: "Any%",
|
||||
time: "2:04.11",
|
||||
pace: "+0:05",
|
||||
status: "normal",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: "stackattack",
|
||||
problem: "String Scramble",
|
||||
category: "Easy",
|
||||
time: "0:48.92",
|
||||
pace: "-0:01",
|
||||
status: "pb",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
username: "reset_king",
|
||||
problem: "Linked List Rush",
|
||||
category: "Any%",
|
||||
time: "1:31.77",
|
||||
pace: "+0:12",
|
||||
status: "danger",
|
||||
},
|
||||
];
|
||||
|
||||
/* ---------------- page ---------------- */
|
||||
import useAction from "@/hook/use-action";
|
||||
import { getLiveRuns } from "@/modules/live-run/actions";
|
||||
|
||||
export default function LiveRuns() {
|
||||
const [liveRuns, liveRunsLoaded, liveRunsError] = useAction(getLiveRuns);
|
||||
|
||||
// Sort top runners (status === 'pb') to the top
|
||||
const sortedRuns = [...liveRuns].sort((a, b) => {
|
||||
const sortedRuns = [...(liveRuns || [])].sort((a, b) => {
|
||||
if (a.status === "pb" && b.status !== "pb") return -1;
|
||||
if (a.status !== "pb" && b.status === "pb") return 1;
|
||||
return 0;
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { Github, Mail } from "lucide-react";
|
||||
import { Button } from "../ui/button";
|
||||
import { signInWithGitHub } from "@/modules/account/actions";
|
||||
|
||||
export default function ContinueWith() {
|
||||
const continueWithGithub = () => {
|
||||
signInWithGitHub();
|
||||
};
|
||||
const continueWithGoogle = () => {};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative">
|
||||
@@ -16,11 +24,15 @@ export default function ContinueWith() {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 animate-fade-in stagger-4">
|
||||
<Button variant="outline">
|
||||
<Button variant="outline" onClick={continueWithGithub}>
|
||||
<Github className="h-4 w-4" />
|
||||
GitHub
|
||||
</Button>
|
||||
<Button variant="outline" className="animate-fade-in stagger-5">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="animate-fade-in stagger-5"
|
||||
onClick={continueWithGoogle}
|
||||
>
|
||||
<Mail className="h-4 w-4" />
|
||||
Google
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function useAction<T>(
|
||||
action: () => Promise<T>,
|
||||
dependencies = [],
|
||||
): [T | null, boolean, Error | null] {
|
||||
const [value, setValue] = useState<T | null>(null);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
action()
|
||||
.then(setValue)
|
||||
.catch(setError)
|
||||
.finally(() => setLoaded(true));
|
||||
}, dependencies);
|
||||
|
||||
return [value, loaded, error];
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import "server-only";
|
||||
|
||||
import { createServerClient, type CookieOptions } from "@supabase/ssr";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export async function createClient() {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
return createServerClient(
|
||||
process.env.SUPABASE_URL!,
|
||||
process.env.SUPABASE_SECRET_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
get(name: string) {
|
||||
return cookieStore.get(name)?.value;
|
||||
},
|
||||
set(name: string, value: string, options: CookieOptions) {
|
||||
cookieStore.set(name, value, options);
|
||||
},
|
||||
remove(name: string, options: CookieOptions) {
|
||||
cookieStore.set(name, "", options);
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
"use server";
|
||||
|
||||
import * as Core from "./core";
|
||||
import { createClient } from "@/lib/supabase";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export async function signInWithGitHub() {
|
||||
const supabase = await createClient();
|
||||
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: "github",
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return redirect("/login?msg=Trouble");
|
||||
}
|
||||
|
||||
return redirect(data.url);
|
||||
}
|
||||
|
||||
export async function signOut() {
|
||||
const supabase = await createClient();
|
||||
|
||||
const { error } = await supabase.auth.signOut();
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
"use server";
|
||||
|
||||
import { SupabaseClient, User } from "@supabase/supabase-js";
|
||||
import "server-only";
|
||||
import { UserProfile } from "../user/types";
|
||||
|
||||
export async function getProfile(
|
||||
supabase: SupabaseClient,
|
||||
user: User,
|
||||
): Promise<UserProfile> {
|
||||
// Try to fetch existing profile
|
||||
const { data: profile, error } = await supabase
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("id", user.id)
|
||||
.single();
|
||||
|
||||
if (error && error.code !== "PGRST116") {
|
||||
// PGRST116 = "No rows found" is fine
|
||||
throw error;
|
||||
}
|
||||
|
||||
// If no profile, create one
|
||||
if (!profile) {
|
||||
return getGithubProfile(supabase, user);
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
export async function getGithubProfile(
|
||||
supabase: SupabaseClient,
|
||||
user: User,
|
||||
): Promise<UserProfile> {
|
||||
const { data: newProfile, error: insertError } = await supabase
|
||||
.from("profiles")
|
||||
.insert({
|
||||
id: user.id,
|
||||
username: user.user_metadata.login || null,
|
||||
full_name: user.user_metadata.name || null,
|
||||
avatar_url: user.user_metadata.avatar_url || null,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (insertError) throw insertError;
|
||||
|
||||
return newProfile;
|
||||
}
|
||||
|
||||
export async function getSession(
|
||||
supabase: SupabaseClient,
|
||||
): Promise<[User, UserProfile]> {
|
||||
const {
|
||||
data: { session },
|
||||
error: sessionError,
|
||||
} = await supabase.auth.getSession();
|
||||
|
||||
if (sessionError || !session?.user) {
|
||||
throw new Error("No authenticated user found");
|
||||
}
|
||||
|
||||
const user = session.user;
|
||||
|
||||
const profile = await getProfile(supabase, user);
|
||||
|
||||
return [user, profile];
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
"use server";
|
||||
|
||||
import * as Core from "./core";
|
||||
import { Leaderboard } from "./types";
|
||||
|
||||
export async function getGlobalLeaderboard(): Promise<Leaderboard> {
|
||||
return [
|
||||
{
|
||||
rank: 1,
|
||||
username: "speedmaster_x",
|
||||
pb: "2:34.21",
|
||||
runsCount: 2341,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 2,
|
||||
username: "algo_ninja",
|
||||
pb: "2:38.54",
|
||||
runsCount: 1876,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 3,
|
||||
username: "clockwork",
|
||||
pb: "2:41.02",
|
||||
runsCount: 3102,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 4,
|
||||
username: "fastfingers99",
|
||||
pb: "2:45.33",
|
||||
runsCount: 987,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 5,
|
||||
username: "recursion_queen",
|
||||
pb: "2:47.89",
|
||||
runsCount: 1543,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 6,
|
||||
username: "bitshifter",
|
||||
pb: "2:52.11",
|
||||
runsCount: 2109,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 7,
|
||||
username: "nullpointer",
|
||||
pb: "2:54.67",
|
||||
runsCount: 876,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 8,
|
||||
username: "stackattack",
|
||||
pb: "2:58.23",
|
||||
runsCount: 1234,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 9,
|
||||
username: "looplegend",
|
||||
pb: "3:01.45",
|
||||
runsCount: 654,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 10,
|
||||
username: "coderunner",
|
||||
pb: "3:05.78",
|
||||
runsCount: 1098,
|
||||
category: "Any%",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export async function getWeeklyLeaderboard(): Promise<Leaderboard> {
|
||||
return [
|
||||
{
|
||||
rank: 1,
|
||||
username: "weekly_warrior",
|
||||
pb: "2:51.33",
|
||||
runsCount: 156,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 2,
|
||||
username: "speedmaster_x",
|
||||
pb: "2:53.12",
|
||||
runsCount: 89,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 3,
|
||||
username: "newbie_grinder",
|
||||
pb: "2:58.67",
|
||||
runsCount: 234,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 4,
|
||||
username: "algo_ninja",
|
||||
pb: "3:02.45",
|
||||
runsCount: 67,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 5,
|
||||
username: "fresh_runner",
|
||||
pb: "3:05.89",
|
||||
runsCount: 45,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 6,
|
||||
username: "practice_makes",
|
||||
pb: "3:08.12",
|
||||
runsCount: 112,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 7,
|
||||
username: "splitmaster",
|
||||
pb: "3:12.34",
|
||||
runsCount: 78,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 8,
|
||||
username: "grindtime",
|
||||
pb: "3:15.56",
|
||||
runsCount: 92,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 9,
|
||||
username: "pb_hunter",
|
||||
pb: "3:18.90",
|
||||
runsCount: 56,
|
||||
category: "Any%",
|
||||
},
|
||||
{
|
||||
rank: 10,
|
||||
username: "reset_king",
|
||||
pb: "3:22.11",
|
||||
runsCount: 187,
|
||||
category: "Any%",
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
"use server";
|
||||
|
||||
import "server-only";
|
||||
@@ -0,0 +1,9 @@
|
||||
export type Leaderboard = LeaderboardRank[];
|
||||
|
||||
export interface LeaderboardRank {
|
||||
rank: number;
|
||||
username: string;
|
||||
pb: string;
|
||||
runsCount: number;
|
||||
category: string;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
"use server";
|
||||
|
||||
import * as Core from "./core";
|
||||
import { Run } from "./types";
|
||||
|
||||
export async function getLiveRuns(): Promise<Run[]> {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
username: "speedmaster_x",
|
||||
problem: "Two Sum Dash",
|
||||
category: "Any%",
|
||||
time: "1:52.34",
|
||||
pace: "-0:03",
|
||||
status: "pb",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: "algo_ninja",
|
||||
problem: "Binary Sprint",
|
||||
category: "Any%",
|
||||
time: "2:04.11",
|
||||
pace: "+0:05",
|
||||
status: "normal",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: "stackattack",
|
||||
problem: "String Scramble",
|
||||
category: "Easy",
|
||||
time: "0:48.92",
|
||||
pace: "-0:01",
|
||||
status: "pb",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
username: "reset_king",
|
||||
problem: "Linked List Rush",
|
||||
category: "Any%",
|
||||
time: "1:31.77",
|
||||
pace: "+0:12",
|
||||
status: "danger",
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
"use server";
|
||||
|
||||
import "server-only";
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface Run {
|
||||
id: number;
|
||||
username: string;
|
||||
problem: string;
|
||||
category: string;
|
||||
time: string;
|
||||
pace: string;
|
||||
status: string;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
"use server";
|
||||
|
||||
import * as Core from "./core";
|
||||
import { Problem } from "./types";
|
||||
|
||||
export async function getProblems(): Promise<Problem[]> {
|
||||
return [
|
||||
{
|
||||
name: "Two Sum Dash",
|
||||
difficulty: "easy" as const,
|
||||
tags: ["arrays", "hash-map"],
|
||||
bestTime: "0:42",
|
||||
attempts: 12453,
|
||||
},
|
||||
{
|
||||
name: "Binary Sprint",
|
||||
difficulty: "easy" as const,
|
||||
tags: ["binary-search"],
|
||||
bestTime: "0:38",
|
||||
attempts: 8921,
|
||||
},
|
||||
{
|
||||
name: "Linked List Rush",
|
||||
difficulty: "medium" as const,
|
||||
tags: ["linked-list", "pointers"],
|
||||
bestTime: "1:24",
|
||||
attempts: 6234,
|
||||
},
|
||||
{
|
||||
name: "Tree Traverse Blitz",
|
||||
difficulty: "medium" as const,
|
||||
tags: ["trees", "recursion"],
|
||||
bestTime: "2:01",
|
||||
attempts: 5102,
|
||||
},
|
||||
{
|
||||
name: "DP Gauntlet",
|
||||
difficulty: "hard" as const,
|
||||
tags: ["dynamic-programming"],
|
||||
bestTime: "4:32",
|
||||
attempts: 2341,
|
||||
},
|
||||
{
|
||||
name: "Graph Navigator",
|
||||
difficulty: "hard" as const,
|
||||
tags: ["graphs", "bfs", "dfs"],
|
||||
bestTime: "3:45",
|
||||
attempts: 3456,
|
||||
},
|
||||
{
|
||||
name: "String Scramble",
|
||||
difficulty: "easy" as const,
|
||||
tags: ["strings", "manipulation"],
|
||||
bestTime: "0:55",
|
||||
attempts: 9876,
|
||||
},
|
||||
{
|
||||
name: "Stack Overflow",
|
||||
difficulty: "medium" as const,
|
||||
tags: ["stacks", "queues"],
|
||||
bestTime: "1:42",
|
||||
attempts: 4567,
|
||||
},
|
||||
{
|
||||
name: "Heap Hero",
|
||||
difficulty: "hard" as const,
|
||||
tags: ["heaps", "priority-queue"],
|
||||
bestTime: "3:21",
|
||||
attempts: 1890,
|
||||
},
|
||||
{
|
||||
name: "Sliding Window Slam",
|
||||
difficulty: "medium" as const,
|
||||
tags: ["sliding-window", "arrays"],
|
||||
bestTime: "1:56",
|
||||
attempts: 5678,
|
||||
},
|
||||
{
|
||||
name: "Trie Triumph",
|
||||
difficulty: "hard" as const,
|
||||
tags: ["trie", "strings"],
|
||||
bestTime: "4:12",
|
||||
attempts: 1234,
|
||||
},
|
||||
{
|
||||
name: "Backtrack Blaster",
|
||||
difficulty: "hard" as const,
|
||||
tags: ["backtracking", "recursion"],
|
||||
bestTime: "5:01",
|
||||
attempts: 987,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
"use server";
|
||||
|
||||
import "server-only";
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface Problem {
|
||||
name: string;
|
||||
difficulty: "easy" | "medium" | "hard";
|
||||
tags: string[];
|
||||
bestTime?: string;
|
||||
attempts: number;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
"use server";
|
||||
|
||||
import * as Core from "./core";
|
||||
@@ -0,0 +1,3 @@
|
||||
"use server";
|
||||
|
||||
import "server-only";
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
username: string;
|
||||
full_name: string;
|
||||
avatar_url: string;
|
||||
}
|
||||
Reference in New Issue
Block a user