diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..21e17fa --- /dev/null +++ b/.env.development @@ -0,0 +1 @@ +API_ENDPOINT=http://localhost:8000 \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..21e17fa --- /dev/null +++ b/.env.production @@ -0,0 +1 @@ +API_ENDPOINT=http://localhost:8000 \ No newline at end of file diff --git a/db.json b/db.json index 68ea3ad..6324504 100644 --- a/db.json +++ b/db.json @@ -25,7 +25,7 @@ } ], "starterCode": "def solve(nums: list, target: int) -> list:\n\t# Code here...", - "timeLimit": 5 + "timeLimit": 60 }, { "id": 3, @@ -52,7 +52,7 @@ } ], "starterCode": "def solve(s: str) -> int:\n\t# Code here...", - "timeLimit": 15 + "timeLimit": 900 }, { "id": 4, @@ -75,7 +75,7 @@ } ], "starterCode": "def solve(nums1: list, nums2: list) -> float:\n\t# Code here...", - "timeLimit": 30 + "timeLimit": 1800 } ] } diff --git a/src/components/Container.tsx b/src/components/Container.tsx index 056dca5..81f7cb0 100644 --- a/src/components/Container.tsx +++ b/src/components/Container.tsx @@ -2,13 +2,23 @@ import { ReactNode } from "react"; type Container = { children: ReactNode; + backgroundStyles?: string; + borderStyles?: string; onHover?: true; }; -const Container = ({ children, onHover }: Container) => { +const Container = (props: Container) => { + const { children, backgroundStyles = "", borderStyles = "", onHover } = props; + return ( -
-
+
+
{children}
diff --git a/src/components/PromptPanel.tsx b/src/components/PromptPanel.tsx deleted file mode 100644 index 0255b20..0000000 --- a/src/components/PromptPanel.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from "react"; - -type PrompPanelProps = { - id: number; - title: string; - difficulty: "Easy" | "Medium" | "Hard"; - objectives: [string]; - examples: [{ output: string; input: string; explanation?: string }]; -}; - -function promptPanel(props: PrompPanelProps) { - const getDifficultyColor = (): string => { - switch (props.difficulty) { - case "Easy": - return "text-green-600"; - case "Medium": - return "text-yellow-600"; - case "Hard": - return "text-red-600"; - default: - return "text-grey-600"; - } - }; - - return ( -
-

- {props.id}. {props.title}{" "} -

-

- ({props.difficulty}) -

- -
-

Objective:

- {props.objectives.map((objective, index) => ( -
-
{objective}
-
-
- ))} - - {props.examples.map((example, index) => ( -
-

Example {index + 1}

-

- Input: - {example.input} -

- -

- Output: - {example.output} -

- - {Object.hasOwn(example, "explanation") && ( -

- Explanation: - {example.explanation} -

- )} -
-
- ))} -
- ); -} - -export default promptPanel; diff --git a/src/pages/playground.tsx b/src/pages/playground.tsx index ea2220c..3c6e972 100644 --- a/src/pages/playground.tsx +++ b/src/pages/playground.tsx @@ -1,229 +1,194 @@ -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 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 PlaygroundProps = { - problemData: { +import Tabs from "@/templates/Playground/Tabs"; +import CustomEditor from "@/templates/Playground/CustomEditor"; +import Description from "@/templates/Playground/Tabs/Description"; +import GameInfo from "@/templates/Playground/GameInfo"; +import { GetServerSideProps } from "next"; +import { useEffect, useRef, useState } from "react"; +import Result from "@/templates/Playground/Tabs/Result"; + +type Playground = { + problem: { id: number; title: string; difficulty: "Easy" | "Medium" | "Hard"; - objectives: [string]; - examples: [ - { - input: string; - output: string; - explanation?: string; - } - ]; + objectives: string[]; + examples: { + output: string; + input: string; + explanation?: string; + }[]; starterCode: string; timeLimit: number; }; }; -// DOM elements here -const DOM = ({ problemData }: PlaygroundProps) => { - const monaco = useMonaco(); - - const [language, setLanguage] = useState("python"); +const Dom = ({ problem }: Playground) => { + const editorRef = useRef(null); // monaco editor + const [tabManager, setTabManager] = useState(0); // instructions - results - (past submissions)? - const [minutesLeft, setMinutesLeft] = useState(problemData.timeLimit); // minutes - const [code, setCode] = useState(problemData.starterCode); + /** + * to start the countdown, set timer to problems.timeLimit + * To stop the countdown, set timer to null or 0, + */ + const [timer, setTimer] = useState(problem.timeLimit); useEffect(() => { - if (!monaco) { - return; + if (timer > 0) { + setTimeout(() => setTimer(timer - 1), 1000); + } else if (timer === 0) { + setTimer(null); } - enum COLORS { - black = "#191919", - white = "#D6D6DD", - grey = "#6D6D6D", - yellow = "#E5C07B", - pink = "#CC8ECD", - orange = "#EFB080", - cyan = "#83D6C5", - blue = "#7ABAEE", - purple = "#AAA0FA" - } - - 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 - } - }); - - monaco.editor.setTheme("code-clash"); - }, [monaco]); - - useEffect(() => { - let timer = null; - - if (minutesLeft > 0) { - timer = setInterval(() => { - setMinutesLeft(prevMinutesLeft => prevMinutesLeft - 1); - }, 60000); // 60000ms / 1 min + if (timer === null) { + alert("Time limit exceeded!"); } + }, [timer]); - return () => clearInterval(timer); - }, [minutesLeft]); - - const displayTimeLeft = () => { - if (minutesLeft >= 2) { - return `${minutesLeft} minutes`; - } else if (minutesLeft === 1) { - return `1 minute... Hurry!`; - } else { - return "Times up!"; - } - }; + const [testCases, setTestCases] = useState(null); + const [completedAllTestCases, setCompletedAllTestCases] = useState(false); // from the sockets if the user was able to do all the test cases - const handleEditorChange = value => { - setCode(value); + const handleSubmit = () => { + /** + * TODO: send code to sockets for validation + */ + alert("Submit: \n\n\n" + editorRef.current.getValue()); }; - 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" + const handleTest = () => { + /** + * TODO: Send code to sockets for test cases + * * editorRef.current.getValue() + */ + setTabManager(1); // show the results tab + setTestCases([ + { + input: "nums = [2,7,11,15], target = 9", + output: "[0]", + expected: "[0,1]", + Stdout: "{}" }, - body: JSON.stringify(body) - }); + { + input: "nums = [2,7,11,15], target = 9", + output: "[]", + expected: "[0,1]" + }, + { + input: "nums = [2,7,11,15], target = 9", + output: "[0,1]", + expected: "[0,1]" + }, + { + input: "nums = [2,7,11,15], target = 9", + output: "[0,1]", + expected: "[0,1]", + Stdout: "[2,7,11,15]" + } + ]); + setCompletedAllTestCases(false); }; return ( - <> -
- - -
-
-

- 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 */} +
+ 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 ( +
+ + + + (timerRef.current = element)} + /> + + + + {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) => ( +
+
+
+

Case: {index + 1}

+
+ +
+ {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;