-
-
-
-
-
- Sebastian vs. Emily
-
-
{displayTimeLeft()}
-
-
-
+
+
+ },
+ {
+ name: "Result",
+ element: (
+
+ )
+ }
+ ]}
+ activeTab={tabManager}
+ switchTab={tab => setTabManager(tab)}
+ />
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
- >
+
);
};
-// Canvas/R3F components here
-const R3F = () => {
- return <>>;
-};
-
-export default function playground(props) {
+export default function Playground(props: Playground) {
return (
<>
-
- {/*
*/}
+
>
);
}
-export async function getStaticProps() {
- const res = await fetch(`${URL}/problems/1`);
- const data = await res.json();
- console.log(data);
+export const getServerSideProps: GetServerSideProps = async ({ query }) => {
+ const { problem } = query;
+
+ const response = await fetch(
+ `${process.env.API_ENDPOINT}/problems/${problem}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json"
+ }
+ }
+ );
+
+ const data = await response.json();
+
+ if (!data.id) {
+ return {
+ notFound: true
+ };
+ }
return {
props: {
- title: "Playground",
- problemData: data
+ problem: data
}
};
-}
+};
diff --git a/src/pages/problems/[problemId].tsx b/src/pages/problems/[problemId].tsx
deleted file mode 100644
index ba9a89b..0000000
--- a/src/pages/problems/[problemId].tsx
+++ /dev/null
@@ -1,265 +0,0 @@
-import useStore from "@/helpers/store";
-import dynamic from "next/dynamic";
-import { useRouter } from "next/router";
-import PromptPanel from "../../components/PromptPanel";
-import { useState, useEffect } from "react";
-import Editor, { useMonaco } from "@monaco-editor/react";
-import Link from "next/link";
-// import Shader from '@/components/canvas/ShaderExample/ShaderExample'
-
-// Prefer dynamic import for production builds
-// But if you have issues and need to debug in local development
-// comment these out and import above instead
-// https://github.com/pmndrs/react-three-next/issues/49
-const Shader = dynamic(
- () => import("@/components/canvas/ShaderExample/ShaderExample"),
- {
- ssr: false
- }
-);
-const URL = "http://localhost:8000";
-
-const editorConfig = {
- theme: "vs-dark",
- height: "calc(100vh - 10rem)",
- defaultLanguage: "python",
- options: {
- minimap: {
- enabled: false
- },
- fontFamily: "JetBrains Mono",
- fontSize: 14,
- readOnly: false,
- smoothScrolling: true
- }
-};
-
-type ProblemProps = {
- problemData: {
- id: number;
- title: string;
- difficulty: "Easy" | "Medium" | "Hard";
- objectives: [string];
- examples: [
- {
- input: string;
- output: string;
- explanation?: string;
- }
- ];
- starterCode: string;
- timeLimit: number;
- };
-};
-
-// DOM elements here
-const DOM = ({ problemData }: ProblemProps) => {
- const monaco = useMonaco();
- const [language, setLanguage] = useState("python");
-
- const [minutesLeft, setMinutesLeft] = useState(problemData.timeLimit); // minutes
- const [code, setCode] = useState(problemData.starterCode);
-
- // updates the countdown timer
- useEffect(() => {
- let timer = null;
-
- if (minutesLeft > 0) {
- timer = setInterval(() => {
- setMinutesLeft(prevMinutesLeft => prevMinutesLeft - 1);
- }, 60000); // 60000ms / 1 min
- }
-
- return () => clearInterval(timer);
- }, [minutesLeft]);
-
- // handles monaco custom theme creation: create theme -> apply the theme
- useEffect(() => {
- // ensures monaco instance has been created before updating the theme
- if (!monaco) {
- return;
- }
-
- enum COLORS {
- black = "#191919",
- white = "#D6D6DD",
- grey = "#6D6D6D",
- yellow = "#E5C07B",
- pink = "#CC8ECD",
- orange = "#EFB080",
- cyan = "#83D6C5",
- blue = "#7ABAEE",
- purple = "#AAA0FA"
- }
-
- // 1. create custom theme
- monaco.editor.defineTheme("code-clash", {
- base: "vs-dark",
- inherit: true,
- rules: [
- // global styling
- {
- token: "",
- foreground: COLORS.white,
- background: COLORS.black,
- fontStyle: ""
- },
-
- // white
- { token: "variable", foreground: COLORS.white },
- { token: "variable", foreground: COLORS.white },
- { token: "variable.predefined", foreground: COLORS.white },
- { token: "variable.predefined", foreground: COLORS.white },
- { token: "variable.parameter", foreground: COLORS.white },
- { token: "delimiter", foreground: COLORS.white },
- { token: "attribute.value", foreground: COLORS.white },
- { token: "delimiter", foreground: COLORS.white },
-
- // colorful
- { token: "string", foreground: COLORS.pink },
- { token: "keyword", foreground: COLORS.cyan },
- { token: "type", foreground: COLORS.cyan },
- { token: "number", foreground: COLORS.yellow },
- { token: "comment", foreground: COLORS.grey },
- { token: "constant", foreground: COLORS.orange },
- { token: "attribute.name", foreground: COLORS.purple },
- { token: "key", foreground: COLORS.purple }
- ],
- colors: {
- "editor.background": COLORS.black,
- "editor.foreground": COLORS.white
- }
- });
-
- // 2. set the Monaco instance to the custom theme
- monaco.editor.setTheme("code-clash");
- }, [monaco]);
-
- const displayTimeLeft = () => {
- if (minutesLeft >= 2) {
- return `${minutesLeft} minutes`;
- } else if (minutesLeft === 1) {
- return `1 minute... Hurry!`;
- } else {
- return "Times up!";
- }
- };
-
- // store the user's input into the current state
- const handleEditorChange = value => {
- setCode(value);
- };
-
- const handleSubmit = async () => {
- const body = {
- language: language,
- script: code
- };
-
- alert(`POST Body: ${JSON.stringify(body)}`);
-
- const res = await fetch(`${URL}/execute`, {
- method: "POST",
- headers: {
- Accept: "application/json",
- "Content-Type": "application/json"
- },
- body: JSON.stringify(body)
- });
- };
-
- return (
- <>
-
-
-
-
-
-
- Player 1 vs. Player 2
-
-
{displayTimeLeft()}
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
-// Canvas/R3F components here
-const R3F = () => {
- return <>>;
-};
-
-export default function problem(props) {
- return (
- <>
-
- {/*
*/}
- >
- );
-}
-
-// export const getServerSideProps = async ({ params }) => {
-// const res = await fetch(`${URL}/problems/${params.problemId}`);
-// const problemData = await res.json();
-
-// if (!problemData) {
-// return {
-// redirect: {
-// destination: "/problems",
-// permanent: false
-// }
-// };
-// }
-
-// return {
-// props: {
-// title: `Problem ${params.problemId}`,
-// problemData
-// }
-// };
-// };
-
-export const getStaticProps = async ({ params }) => {
- const res = await fetch(`${URL}/problems/${params.problemId}`);
- const problemData = await res.json();
-
- return {
- props: {
- title: `Problem ${params.problemId}`,
- problemData
- }
- };
-};
-
-export const getStaticPaths = async () => {
- const res = await fetch(`${URL}/problems`);
- const problems = await res.json();
-
- const paths = problems.map(problem => ({
- params: { problemId: "" + problem.id }
- }));
-
- return { paths, fallback: false };
-};
diff --git a/src/styles/global.css b/src/styles/global.css
index 9410ef4..ecfc70b 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -17,13 +17,35 @@
@apply scroll-smooth font-jetBrains;
}
body {
- @apply bg-[#0F1021] text-white;
+ @apply bg-[#0F1021] text-white overflow-x-hidden;
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ }
+ body::-webkit-scrollbar {
+ @apply hidden;
}
p {
@apply text-xl font-light font-gilroy;
}
}
+@layer components {
+ .polymorphism {
+ box-shadow: 0px 4px 4px theme(colors.primary), inset 0px 1px 2px #ffffff,
+ inset 0px 20px 80px theme(colors.primary), inset 0px -4px 4px #ffffff,
+ inset 0px -40px 40px rgba(15, 16, 33, 0.2), inset 0px 4px 12px #ffffff;
+ backdrop-filter: blur(20px);
+ }
+
+ .hide-scroll-bar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ }
+ .hide-scroll-bar::-webkit-scrollbar {
+ display: none;
+ }
+}
+
/* ! Do NOT add @layer these three button classes /*
/* https://tailwindcss.com/docs/adding-custom-styles#removing-unused-custom-css */
/* https://v2.tailwindcss.com/docs/just-in-time-mode#arbitrary-value-support */
diff --git a/src/templates/Playground/CustomEditor.tsx b/src/templates/Playground/CustomEditor.tsx
new file mode 100644
index 0000000..5800fdc
--- /dev/null
+++ b/src/templates/Playground/CustomEditor.tsx
@@ -0,0 +1,62 @@
+import Editor, { useMonaco } from "@monaco-editor/react";
+import { MutableRefObject, useEffect, useRef } from "react";
+import resolveConfig from "tailwindcss/resolveConfig";
+import tailwindConfig from "../../../tailwind.config";
+
+const { theme: tailwindVariables } = resolveConfig(tailwindConfig);
+
+type CustomEditor = {
+ editorRef: MutableRefObject
;
+ editorConfig: {
+ defaultValue: string;
+ language: string;
+ };
+};
+
+const CustomEditor = ({ editorConfig, editorRef }: CustomEditor) => {
+ const monaco = useMonaco();
+
+ useEffect(() => {
+ if (!monaco) {
+ return;
+ }
+
+ monaco.editor.defineTheme("code-clash", {
+ base: "vs-dark",
+ inherit: true,
+ rules: [
+ {
+ token: "",
+ foreground: "#ffffff",
+ background: tailwindVariables.colors["primary"]
+ }
+ ],
+ colors: {
+ "editor.foreground": "#ffffff",
+ "editor.background": tailwindVariables.colors["primary"],
+ "editor.lineHighlightBackground": tailwindVariables.colors["quaternary"]
+ }
+ });
+
+ monaco.editor.setTheme("code-clash");
+ }, [monaco]);
+
+ return (
+
+ (editorRef.current = editor)}
+ />
+
+ );
+};
+
+export default CustomEditor;
diff --git a/src/templates/Playground/GameInfo/PlayerStats.tsx b/src/templates/Playground/GameInfo/PlayerStats.tsx
new file mode 100644
index 0000000..968041a
--- /dev/null
+++ b/src/templates/Playground/GameInfo/PlayerStats.tsx
@@ -0,0 +1,73 @@
+import Crystal from "@/components/Crystal";
+import Image from "next/image";
+
+type PlayerStats = {
+ username: string;
+ profilePicture: string;
+ achievements: number;
+ totalTestCases: number;
+ completedTestCases: number;
+ inverted?: true;
+};
+
+const PlayerStats = (params: PlayerStats) => {
+ const {
+ username,
+ profilePicture,
+ achievements,
+ totalTestCases,
+ completedTestCases,
+ inverted
+ } = params;
+
+ return (
+
+
+ {/* profile picture */}
+
+
+
+
+ {/* number of test completed out of total test cases */}
+
+ {completedTestCases}/{totalTestCases}
+
+
+ {/* Test cases progress bar */}
+
+
+
+ {/* user name and total cystals */}
+
+
{username}
+
+ {achievements}
+
+
+
+
+ );
+};
+
+export default PlayerStats;
diff --git a/src/templates/Playground/GameInfo/Timer.tsx b/src/templates/Playground/GameInfo/Timer.tsx
new file mode 100644
index 0000000..5ae1bae
--- /dev/null
+++ b/src/templates/Playground/GameInfo/Timer.tsx
@@ -0,0 +1,77 @@
+import { MutableRefObject, useEffect, useRef, useState } from "react";
+
+type Timer = {
+ timeRemaining: number;
+ timeLimit: number;
+};
+
+const Timer = ({ timeLimit, timeRemaining }: Timer) => {
+ const timerRef: MutableRefObject = useRef(null);
+
+ useEffect(() => {
+ const interval = window.setInterval(() => {
+ timerRef.current.setAttribute("stroke-dasharray", getCircleDashArray());
+ }, 1000);
+
+ return () => {
+ window.clearInterval(interval);
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const formatTime = (time: number) => {
+ let minutes: number | string = Math.floor(time / 60);
+ let seconds: number | string = time % 60;
+
+ if (minutes < 10) {
+ minutes = `0${minutes}`;
+ }
+
+ if (seconds < 10) {
+ seconds = `0${seconds}`;
+ }
+
+ return `${minutes}:${seconds}`;
+ };
+
+ const getCircleDashArray = () => {
+ const equation = timeRemaining / timeLimit;
+ const timeFraction = equation - (1 / timeLimit) * (1 - equation);
+
+ return `${(timeFraction * 283).toFixed(0)} 283`;
+ };
+
+ return (
+
+
+
+ {formatTime(timeRemaining)}
+
+
+ );
+};
+
+export default Timer;
diff --git a/src/templates/Playground/GameInfo/index.tsx b/src/templates/Playground/GameInfo/index.tsx
new file mode 100644
index 0000000..28ccbb3
--- /dev/null
+++ b/src/templates/Playground/GameInfo/index.tsx
@@ -0,0 +1,52 @@
+import { ElementRef, useRef, useState } from "react";
+import PlayerStats from "./PlayerStats";
+import Timer from "./Timer";
+
+type Player = {
+ username: string;
+ profilePicture: string;
+ achievements: number;
+};
+
+type GameInfo = {
+ opponent: Player;
+ testCases: {
+ total: number;
+ userCompletion: number;
+ opponentCompletion: number;
+ };
+ timer: {
+ timeLimit: number;
+ timeRemaining: number;
+ };
+};
+
+const GameInfo = ({ opponent, testCases, timer }: GameInfo) => {
+ /**
+ * TODO: Get user from next/auth
+ */
+ const [user] = useState({
+ username: "SEBAS0228",
+ profilePicture: "/static/placeholder.jpeg",
+ achievements: 12
+ });
+
+ return (
+
+ );
+};
+
+export default GameInfo;
diff --git a/src/templates/Playground/Tabs/Description.tsx b/src/templates/Playground/Tabs/Description.tsx
new file mode 100644
index 0000000..08dcdb4
--- /dev/null
+++ b/src/templates/Playground/Tabs/Description.tsx
@@ -0,0 +1,56 @@
+type Problem = {
+ id: number;
+ title: string;
+ difficulty: "Easy" | "Medium" | "Hard";
+ objectives: string[];
+ examples: {
+ output: string;
+ input: string;
+ explanation?: string;
+ }[];
+};
+
+const Description = (problem: Problem) => {
+ const difficultyColors = {
+ Easy: "text-green-600",
+ Medium: "text-yellow-600",
+ Hard: "text-red-600"
+ };
+
+ return (
+
+
+ {problem.id}.
+ {problem.title}
+
+ ({problem.difficulty})
+
+
+
+
+ {problem.objectives.map((obj, index) => (
+
{obj}
+ ))}
+
+
+
+ {problem.examples.map((example, index) => (
+
+
Example {index + 1}
+
+
+ {Object.entries(example).map(([key, value], index) => (
+
+ {key}:
+ {value}
+
+ ))}
+
+
+ ))}
+
+
+ );
+};
+
+export default Description;
diff --git a/src/templates/Playground/Tabs/Result.tsx b/src/templates/Playground/Tabs/Result.tsx
new file mode 100644
index 0000000..24d8f9c
--- /dev/null
+++ b/src/templates/Playground/Tabs/Result.tsx
@@ -0,0 +1,65 @@
+type Result = {
+ testCases:
+ | {
+ input: string;
+ output: string;
+ expected: string;
+ Stdout?: string;
+ }[]
+ | null;
+ passed: boolean;
+};
+
+const Result = ({ testCases, passed }: Result) => {
+ if (testCases === null) {
+ return (
+
+
+ You must test your code first
+
+
+ );
+ }
+
+ return (
+
+
+ {passed ? "Congratulations!!!" : "At least one test case Failed!"}
+
+
+
+ {testCases.map((testC, index) => (
+
+
+
+
+ {Object.entries(testC).map(([key, value], index) => (
+
+
+ {key}:{" "}
+
+ {value}
+
+ ))}
+
+
+ ))}
+
+
+ );
+};
+
+export default Result;
diff --git a/src/templates/Playground/Tabs/index.tsx b/src/templates/Playground/Tabs/index.tsx
new file mode 100644
index 0000000..ac386b3
--- /dev/null
+++ b/src/templates/Playground/Tabs/index.tsx
@@ -0,0 +1,45 @@
+import { ReactNode, useEffect, useState } from "react";
+import Container from "@/components/Container";
+
+type Tabs = {
+ tabs: {
+ name: string;
+ element: ReactNode;
+ }[];
+ activeTab?: number;
+ switchTab: (tab: number) => void;
+};
+
+const Tabs = ({ tabs, switchTab, activeTab = 0 }: Tabs) => {
+ return (
+
+
+
+ {tabs.map(({ name }, index) => (
+
switchTab(index)}
+ className={`${
+ activeTab === index && "border-b-2"
+ } px-6 py-3 capitalize cursor-pointer font-gilroy-bold rounded-t-2xl hover:bg-primary active:[&>*]:translate-y-1`}
+ >
+
{name}
+
+ ))}
+
+
+
+
+ {tabs[activeTab].element}
+
+
+ );
+};
+
+export default Tabs;