├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── components.json
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
├── og-image.png
└── upstash-logo.png
├── src
├── app
│ ├── (root)
│ │ ├── _components
│ │ │ ├── Benefits.tsx
│ │ │ ├── ChallengeCards.tsx
│ │ │ ├── FeatureCards.tsx
│ │ │ └── Testimonials.tsx
│ │ └── page.tsx
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ ├── learn-upstash
│ │ ├── [slug]
│ │ │ └── page.tsx
│ │ └── page.tsx
│ ├── not-found.tsx
│ ├── redis-basics
│ │ └── page.tsx
│ └── redis-data-structures
│ │ ├── [slug]
│ │ └── page.tsx
│ │ └── page.tsx
├── components
│ ├── ChallengeSection.tsx
│ ├── CommandsTab.tsx
│ ├── Congrats.tsx
│ ├── Footer.tsx
│ ├── Header.tsx
│ ├── LearningSection.tsx
│ ├── OverviewTab.tsx
│ ├── ProgressBar.tsx
│ ├── RedisCli.tsx
│ ├── UpstashRedisLearningPlatform.tsx
│ ├── UseCaseTab.tsx
│ ├── decorators
│ │ ├── RotatedText.tsx
│ │ └── UnderlinedText.tsx
│ ├── providers
│ │ └── ThemeProvider.tsx
│ └── ui
│ │ ├── accordion.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── dialog.tsx
│ │ ├── progress.tsx
│ │ ├── resizable.tsx
│ │ ├── scroll-area.tsx
│ │ ├── slider.tsx
│ │ ├── tabs.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ └── use-toast.ts
├── lib
│ └── utils.ts
└── utils
│ ├── constants.ts
│ └── problems.ts
├── tailwind.config.ts
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals",
3 | "rules": {
4 | "react/no-unescaped-entities": "off"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Burak
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
37 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "upstash-playground",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@auth/mongodb-adapter": "^3.4.1",
13 | "@codemirror/lang-javascript": "^6.2.2",
14 | "@radix-ui/react-accordion": "^1.2.0",
15 | "@radix-ui/react-dialog": "^1.1.1",
16 | "@radix-ui/react-icons": "^1.3.0",
17 | "@radix-ui/react-progress": "^1.1.0",
18 | "@radix-ui/react-scroll-area": "^1.1.0",
19 | "@radix-ui/react-slider": "^1.2.0",
20 | "@radix-ui/react-slot": "^1.1.0",
21 | "@radix-ui/react-tabs": "^1.1.0",
22 | "@radix-ui/react-toast": "^1.2.1",
23 | "@tanstack/react-query": "^5.51.15",
24 | "@uiw/codemirror-theme-dracula": "^4.23.0",
25 | "@uiw/react-codemirror": "^4.23.0",
26 | "class-variance-authority": "^0.7.0",
27 | "clsx": "^2.1.1",
28 | "framer-motion": "^11.3.8",
29 | "lucide-react": "^0.412.0",
30 | "mongodb": "^6.8.0",
31 | "mongoose": "^8.5.1",
32 | "next": "14.2.5",
33 | "next-auth": "^4.24.7",
34 | "next-themes": "^0.3.0",
35 | "react": "^18",
36 | "react-confetti": "^6.1.0",
37 | "react-dom": "^18",
38 | "react-markdown": "^9.0.1",
39 | "react-resizable-panels": "^2.0.22",
40 | "react-syntax-highlighter": "^15.5.0",
41 | "tailwind-merge": "^2.4.0",
42 | "tailwindcss-animate": "^1.0.7"
43 | },
44 | "devDependencies": {
45 | "@types/node": "^20",
46 | "@types/react": "^18",
47 | "@types/react-dom": "^18",
48 | "@types/react-syntax-highlighter": "^15.5.13",
49 | "eslint": "^8",
50 | "eslint-config-next": "14.2.5",
51 | "postcss": "^8",
52 | "tailwindcss": "^3.4.1",
53 | "typescript": "^5"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/public/og-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/redis-rush/99a734cade26173a49b7235300c791ed092b21e1/public/og-image.png
--------------------------------------------------------------------------------
/public/upstash-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/redis-rush/99a734cade26173a49b7235300c791ed092b21e1/public/upstash-logo.png
--------------------------------------------------------------------------------
/src/app/(root)/_components/Benefits.tsx:
--------------------------------------------------------------------------------
1 | import { benefits } from "@/utils/constants";
2 | import { motion } from "framer-motion";
3 |
4 | const Benefits = () => {
5 | return (
6 | <>
7 |
13 |
14 | Why Choose{" "}
15 |
16 | Rush?
17 | {" "}
18 |
19 |
20 | {benefits.map((benefit, index) => (
21 |
22 |
27 |
28 |
{benefit.title}
29 |
{benefit.description}
30 |
31 |
32 | ))}
33 |
34 |
35 |
39 | >
40 | );
41 | };
42 | export default Benefits;
43 |
--------------------------------------------------------------------------------
/src/app/(root)/_components/ChallengeCards.tsx:
--------------------------------------------------------------------------------
1 | import { challenges } from "@/utils/constants";
2 | import { motion } from "framer-motion";
3 | import { useState } from "react";
4 |
5 | import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
6 | import { Button } from "@/components/ui/button";
7 | import Link from "next/link";
8 |
9 | const MotionCard = motion(Card);
10 |
11 | const ChallengeCards = () => {
12 | const [hoveredChallenge, setHoveredChallenge] = useState(null);
13 |
14 | return (
15 | <>
16 |
22 | Choose Your Path
23 |
24 |
25 | {challenges.map((challenge, index) => (
26 | setHoveredChallenge(challenge?.id)}
33 | onMouseLeave={() => setHoveredChallenge(null)}
34 | >
35 |
36 |
44 |
45 |
46 | {challenge.title}
47 | {challenge.description}
48 |
49 |
50 |
58 |
59 |
60 | ))}
61 |
62 | >
63 | );
64 | };
65 | export default ChallengeCards;
66 |
--------------------------------------------------------------------------------
/src/app/(root)/_components/FeatureCards.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
2 | import { featureCards } from "@/utils/constants";
3 | import { motion } from "framer-motion";
4 | import Image from "next/image";
5 |
6 | const FeatureCards = () => {
7 | return (
8 |
14 | {featureCards.map((card, index) => (
15 |
19 |
20 |
21 | {card.icon && }
22 | {card.img && (
23 |
24 | )}
25 | {card.title}
26 |
27 |
28 |
29 | {card.description}
30 |
31 |
32 | ))}
33 |
34 | );
35 | };
36 | export default FeatureCards;
37 |
--------------------------------------------------------------------------------
/src/app/(root)/_components/Testimonials.tsx:
--------------------------------------------------------------------------------
1 | import { testimonials } from "@/utils/constants";
2 | import { motion } from "framer-motion";
3 | import { Card, CardContent } from "@/components/ui/card";
4 | import { Star, Quote } from "lucide-react";
5 |
6 | const Testimonials = () => {
7 | return (
8 |
14 |
15 | What Is Our{" "}
16 | Goal?
17 |
18 |
19 | (To hear these from you soon! 🙌)
20 |
21 |
22 | {testimonials.map((testimonial, index) => (
23 |
29 |
30 |
31 |
32 |
33 | {[...Array(5)].map((_, i) => (
34 |
35 | ))}
36 |
37 | "{testimonial.quote}"
38 |
39 |
40 | {testimonial.name.charAt(0)}
41 |
42 |
43 |
{testimonial.name}
44 |
{testimonial.role}
45 |
46 |
47 |
48 |
49 |
50 | ))}
51 |
52 |
53 | );
54 | };
55 |
56 | export default Testimonials;
57 |
--------------------------------------------------------------------------------
/src/app/(root)/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import Link from "next/link";
3 | import { motion } from "framer-motion";
4 | import { Button } from "@/components/ui/button";
5 | import { ChevronRight } from "lucide-react";
6 | import ChallengeCards from "./_components/ChallengeCards";
7 | import FeatureCards from "./_components/FeatureCards";
8 | import Benefits from "./_components/Benefits";
9 | import Testimonials from "./_components/Testimonials";
10 | import RotatedText from "@/components/decorators/RotatedText";
11 |
12 | // hover:border-emerald-500 hover:border-green-500 hover:border-teal-500
13 | // text-emerald-400 text-green-400 text-teal-400
14 |
15 | const GridBackground = () => (
16 |
19 | );
20 |
21 | export default function Home() {
22 | return (
23 |
24 |
25 |
26 |
27 |
28 | {/* Hero Section */}
29 |
30 |
36 | Redis Rush
37 |
38 |
44 |
45 | Rush
46 | {" "}
47 | brings you a comprehensive{" "}
48 |
49 | Redis
50 | {" "}
51 | learning experience with interactive,{" "}
52 | hands-on challenges
53 |
54 |
59 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/redis-rush/99a734cade26173a49b7235300c791ed092b21e1/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 240 10% 3.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 240 10% 3.9%;
13 | --primary: 240 5.9% 10%;
14 | --primary-foreground: 0 0% 98%;
15 | --secondary: 240 4.8% 95.9%;
16 | --secondary-foreground: 240 5.9% 10%;
17 | --muted: 240 4.8% 95.9%;
18 | --muted-foreground: 240 3.8% 46.1%;
19 | --accent: 240 4.8% 95.9%;
20 | --accent-foreground: 240 5.9% 10%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 0 0% 98%;
23 | --border: 240 5.9% 90%;
24 | --input: 240 5.9% 90%;
25 | --ring: 240 10% 3.9%;
26 | --radius: 0.5rem;
27 | --chart-1: 12 76% 61%;
28 | --chart-2: 173 58% 39%;
29 | --chart-3: 197 37% 24%;
30 | --chart-4: 43 74% 66%;
31 | --chart-5: 27 87% 67%;
32 | }
33 |
34 | .dark {
35 | --background: 240 10% 3.9%;
36 | --foreground: 0 0% 98%;
37 | --card: 240 10% 3.9%;
38 | --card-foreground: 0 0% 98%;
39 | --popover: 240 10% 3.9%;
40 | --popover-foreground: 0 0% 98%;
41 | --primary: 0 0% 98%;
42 | --primary-foreground: 240 5.9% 10%;
43 | --secondary: 240 3.7% 15.9%;
44 | --secondary-foreground: 0 0% 98%;
45 | --muted: 240 3.7% 15.9%;
46 | --muted-foreground: 240 5% 64.9%;
47 | --accent: 240 3.7% 15.9%;
48 | --accent-foreground: 0 0% 98%;
49 | --destructive: 0 62.8% 30.6%;
50 | --destructive-foreground: 0 0% 98%;
51 | --border: 240 3.7% 15.9%;
52 | --input: 240 3.7% 15.9%;
53 | --ring: 240 4.9% 83.9%;
54 | --chart-1: 220 70% 50%;
55 | --chart-2: 160 60% 45%;
56 | --chart-3: 30 80% 55%;
57 | --chart-4: 280 65% 60%;
58 | --chart-5: 340 75% 55%;
59 | }
60 | }
61 |
62 | @layer base {
63 | * {
64 | @apply border-border;
65 | }
66 | body {
67 | @apply bg-background text-foreground;
68 | }
69 | }
70 |
71 | /* Basic green selection color */
72 | ::selection {
73 | background-color: #4ade80;
74 | color: #ffffff;
75 | }
76 |
77 | /* For Firefox */
78 | ::-moz-selection {
79 | background-color: #4ade80;
80 | color: #ffffff;
81 | }
82 |
83 | /* cool looking, rounded dark mode-theme scrollbar */
84 |
85 | ::-webkit-scrollbar {
86 | width: 12px;
87 | }
88 |
89 | ::-webkit-scrollbar-track {
90 | background: #1f2937; /* dark gray background */
91 | }
92 |
93 | ::-webkit-scrollbar-thumb {
94 | background: #4b5563; /* lighter gray thumb */
95 | }
96 |
97 | /* For Firefox */
98 | * {
99 | scrollbar-width: thin;
100 | scrollbar-color: #4b5563 #1f2937;
101 | }
102 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import Footer from "@/components/Footer";
2 | import Header from "@/components/Header";
3 | import { ThemeProvider } from "@/components/providers/ThemeProvider";
4 | import { Toaster } from "@/components/ui/toaster";
5 | import type { Metadata } from "next";
6 | import { Inter } from "next/font/google";
7 | import "./globals.css";
8 |
9 | const inter = Inter({ subsets: ["latin"] });
10 |
11 | export const metadata: Metadata = {
12 | metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"),
13 | title: "Redis Rush",
14 | description:
15 | "Redis Rush brings you a comprehensive Redis learning experience with interactive, hands-on challenges designed to take you from beginner to expert",
16 | openGraph: {
17 | title: "Redis Rush | Practice Redis",
18 | description:
19 | "Master Redis through hands-on challenges. Learn, practice, and excel with real-world scenarios using cutting-edge serverless technology.",
20 | url: "https://redisrush.com",
21 | siteName: "Redis Rush",
22 | images: [
23 | {
24 | url: "/og-image.png",
25 | width: 1200,
26 | height: 630,
27 | alt: "Redis Rush - Interactive Redis Learning",
28 | },
29 | ],
30 | locale: "en_US",
31 | type: "website",
32 | },
33 | twitter: {
34 | card: "summary_large_image",
35 | title: "Redis Rush | Master Redis Interactively",
36 | description:
37 | "Level up your Redis skills with hands-on challenges and real-world scenarios. Learn, practice, excel!",
38 | images: ["https://redisrush.com/og-image.png"],
39 | creator: "@asaprogrammer_",
40 | },
41 | robots: {
42 | index: true,
43 | follow: true,
44 | googleBot: {
45 | index: true,
46 | follow: true,
47 | "max-video-preview": -1,
48 | "max-image-preview": "large",
49 | "max-snippet": -1,
50 | },
51 | },
52 | };
53 |
54 | export default function RootLayout({
55 | children,
56 | }: Readonly<{
57 | children: React.ReactNode;
58 | }>) {
59 | return (
60 |
61 |
62 |
63 |
64 |
65 | {children}
66 |
67 |
68 |
69 |
70 |
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/src/app/learn-upstash/[slug]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useParams, useRouter } from "next/navigation";
4 | import Link from "next/link";
5 | import { getProblemBySlug, getNextProblemSlug, getPreviousProblemSlug } from "@/utils/problems";
6 | import UpstashRedisLearningPlatform from "@/components/UpstashRedisLearningPlatform";
7 | import { AlertCircle, Home } from "lucide-react";
8 | import { Button } from "@/components/ui/button";
9 |
10 | const ProblemNotFound = () => (
11 |
12 |
13 |
Oops! Problem Not Found
14 |
15 | We couldn't find the problem you're looking for. It might have been moved or doesn't exist.
16 |
17 |
18 |
23 |
24 |
25 | );
26 |
27 | export default function ProblemPage() {
28 | const params = useParams();
29 | const router = useRouter();
30 | const slug = params.slug as string;
31 |
32 | const problem = getProblemBySlug(slug);
33 | const nextProblemSlug = getNextProblemSlug(slug);
34 | const previousProblemSlug = getPreviousProblemSlug(slug);
35 |
36 | if (!problem) {
37 | return ;
38 | }
39 |
40 | const handleNextProblem = () => {
41 | if (nextProblemSlug) {
42 | router.push(`/learn-upstash/${nextProblemSlug}`);
43 | }
44 | };
45 |
46 | const handlePreviousProblem = () => {
47 | if (previousProblemSlug) {
48 | router.push(`/learn-upstash/${previousProblemSlug}`);
49 | }
50 | };
51 |
52 | return (
53 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/src/app/learn-upstash/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ReactNode, useEffect, useState } from "react";
4 | import { motion } from "framer-motion";
5 | import { useRouter } from "next/navigation";
6 | import { Card, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
7 | import { problems } from "@/utils/problems";
8 | import {
9 | ChevronRight,
10 | Lock,
11 | Database,
12 | List,
13 | Hash,
14 | Layers,
15 | BarChart3,
16 | ExternalLink,
17 | Cloud,
18 | DollarSign,
19 | Zap,
20 | Info,
21 | } from "lucide-react";
22 | import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
23 |
24 | const ChallengePage = () => {
25 | const router = useRouter();
26 | const [showUpstashInfo, setShowUpstashInfo] = useState(false);
27 |
28 | useEffect(() => {
29 | const handleKeyPress = (event: KeyboardEvent) => {
30 | if (event.key === "h" || event.key === "H") {
31 | setShowUpstashInfo(true);
32 | }
33 | };
34 |
35 | window.addEventListener("keydown", handleKeyPress);
36 |
37 | return () => {
38 | window.removeEventListener("keydown", handleKeyPress);
39 | };
40 | }, []);
41 |
42 | const cardVariants = {
43 | hidden: { opacity: 0, y: 20 },
44 | visible: (i: number) => ({
45 | opacity: 1,
46 | y: 0,
47 | transition: {
48 | delay: i * 0.1,
49 | duration: 0.5,
50 | },
51 | }),
52 | };
53 |
54 | const iconVariants = {
55 | hover: { scale: 1.1, transition: { duration: 0.2 } },
56 | };
57 |
58 | const problemIcons: Record>> = {
59 | "basic-caching": Database,
60 | "list-operations": List,
61 | "hash-operations": Hash,
62 | "set-operations": Layers,
63 | "sorted-set-operations": BarChart3,
64 | };
65 |
66 | const getDifficultyColor = (difficulty: string) => {
67 | switch (difficulty.toLowerCase()) {
68 | case "easy":
69 | return "bg-green-400 text-black";
70 | case "medium":
71 | return "bg-yellow-400 text-black";
72 | case "hard":
73 | return "bg-red-400 text-black";
74 | default:
75 | return "bg-gray-400 text-black";
76 | }
77 | };
78 |
79 | const getIconColor = (difficulty: string) => {
80 | switch (difficulty.toLowerCase()) {
81 | case "easy":
82 | return "text-green-400";
83 | case "medium":
84 | return "text-yellow-400";
85 | case "hard":
86 | return "text-red-400";
87 | default:
88 | return "text-gray-400";
89 | }
90 | };
91 |
92 | return (
93 |
94 |
100 |
101 | Press
102 | H
103 |
104 | to learn about
105 | Upstash
106 |
107 |
108 |
109 |
115 |
116 | Upstash Redis Challenges
117 |
118 | Master Redis with hands-on exercises
119 |
120 |
168 |
169 |
170 | {problems.map((problem, index) => {
171 | const IconComponent = problemIcons[problem.slug] || Database;
172 | const iconColor = getIconColor(problem.difficulty);
173 | return (
174 |
182 | router.push(`/learn-upstash/${problem.slug}`)}
186 | >
187 |
188 |
189 |
190 |
196 |
197 |
198 | {problem.title}
199 |
200 |
201 |
206 | {problem.difficulty}
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 | );
215 | })}
216 |
217 |
218 |
224 |
225 | More Challenges Coming Soon!
226 |
227 |
228 | {[1, 2, 3].map((_, index) => (
229 |
230 |
231 |
232 |
233 |
238 |
239 |
240 | Coming Soon
241 |
242 |
243 |
244 | New exciting challenge on its way...
245 |
246 |
247 |
248 | ))}
249 |
250 |
251 |
252 |
253 | );
254 | };
255 |
256 | export default ChallengePage;
257 |
258 | const FeatureCard = ({ icon, title, children }: { icon: ReactNode; title: string; children: ReactNode }) => (
259 |
260 |
261 | {icon}
262 |
{title}
263 |
264 |
{children}
265 |
266 | );
267 |
--------------------------------------------------------------------------------
/src/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import Link from "next/link";
3 | import { motion } from "framer-motion";
4 | import { Home, BookOpen } from "lucide-react";
5 | import { Button } from "@/components/ui/button";
6 |
7 | const CustomNotFoundPage = () => {
8 | return (
9 |
10 |
16 |
17 | 404
18 |
19 | Oops! Page Not Found
20 |
21 |
22 | Looks like this page got lost in the Redis cache. Don't worry, even the fastest databases have
23 | their moments!
24 |
25 |
26 |
27 |
36 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default CustomNotFoundPage;
48 |
--------------------------------------------------------------------------------
/src/app/redis-basics/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Suspense, useState } from "react";
4 | import { motion } from "framer-motion";
5 | import { Card, CardHeader, CardContent } from "@/components/ui/card";
6 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
7 | import ChallengeSection from "@/components/ChallengeSection";
8 | import LearningSection from "@/components/LearningSection";
9 | import Congrats from "@/components/Congrats";
10 | import ProgressBar from "@/components/ProgressBar";
11 | import { redisBasicsContent } from "@/utils/constants";
12 |
13 | export default function RedisBasics() {
14 | const [currentSection, setCurrentSection] = useState(0);
15 | const [currentChallenge, setCurrentChallenge] = useState(0);
16 | const [activeTab, setActiveTab] = useState("learn");
17 | const [showCongrats, setShowCongrats] = useState(false);
18 |
19 | const isLastChallenge = currentChallenge === redisBasicsContent.challenges.length - 1;
20 |
21 | const handleNext = () => {
22 | if (activeTab === "learn") {
23 | setActiveTab("challenge");
24 | } else {
25 | if (currentChallenge < redisBasicsContent.challenges.length - 1) {
26 | setCurrentChallenge(currentChallenge + 1);
27 | } else {
28 | setShowCongrats(true);
29 | }
30 | }
31 | };
32 |
33 | const handlePrevious = () => {
34 | if (activeTab === "challenge" && currentChallenge === 0) {
35 | setActiveTab("learn");
36 | } else if (currentChallenge > 0) {
37 | setCurrentChallenge(currentChallenge - 1);
38 | } else if (currentSection > 0) {
39 | setCurrentSection(currentSection - 1);
40 | setCurrentChallenge(redisBasicsContent.challenges.length - 1);
41 | setActiveTab("challenge");
42 | }
43 | };
44 |
45 | return (
46 |
47 |
48 | {/* Blurry radial gradient */}
49 |
52 |
53 |
54 |
55 |
61 |
62 | Redis Basics
63 |
64 |
65 | Challenge {currentChallenge + 1} of {redisBasicsContent.challenges.length}
66 |
67 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
85 | Learn
86 |
87 |
91 | Challenge
92 |
93 |
94 |
95 |
96 |
97 |
98 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | {showCongrats &&
setShowCongrats(false)} />}
110 |
111 |
112 | );
113 | }
114 |
--------------------------------------------------------------------------------
/src/app/redis-data-structures/[slug]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useEffect } from "react";
4 | import { useParams, useRouter } from "next/navigation";
5 | import { motion } from "framer-motion";
6 | import { Card, CardContent } from "@/components/ui/card";
7 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
8 | import ProgressBar from "@/components/ProgressBar";
9 | import LearningSection from "@/components/LearningSection";
10 | import ChallengeSection from "@/components/ChallengeSection";
11 | import Congrats from "@/components/Congrats";
12 | import { dataStructuresContent } from "@/utils/constants";
13 |
14 | export default function DataStructurePage() {
15 | const router = useRouter();
16 | const { slug } = useParams();
17 | const [content, setContent] = useState(null);
18 | const [currentChallenge, setCurrentChallenge] = useState(0);
19 | const [activeTab, setActiveTab] = useState("learn");
20 | const [showCongrats, setShowCongrats] = useState(false);
21 |
22 | useEffect(() => {
23 | const structureContent = dataStructuresContent.find((s) => s.slug === slug);
24 | if (structureContent) {
25 | setContent(structureContent);
26 | } else {
27 | router.push("/redis-data-structures");
28 | }
29 | }, [slug, router]);
30 |
31 | if (!content) {
32 | return null;
33 | }
34 |
35 | const isLastChallenge = currentChallenge === content.challenges.length - 1;
36 |
37 | const handleNext = () => {
38 | if (activeTab === "learn") {
39 | setActiveTab("challenge");
40 | } else if (currentChallenge < content.challenges.length - 1) {
41 | setCurrentChallenge(currentChallenge + 1);
42 | } else {
43 | setShowCongrats(true);
44 | }
45 | };
46 |
47 | const handlePrevious = () => {
48 | if (activeTab === "challenge" && currentChallenge === 0) {
49 | setActiveTab("learn");
50 | } else if (currentChallenge > 0) {
51 | setCurrentChallenge(currentChallenge - 1);
52 | }
53 | };
54 |
55 | return (
56 |
57 |
58 |
64 |
65 | {content.title}
66 |
67 | Master the power of {content.title}
68 | s.slug === slug)}
70 | totalSections={dataStructuresContent.length}
71 | currentChallenge={currentChallenge}
72 | totalChallenges={dataStructuresContent.find((s) => s.slug === slug)!.challenges.length}
73 | isLastChallenge={isLastChallenge}
74 | />
75 |
76 |
77 |
78 |
79 |
80 |
81 | Learn
82 | Challenge
83 |
84 |
85 |
86 |
87 |
88 |
93 |
94 |
95 |
96 |
97 |
98 | {showCongrats &&
setShowCongrats(false)} />}
99 |
100 | );
101 | }
102 |
--------------------------------------------------------------------------------
/src/app/redis-data-structures/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import Link from "next/link";
5 | import { motion } from "framer-motion";
6 | import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
7 | import { List, Hash, LayoutGrid, BarChart3 } from "lucide-react";
8 | import { dataStructuresContent } from "@/utils/constants";
9 |
10 | const icons: Record> = {
11 | lists: List,
12 | sets: LayoutGrid,
13 | hashes: Hash,
14 | "sorted-sets": BarChart3,
15 | };
16 |
17 | const colors: Record> = {
18 | lists: { bg: "from-blue-500 to-blue-700", text: "text-blue-400", border: "border-blue-400/30" },
19 | sets: { bg: "from-green-500 to-green-700", text: "text-green-400", border: "border-green-400/30" },
20 | hashes: { bg: "from-purple-500 to-purple-700", text: "text-purple-400", border: "border-purple-400/30" },
21 | "sorted-sets": { bg: "from-red-500 to-red-700", text: "text-red-400", border: "border-red-400/30" },
22 | };
23 |
24 | export default function RedisDataStructures() {
25 | return (
26 |
27 |
28 |
34 |
35 | Redis Data Structures
36 |
37 | Master the powerful data structures in Redis
38 |
39 |
40 |
41 | {dataStructuresContent.map((structure, index) => {
42 | if (!structure.slug) return null;
43 | const Icon = icons[structure.slug];
44 | const color = colors[structure.slug];
45 |
46 | return (
47 |
53 |
54 |
58 |
59 |
60 |
63 | {index + 1}
64 |
65 |
66 |
67 | {structure.title}
68 |
69 |
70 |
71 |
72 |
75 |
76 |
77 |
78 |
79 | );
80 | })}
81 |
82 |
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/ChallengeSection.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { CardHeader, CardTitle, CardContent } from "@/components/ui/card";
3 | import { Button } from "@/components/ui/button";
4 | import { Terminal, CheckCircle2, AlertCircle, Eye, EyeOff } from "lucide-react";
5 | import RedisCLI from "./RedisCli";
6 |
7 | type ChallengeSectionProps = {
8 | challenge: {
9 | id: number;
10 | prompt: string;
11 | answer: string;
12 | };
13 | onNext: () => void;
14 | onPrevious: () => void;
15 | };
16 |
17 | const ChallengeSection = ({ challenge, onNext, onPrevious }: ChallengeSectionProps) => {
18 | const [feedback, setFeedback] = useState("");
19 | const [isChallengeCompleted, setIsChallengeCompleted] = useState(false);
20 | const [showSolution, setShowSolution] = useState(false);
21 | const [isTransitioning, setIsTransitioning] = useState(false);
22 |
23 | useEffect(() => {
24 | if (isChallengeCompleted) {
25 | const timer = setTimeout(() => {
26 | setIsTransitioning(true);
27 | setTimeout(() => {
28 | setFeedback("");
29 | setIsChallengeCompleted(false);
30 | setIsTransitioning(false);
31 | }, 300);
32 | }, 2000);
33 | return () => clearTimeout(timer);
34 | }
35 | }, [isChallengeCompleted]);
36 |
37 | const handleSubmit = async (input: string) => {
38 | if (input.toLowerCase().trim() === challenge.answer.toLowerCase()) {
39 | setFeedback("Correct! Great job!");
40 | setIsChallengeCompleted(true);
41 | setShowSolution(false);
42 | onNext();
43 | } else {
44 | setFeedback("Not quite. Try again!");
45 | setIsChallengeCompleted(false);
46 | }
47 | };
48 |
49 | return (
50 | <>
51 |
52 | Challenge
53 |
54 |
55 |
56 |
57 | {challenge.prompt}
58 |
59 |
60 | {feedback && (
61 |
71 | {isChallengeCompleted ? (
72 |
73 | ) : (
74 |
75 | )}
76 | {feedback}
77 |
78 | )}
79 | {showSolution && (
80 |
81 |
82 | Solution: {challenge.answer}
83 |
84 |
85 | )}
86 |
87 |
94 |
108 |
109 |
110 | >
111 | );
112 | };
113 |
114 | export default ChallengeSection;
115 |
--------------------------------------------------------------------------------
/src/components/CommandsTab.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent } from "@/components/ui/card";
2 | import SyntaxHighlighter from "react-syntax-highlighter";
3 | import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
4 |
5 | const CommandsTab = ({ keyCommands }: { keyCommands: { name: string; description: string }[] }) => {
6 | return (
7 |
8 |
9 |
10 | {keyCommands?.map((command, index) => {
11 | const { name, description } = command;
12 | return (
13 |
17 |
18 | {index + 1}. {name.trim()}
19 |
20 |
{description.trim()}
21 |
26 | {name.trim()}
27 |
28 |
29 | );
30 | })}
31 |
32 |
33 |
34 | );
35 | };
36 | export default CommandsTab;
37 |
--------------------------------------------------------------------------------
/src/components/Congrats.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import Confetti from "react-confetti";
3 | import { motion } from "framer-motion";
4 | import { Button } from "@/components/ui/button";
5 | import Link from "next/link";
6 |
7 | interface CongratsProps {
8 | onClose: () => void;
9 | }
10 |
11 | const Congrats: React.FC = ({ onClose }) => {
12 | const [windowDimension, setWindowDimension] = useState({ width: 0, height: 0 });
13 |
14 | useEffect(() => {
15 | setWindowDimension({ width: window.innerWidth, height: window.innerHeight });
16 | }, []);
17 |
18 | return (
19 |
20 |
26 |
32 | Congratulations!
33 |
34 | You've completed all the challenges on this section. Let's move on to the next one.
35 |
36 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default Congrats;
45 |
--------------------------------------------------------------------------------
/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { Zap, Twitter, Github, Youtube } from "lucide-react";
2 | import Link from "next/link";
3 |
4 | const Footer = () => {
5 | const socialLinks = [
6 | { href: "https://x.com/asaprogrammer_", icon: Twitter, label: "Twitter" },
7 | { href: "https://github.com/burakorkmez/redis-rush", icon: Github, label: "GitHub" },
8 | { href: "https://www.youtube.com/@asaprogrammer_", icon: Youtube, label: "YouTube" },
9 | ];
10 |
11 | return (
12 |
58 | );
59 | };
60 |
61 | export default Footer;
62 |
--------------------------------------------------------------------------------
/src/components/Header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useState } from "react";
3 | import Link from "next/link";
4 | import { motion } from "framer-motion";
5 | import { Button } from "@/components/ui/button";
6 | import { Zap, Menu, X, Github, Star } from "lucide-react";
7 |
8 | const Header = () => {
9 | const [isMenuOpen, setIsMenuOpen] = useState(false);
10 |
11 | const toggleMenu = () => setIsMenuOpen(!isMenuOpen);
12 |
13 | const navItems = [
14 | { href: "/redis-basics", label: "Basics" },
15 | { href: "/redis-data-structures", label: "Data Structures" },
16 | { href: "/learn-upstash", label: "Upstash" },
17 | ];
18 |
19 | return (
20 |
21 |
22 |
23 |
24 | rush
25 |
26 |
27 | BETA
28 |
29 |
30 | {/* Hamburger menu for mobile */}
31 |
34 |
35 | {/* Navigation for larger screens */}
36 |
49 |
50 |
51 | {/* Mobile menu */}
52 | {isMenuOpen && (
53 |
54 |
68 |
69 | )}
70 |
71 | );
72 | };
73 |
74 | export default Header;
75 |
76 | const GithubStarButton = () => {
77 | return (
78 |
86 |
87 | Star us
88 |
96 |
97 | :)
98 |
99 |
100 | );
101 | };
102 |
--------------------------------------------------------------------------------
/src/components/LearningSection.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { motion } from "framer-motion";
3 | import { Button } from "@/components/ui/button";
4 | import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
5 | import { ArrowRight, Book, Code, Lightbulb } from "lucide-react";
6 |
7 | import UseCaseTab from "./UseCaseTab";
8 | import { useRouter, useSearchParams } from "next/navigation";
9 | import OverviewTab from "./OverviewTab";
10 | import CommandsTab from "./CommandsTab";
11 | import { Section } from "@/utils/constants";
12 |
13 | interface LearningSectionProps {
14 | section: Section;
15 | onNext: () => void;
16 | }
17 |
18 | const LearningSection = ({ section, onNext }: LearningSectionProps) => {
19 | const router = useRouter();
20 | const searchParams = useSearchParams();
21 | const [activeTab, setActiveTab] = useState("overview");
22 |
23 | useEffect(() => {
24 | const tab = searchParams.get("tab");
25 | if (tab && ["overview", "commands", "usecase"].includes(tab)) {
26 | setActiveTab(tab);
27 | }
28 | }, [searchParams]);
29 |
30 | const overview = section.content.overview;
31 | const keyCommands = section.content.keyCommands;
32 | const useCase = section.content.useCase;
33 |
34 | const handleTabChange = (value: string) => {
35 | setActiveTab(value);
36 | const params = new URLSearchParams(searchParams);
37 | params.set("tab", value);
38 | router.push(`?${params.toString()}`, { scroll: false });
39 | };
40 |
41 | return (
42 |
43 |
44 | First Learn, Then Practice
45 |
46 |
47 |
48 |
49 | {["overview", "commands", "usecase"].map((tab) => (
50 |
55 |
56 | {tab === "overview" && }
57 | {tab === "commands" &&
}
58 | {tab === "usecase" && }
59 | {tab.charAt(0).toUpperCase() + tab.slice(1)}
60 |
61 |
62 | ))}
63 |
64 |
65 |
72 | {/* OVERVIEW TAB */}
73 |
74 |
75 |
76 |
77 | {/* COMMANDS TAB */}
78 |
79 |
80 |
81 |
82 | {/* USE-CASE TAB */}
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
99 |
100 |
101 | );
102 | };
103 |
104 | export default LearningSection;
105 |
--------------------------------------------------------------------------------
/src/components/OverviewTab.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent } from "./ui/card";
2 |
3 | const OverviewTab = ({ overview, title }: { overview: string; title: string }) => {
4 | const formatOverview = (text: string) => {
5 | const lines = text.split("\n").filter((line) => line.trim() !== "");
6 | const formattedLines = lines.map((line, index) => {
7 | if (line.match(/^\d+\./)) {
8 | return (
9 |
10 | {line.split(".")[0]}.
11 | {line.split(".").slice(1).join(".")}
12 |
13 | );
14 | }
15 | return (
16 |
17 | {line}
18 |
19 | );
20 | });
21 | return formattedLines;
22 | };
23 |
24 | return (
25 |
26 |
27 |
28 |
{title}
29 | {formatOverview(overview)}
30 |
31 |
32 |
33 | );
34 | };
35 | export default OverviewTab;
36 |
--------------------------------------------------------------------------------
/src/components/ProgressBar.tsx:
--------------------------------------------------------------------------------
1 | import { Progress } from "@/components/ui/progress";
2 |
3 | interface ProgressBarProps {
4 | currentSection: number;
5 | totalSections: number;
6 | currentChallenge: number;
7 | totalChallenges: number;
8 | isLastChallenge: boolean;
9 | }
10 |
11 | const ProgressBar: React.FC = ({
12 | currentSection,
13 | totalSections,
14 | currentChallenge,
15 | totalChallenges,
16 | isLastChallenge,
17 | }) => {
18 | const sectionProgress = (currentSection / totalSections) * 100;
19 | const challengeProgress = (currentChallenge / totalChallenges) * (100 / totalSections);
20 | const totalProgress = isLastChallenge ? 100 : Math.min(sectionProgress + challengeProgress, 99);
21 |
22 | return ;
23 | };
24 |
25 | export default ProgressBar;
26 |
--------------------------------------------------------------------------------
/src/components/RedisCli.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useEffect } from "react";
2 |
3 | type RedisCLIProps = {
4 | onSubmit: (input: string) => void;
5 | };
6 |
7 | const RedisCLI = ({ onSubmit }: RedisCLIProps) => {
8 | const [inputValue, setInputValue] = useState("");
9 | const [commandHistory, setCommandHistory] = useState([]);
10 | const [historyIndex, setHistoryIndex] = useState(-1);
11 | const inputRef = useRef(null);
12 | const [cursorPosition, setCursorPosition] = useState(0);
13 |
14 | useEffect(() => {
15 | // without setTimeout, the focus doesn't work. One more reason to hate react or js...
16 | setTimeout(() => {
17 | if (inputRef.current) {
18 | console.log("run if");
19 | inputRef.current.focus();
20 | }
21 | }, 100);
22 | });
23 |
24 | const handleKeyDown = (e: React.KeyboardEvent) => {
25 | if (e.key === "Enter") {
26 | e.preventDefault();
27 | handleSubmit();
28 | } else if (e.key === "ArrowUp") {
29 | e.preventDefault();
30 | if (historyIndex < commandHistory.length - 1) {
31 | let newIndex = historyIndex + 1;
32 | setHistoryIndex(newIndex);
33 | setInputValue(commandHistory[commandHistory.length - 1 - newIndex]);
34 | setCursorPosition(commandHistory[commandHistory.length - 1 - newIndex].length);
35 | }
36 | } else if (e.key === "ArrowDown") {
37 | e.preventDefault();
38 | if (historyIndex > 0) {
39 | const newIndex = historyIndex - 1;
40 | setHistoryIndex(newIndex);
41 | setInputValue(commandHistory[commandHistory.length - 1 - newIndex]);
42 | setCursorPosition(commandHistory[commandHistory.length - 1 - newIndex].length);
43 | } else if (historyIndex === 0) {
44 | setHistoryIndex(-1);
45 | setInputValue("");
46 | setCursorPosition(0);
47 | }
48 | }
49 | };
50 |
51 | const handleSubmit = () => {
52 | if (inputValue.trim()) {
53 | setCommandHistory([...commandHistory, inputValue]);
54 | onSubmit(inputValue);
55 | setInputValue("");
56 | setHistoryIndex(-1);
57 | setCursorPosition(0);
58 | }
59 | };
60 |
61 | useEffect(() => {
62 | if (inputRef.current) {
63 | inputRef.current.value = inputValue;
64 | inputRef.current.setSelectionRange(cursorPosition, cursorPosition);
65 | }
66 | }, [inputValue, cursorPosition]);
67 |
68 | return (
69 |
70 | {/*
71 | {commandHistory.map((cmd, index) => (
72 |
73 | $ {cmd}
74 |
75 | ))}
76 |
*/}
77 |
78 | $
79 | {
84 | setInputValue(e.target.value);
85 | setCursorPosition(e.target.selectionStart || 0);
86 | }}
87 | onKeyDown={handleKeyDown}
88 | className='bg-transparent text-white outline-none flex-grow'
89 | placeholder='Enter Redis command...'
90 | />
91 |
92 |
93 | );
94 | };
95 |
96 | export default RedisCLI;
97 |
--------------------------------------------------------------------------------
/src/components/UpstashRedisLearningPlatform.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 | import { motion } from "framer-motion";
3 | import CodeMirror from "@uiw/react-codemirror";
4 | import { javascript } from "@codemirror/lang-javascript";
5 | import { Card, CardContent } from "@/components/ui/card";
6 | import { Button } from "@/components/ui/button";
7 | import { Slider } from "@/components/ui/slider";
8 | import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
9 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
10 | import { Play, Code, Type, Eye, ChevronLeft, ChevronRight } from "lucide-react";
11 | import dynamic from "next/dynamic";
12 | import { Problem } from "../utils/problems";
13 | import { useToast } from "@/components/ui/use-toast";
14 |
15 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
16 | import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
17 |
18 | const ReactConfetti = dynamic(() => import("react-confetti"), { ssr: false });
19 |
20 | interface UpstashRedisLearningPlatformProps {
21 | problem: Problem;
22 | onNextProblem: () => void;
23 | onPreviousProblem: () => void;
24 | hasNextProblem: boolean;
25 | hasPreviousProblem: boolean;
26 | }
27 |
28 | // TODO: should be in a separate file
29 | class MockRedis {
30 | private storage: { [key: string]: any } = {};
31 |
32 | async set(key: string, value: any, options?: { ex?: number }) {
33 | this.storage[key] = { value, expiry: options?.ex ? Date.now() + options.ex * 1000 : null };
34 | return "OK";
35 | }
36 |
37 | async get(key: string) {
38 | const item = this.storage[key];
39 | if (item && (!item.expiry || item.expiry > Date.now())) {
40 | return item.value;
41 | }
42 | return null;
43 | }
44 |
45 | async rpush(key: string, value: any) {
46 | if (!Array.isArray(this.storage[key])) {
47 | this.storage[key] = [];
48 | }
49 | this.storage[key].push(value);
50 | return this.storage[key].length;
51 | }
52 |
53 | async lpop(key: string) {
54 | if (Array.isArray(this.storage[key]) && this.storage[key].length > 0) {
55 | return this.storage[key].shift();
56 | }
57 | return null;
58 | }
59 |
60 | async hset(key: string, field: string, value: any) {
61 | if (typeof this.storage[key] !== "object" || this.storage[key] === null) {
62 | this.storage[key] = {};
63 | }
64 | this.storage[key][field] = value;
65 | return 1;
66 | }
67 |
68 | async hgetall(key: string) {
69 | return this.storage[key] || null;
70 | }
71 |
72 | async incr(key: string) {
73 | if (typeof this.storage[key] !== "number") {
74 | this.storage[key] = 0;
75 | }
76 | this.storage[key]++;
77 | return this.storage[key];
78 | }
79 |
80 | async setnx(key: string, value: any) {
81 | if (this.storage[key] === undefined) {
82 | this.storage[key] = value;
83 | return 1;
84 | }
85 | return 0;
86 | }
87 |
88 | async del(key: string) {
89 | const existed = key in this.storage;
90 | delete this.storage[key];
91 | return existed ? 1 : 0;
92 | }
93 |
94 | async exists(key: string) {
95 | return key in this.storage ? 1 : 0;
96 | }
97 |
98 | async sadd(key: string, value: any) {
99 | if (!Array.isArray(this.storage[key])) {
100 | this.storage[key] = [];
101 | }
102 | if (!this.storage[key].includes(value)) {
103 | this.storage[key].push(value);
104 | return 1;
105 | }
106 | return 0;
107 | }
108 |
109 | async smembers(key: string) {
110 | return Array.isArray(this.storage[key]) ? this.storage[key] : [];
111 | }
112 |
113 | async publish(channel: string, message: string) {
114 | console.log(`Published to ${channel}: ${message}`);
115 | return 0; // returns 0 bc we don't have actual subscribers in this mock
116 | }
117 | }
118 |
119 | const UpstashRedisLearningPlatform = ({
120 | problem,
121 | onNextProblem,
122 | onPreviousProblem,
123 | hasNextProblem,
124 | hasPreviousProblem,
125 | }: UpstashRedisLearningPlatformProps) => {
126 | const [code, setCode] = useState(problem.initialCode);
127 | const [fontSize, setFontSize] = useState(14);
128 | const [showConfetti, setShowConfetti] = useState(false);
129 | const { toast } = useToast();
130 |
131 | const highlightText = (hint: string) => {
132 | return hint.split(/(`[^`]+`)/g).map((part, index) => {
133 | if (part.startsWith("`") && part.endsWith("`")) {
134 | return (
135 |
136 | {part.slice(1, -1)}
137 |
138 | );
139 | }
140 | return part;
141 | });
142 | };
143 |
144 | const runTests = useCallback(async () => {
145 | const results = [];
146 | const client = new MockRedis();
147 |
148 | try {
149 | // Strip out the "do not modify" section
150 | const userCode = code.split("/* DO NOT MODIFY THE CODE ABOVE */")[1];
151 |
152 | const userCodeFunction = new Function(
153 | "client",
154 | `
155 | ${userCode}
156 | return {
157 | ${userCode
158 | .match(/async function (\w+)/g)
159 | ?.map((match) => match.split(" ")[2])
160 | .join(", ")}
161 | };
162 | `
163 | );
164 |
165 | const userFunctions = userCodeFunction(client);
166 |
167 | for (const testCase of problem.testCases) {
168 | try {
169 | const passed = await testCase.run(userFunctions);
170 | results.push({
171 | name: testCase.name,
172 | passed,
173 | message: passed ? "Passed" : "Failed",
174 | });
175 | } catch (error: any) {
176 | results.push({
177 | name: testCase.name,
178 | passed: false,
179 | message: `Error: ${error.message}`,
180 | });
181 | }
182 | }
183 | } catch (error: any) {
184 | results.push({
185 | name: "Code Execution",
186 | passed: false,
187 | message: `Error: ${error.message}`,
188 | });
189 | }
190 |
191 | const allPassed = results.every((result) => result.passed);
192 | if (allPassed) {
193 | setShowConfetti(true);
194 | toast({
195 | title: "Congratulations!",
196 | description: "All tests passed successfully!",
197 | });
198 | } else {
199 | toast({
200 | title: "Tests Failed",
201 | description: "Some tests did not pass. Check the results for details.",
202 | variant: "destructive",
203 | });
204 | }
205 | }, [code, problem.testCases, toast]);
206 |
207 | return (
208 | <>
209 |
210 |
216 |
217 |
218 |
219 |
220 | Description
221 | Hints
222 |
223 |
224 |
225 |
226 |
227 |
236 | {problem.title}
237 |
238 |
247 | {problem.difficulty}
248 |
249 |
250 | {highlightText(problem.description)}
251 |
252 |
253 | Example Usage
254 |
255 |
266 | {problem.exampleUsage}
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 | Hints
275 |
276 | {problem.hints.map((hint, index) => (
277 | -
278 | •
279 | {highlightText(hint)}
280 |
281 | ))}
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
Solution
293 |
294 |
295 |
296 | setFontSize(value[0])}
299 | min={10}
300 | max={24}
301 | step={1}
302 | className='w-24'
303 | />
304 | {fontSize}px
305 |
306 |
314 |
324 |
333 |
342 |
343 |
344 |
345 | setCode(value)}
352 | style={{ fontSize: `${fontSize}px` }}
353 | />
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 | {showConfetti && (
362 |
368 | )}
369 | >
370 | );
371 | };
372 |
373 | export default UpstashRedisLearningPlatform;
374 |
--------------------------------------------------------------------------------
/src/components/UseCaseTab.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent } from "@/components/ui/card";
2 | import { Lightbulb } from "lucide-react";
3 |
4 | const UseCaseTab = ({ useCase }: { useCase: string }) => {
5 | const lines = useCase.trim().split("\n");
6 | const intro = lines[0];
7 | const steps = lines.slice(2, -2);
8 | const conclusion = lines[lines.length - 1];
9 |
10 | const highlightCommands = (text: string) => {
11 | const commands = ["SET", "GET", "EXISTS", "DEL", "MSET", "MGET", "INCR", "DECR", "EXPIRE", "TTL"];
12 | const regex = new RegExp(`\\b(${commands.join("|")})\\b`, "g");
13 | return text.replace(regex, '$1
');
14 | };
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
Use Case:
22 |
23 | {intro}
24 |
25 | {steps.map((step, index) => (
26 | -
27 | •
28 |
32 |
33 | ))}
34 |
35 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default UseCaseTab;
45 |
--------------------------------------------------------------------------------
/src/components/decorators/RotatedText.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { ReactNode } from "react";
3 |
4 | const RotatedText = ({ children, className }: { children: ReactNode; className?: string }) => {
5 | return (
6 |
7 |
14 | {children}
15 |
16 | );
17 | };
18 | export default RotatedText;
19 |
--------------------------------------------------------------------------------
/src/components/decorators/UnderlinedText.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { ReactNode } from "react";
3 |
4 | const UnderlinedText = ({ children, className }: { children: ReactNode; className?: string }) => {
5 | return (
6 |
7 | {children}
8 |
9 | );
10 | };
11 | export default UnderlinedText;
12 |
--------------------------------------------------------------------------------
/src/components/providers/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { ThemeProvider as NextThemesProvider } from "next-themes";
5 | import { type ThemeProviderProps } from "next-themes/dist/types";
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children};
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
5 | import { ChevronDownIcon } from "@radix-ui/react-icons"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Accordion = AccordionPrimitive.Root
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ))
21 | AccordionItem.displayName = "AccordionItem"
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ))
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
52 | {children}
53 |
54 | ))
55 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
56 |
57 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
58 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ))
42 | CardTitle.displayName = "CardTitle"
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLParagraphElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ))
54 | CardDescription.displayName = "CardDescription"
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ))
62 | CardContent.displayName = "CardContent"
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ))
74 | CardFooter.displayName = "CardFooter"
75 |
76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
77 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { Cross2Icon } from "@radix-ui/react-icons"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ))
26 | Progress.displayName = ProgressPrimitive.Root.displayName
27 |
28 | export { Progress }
29 |
--------------------------------------------------------------------------------
/src/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { DragHandleDots2Icon } from "@radix-ui/react-icons"
4 | import * as ResizablePrimitive from "react-resizable-panels"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ResizablePanelGroup = ({
9 | className,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
19 | )
20 |
21 | const ResizablePanel = ResizablePrimitive.Panel
22 |
23 | const ResizableHandle = ({
24 | withHandle,
25 | className,
26 | ...props
27 | }: React.ComponentProps & {
28 | withHandle?: boolean
29 | }) => (
30 | div]:rotate-90",
33 | className
34 | )}
35 | {...props}
36 | >
37 | {withHandle && (
38 |
39 |
40 |
41 | )}
42 |
43 | )
44 |
45 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
46 |
--------------------------------------------------------------------------------
/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SliderPrimitive from "@radix-ui/react-slider"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 |
25 | ))
26 | Slider.displayName = SliderPrimitive.Root.displayName
27 |
28 | export { Slider }
29 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Cross2Icon } from "@radix-ui/react-icons"
5 | import * as ToastPrimitives from "@radix-ui/react-toast"
6 | import { cva, type VariantProps } from "class-variance-authority"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const ToastProvider = ToastPrimitives.Provider
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ))
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26 |
27 | const toastVariants = cva(
28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
29 | {
30 | variants: {
31 | variant: {
32 | default: "border bg-background text-foreground",
33 | destructive:
34 | "destructive group border-destructive bg-destructive text-destructive-foreground",
35 | },
36 | },
37 | defaultVariants: {
38 | variant: "default",
39 | },
40 | }
41 | )
42 |
43 | const Toast = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef &
46 | VariantProps
47 | >(({ className, variant, ...props }, ref) => {
48 | return (
49 |
54 | )
55 | })
56 | Toast.displayName = ToastPrimitives.Root.displayName
57 |
58 | const ToastAction = React.forwardRef<
59 | React.ElementRef,
60 | React.ComponentPropsWithoutRef
61 | >(({ className, ...props }, ref) => (
62 |
70 | ))
71 | ToastAction.displayName = ToastPrimitives.Action.displayName
72 |
73 | const ToastClose = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
86 |
87 |
88 | ))
89 | ToastClose.displayName = ToastPrimitives.Close.displayName
90 |
91 | const ToastTitle = React.forwardRef<
92 | React.ElementRef,
93 | React.ComponentPropsWithoutRef
94 | >(({ className, ...props }, ref) => (
95 |
100 | ))
101 | ToastTitle.displayName = ToastPrimitives.Title.displayName
102 |
103 | const ToastDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | ToastDescription.displayName = ToastPrimitives.Description.displayName
114 |
115 | type ToastProps = React.ComponentPropsWithoutRef
116 |
117 | type ToastActionElement = React.ReactElement
118 |
119 | export {
120 | type ToastProps,
121 | type ToastActionElement,
122 | ToastProvider,
123 | ToastViewport,
124 | Toast,
125 | ToastTitle,
126 | ToastDescription,
127 | ToastClose,
128 | ToastAction,
129 | }
130 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "@/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 1
12 | const TOAST_REMOVE_DELAY = 1000000
13 |
14 | type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: "REMOVE_TOAST",
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case "ADD_TOAST":
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case "UPDATE_TOAST":
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case "DISMISS_TOAST": {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case "REMOVE_TOAST":
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: "UPDATE_TOAST",
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154 |
155 | dispatch({
156 | type: "ADD_TOAST",
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/constants.ts:
--------------------------------------------------------------------------------
1 | import { BarChart, BookOpen, Briefcase, Code, Layout, Rocket, Shield, Users, Zap } from "lucide-react";
2 |
3 | export const featureCards = [
4 | {
5 | title: "Interactive Learning",
6 | description: "Hands-on challenges for practical skill development",
7 | icon: Layout,
8 | color: "teal",
9 | },
10 | {
11 | title: "Real-World Use Cases",
12 | description: "Industry-relevant scenarios and case studies",
13 | icon: Briefcase,
14 | color: "teal",
15 | },
16 | {
17 | title: "Upstash Integration",
18 | description: "Learn with cutting-edge serverless Redis technology",
19 | img: "/upstash-logo.png",
20 | color: "teal",
21 | },
22 | ];
23 |
24 | export const challenges = [
25 | {
26 | id: 1,
27 | title: "Redis Basics",
28 | description: "Master the fundamentals",
29 | icon: BookOpen,
30 | color: "from-emerald-500 to-teal-500",
31 | href: "/redis-basics",
32 | },
33 | {
34 | id: 2,
35 | title: "Data Structures",
36 | description: "Lists, sets, and beyond",
37 | icon: Code,
38 | color: "from-emerald-500 to-teal-500",
39 | href: "/redis-data-structures",
40 | },
41 | {
42 | id: 3,
43 | title: "Practice Upstash",
44 | description: "Real-world challenges",
45 | icon: Rocket,
46 | color: "from-emerald-500 to-teal-500",
47 | href: "/learn-upstash",
48 | },
49 | ];
50 |
51 | export const benefits = [
52 | {
53 | title: "Rapid Skill Acquisition",
54 | description: "Master Redis concepts faster with our hands-on approach",
55 | icon: Zap,
56 | },
57 | { title: "Industry-Relevant Skills", description: "Learn techniques used by top tech companies", icon: Users },
58 | { title: "Real-Time Feedback", description: "Get instant feedback on your code and progress", icon: Code },
59 | { title: "Performance Tracking", description: "Monitor your growth with detailed analytics", icon: BarChart },
60 | ];
61 |
62 | export const testimonials = [
63 | {
64 | name: "Alex Johnson",
65 | role: "Senior Developer at TechCorp",
66 | quote: "Redis Rush transformed my team's understanding of Redis. It's now an integral part of our tech stack.",
67 | },
68 | {
69 | name: "Samantha Lee",
70 | role: "CTO at StartupX",
71 | quote: "The real-world scenarios in Redis Rush's challenges prepared us for scaling our infrastructure effortlessly.",
72 | },
73 | {
74 | name: "Raj Chen",
75 | role: "Software Engineer",
76 | quote: "Redis Rush's hands-on challenges fast-tracked my Redis skills, making me more confident in building high-performance apps.",
77 | },
78 | ];
79 |
80 | export interface Section {
81 | slug: string;
82 | title: string;
83 | content: {
84 | overview: string;
85 | keyCommands: {
86 | name: string;
87 | description: string;
88 | example: string;
89 | }[];
90 | useCase: string;
91 | };
92 | challenges: Challenge[];
93 | }
94 |
95 | export interface Challenge {
96 | id: number;
97 | prompt: string;
98 | answer: string;
99 | type: "command" | "multiple_choice" | "true_false";
100 | }
101 |
102 | export const redisBasicsContent: Section = {
103 | slug: "basics",
104 | title: "Redis Basics",
105 | content: {
106 | overview: `
107 | Redis is a key-value store, and its fundamental operations revolve around setting and retrieving values. These basic operations form the foundation for more complex data manipulations and are essential for understanding how Redis works.
108 | `,
109 | keyCommands: [
110 | {
111 | name: "SET key value",
112 | description: "Sets the value for a key",
113 | example: 'SET mykey "Hello Redis"',
114 | },
115 | {
116 | name: "GET key",
117 | description: "Retrieves the value of a key",
118 | example: "GET mykey",
119 | },
120 | {
121 | name: "EXISTS key",
122 | description: "Checks if a key exists",
123 | example: "EXISTS mykey",
124 | },
125 | {
126 | name: "DEL key",
127 | description: "Deletes a key",
128 | example: "DEL mykey",
129 | },
130 | {
131 | name: "MSET key1 value1 [key2 value2 ...]",
132 | description: "Sets multiple key-value pairs",
133 | example: 'MSET key1 "value1" key2 "value2"',
134 | },
135 | {
136 | name: "MGET key1 [key2 ...]",
137 | description: "Gets the values of multiple keys",
138 | example: "MGET key1 key2",
139 | },
140 | {
141 | name: "INCR key",
142 | description: "Increments the integer value of a key",
143 | example: "INCR counter",
144 | },
145 | {
146 | name: "DECR key",
147 | description: "Decrements the integer value of a key",
148 | example: "DECR counter",
149 | },
150 | {
151 | name: "EXPIRE key seconds",
152 | description: "Sets a key's time to live in seconds",
153 | example: "EXPIRE mykey 60",
154 | },
155 | {
156 | name: "TTL key",
157 | description: "Returns the remaining time to live of a key",
158 | example: "TTL mykey",
159 | },
160 | ],
161 | useCase: `
162 | In a real-world scenario, these basic operations can be used to build a simple caching system:
163 |
164 | 1. Use SET to store frequently accessed data in Redis.
165 | 2. Use GET to retrieve this data quickly without hitting the main database.
166 | 3. Use EXPIRE to ensure the cache stays fresh by automatically removing old entries.
167 | 4. Use INCR/DECR for maintaining counters, like tracking page views or API calls.
168 | 5. Use MSET and MGET for bulk operations, improving efficiency when dealing with multiple related pieces of data.
169 |
170 | This caching system can significantly reduce load on your primary database and improve application response times.
171 | `,
172 | },
173 | challenges: [
174 | {
175 | id: 0,
176 | prompt: "Set a key 'mykey' with the value 'Hello Redis'",
177 | answer: "SET mykey 'Hello Redis'",
178 | type: "command",
179 | },
180 | {
181 | id: 1,
182 | prompt: "Retrieve the value of 'mykey'",
183 | answer: "GET mykey",
184 | type: "command",
185 | },
186 | {
187 | id: 2,
188 | prompt: "Check if 'mykey' exists",
189 | answer: "EXISTS mykey",
190 | type: "command",
191 | },
192 | {
193 | id: 3,
194 | prompt: "Delete 'mykey'",
195 | answer: "DEL mykey",
196 | type: "command",
197 | },
198 | {
199 | id: 4,
200 | prompt: "Set keys 'fruit1' to 'apple' and 'fruit2' to 'banana' in one command",
201 | answer: "MSET fruit1 apple fruit2 banana",
202 | type: "command",
203 | },
204 | {
205 | id: 5,
206 | prompt: "Retrieve the values of both 'fruit1' and 'fruit2' in one command",
207 | answer: "MGET fruit1 fruit2",
208 | type: "command",
209 | },
210 | {
211 | id: 6,
212 | prompt: "Increment the value of 'counter' by 1",
213 | answer: "INCR counter",
214 | type: "command",
215 | },
216 | {
217 | id: 7,
218 | prompt: "Decrement the value of 'counter' by 1",
219 | answer: "DECR counter",
220 | type: "command",
221 | },
222 | {
223 | id: 8,
224 | prompt: "Set 'tempkey' to expire in 60 seconds",
225 | answer: "EXPIRE tempkey 60",
226 | type: "command",
227 | },
228 | {
229 | id: 9,
230 | prompt: "Check how many seconds are left until 'tempkey' expires",
231 | answer: "TTL tempkey",
232 | type: "command",
233 | },
234 | ],
235 | };
236 |
237 | export const dataStructuresContent: Section[] = [
238 | {
239 | slug: "lists",
240 | title: "Lists in Redis",
241 | content: {
242 | overview: `
243 | Redis Lists are linked lists of string values. They're ideal for:
244 | • Implementing stacks and queues
245 | • Managing shared to-do lists
246 | • Tracking latest items (like recent posts)
247 | • Creating a timeline of events
248 | `,
249 | keyCommands: [
250 | {
251 | name: "LPUSH key element [element ...]",
252 | description: "Add one or more elements to the left (head) of the list",
253 | example: "LPUSH mylist world hello",
254 | },
255 | {
256 | name: "RPUSH key element [element ...]",
257 | description: "Add one or more elements to the right (tail) of the list",
258 | example: "RPUSH mylist good morning",
259 | },
260 | {
261 | name: "LRANGE key start stop",
262 | description: "Retrieve a range of elements from the list",
263 | example: "LRANGE mylist 0 -1",
264 | },
265 | {
266 | name: "LPOP key",
267 | description: "Remove and return the first element of the list",
268 | example: "LPOP mylist",
269 | },
270 | {
271 | name: "RPOP key",
272 | description: "Remove and return the last element of the list",
273 | example: "RPOP mylist",
274 | },
275 | {
276 | name: "LLEN key",
277 | description: "Get the length of the list",
278 | example: "LLEN mylist",
279 | },
280 | {
281 | name: "LINDEX key index",
282 | description: "Get an element from a list by its index",
283 | example: "LINDEX mylist 1",
284 | },
285 | {
286 | name: "LINSERT key BEFORE|AFTER pivot element",
287 | description: "Insert an element before or after another element in the list",
288 | example: "LINSERT mylist BEFORE world there",
289 | },
290 | ],
291 | useCase: `
292 | Imagine you're building a social media application. You can use Redis Lists to maintain a user's timeline:
293 |
294 | 1. When a user posts a new status, use LPUSH to add it to their timeline list.
295 | 2. To display the most recent posts, use LRANGE to retrieve a certain number of posts.
296 | 3. If you want to limit the timeline to the last 1000 posts, you can use LTRIM after each LPUSH to keep only the most recent 1000 items.
297 |
298 | This approach ensures fast insertion of new posts and quick retrieval of the most recent content, which is crucial for a responsive social media platform.
299 | `,
300 | },
301 | challenges: [
302 | {
303 | id: 1,
304 | prompt: "Create a list 'fruits' and add 'apple' and 'banana' to it",
305 | answer: "LPUSH fruits apple banana",
306 | type: "command",
307 | },
308 | {
309 | id: 2,
310 | prompt: "Retrieve all elements from the 'fruits' list",
311 | answer: "LRANGE fruits 0 -1",
312 | type: "command",
313 | },
314 | {
315 | id: 3,
316 | prompt: "Add 'orange' to the right side of the 'fruits' list",
317 | answer: "RPUSH fruits orange",
318 | type: "command",
319 | },
320 | {
321 | id: 4,
322 | prompt: "Remove and return the leftmost element from 'fruits'",
323 | answer: "LPOP fruits",
324 | type: "command",
325 | },
326 | {
327 | id: 5,
328 | prompt: "Get the current length of the 'fruits' list",
329 | answer: "LLEN fruits",
330 | type: "command",
331 | },
332 | {
333 | id: 6,
334 | prompt: "Insert 'grape' before 'orange' in the 'fruits' list",
335 | answer: "LINSERT fruits BEFORE orange grape",
336 | type: "command",
337 | },
338 | ],
339 | },
340 | {
341 | slug: "sets",
342 | title: "Sets in Redis",
343 | content: {
344 | overview: `
345 | Redis Sets are unordered collections of unique strings. They're great for:
346 | • Tracking unique items (e.g., unique visitors)
347 | • Implementing tag systems
348 | • Performing set operations (unions, intersections)
349 | • Managing relationships in social networks
350 | `,
351 | keyCommands: [
352 | {
353 | name: "SADD key member [member ...]",
354 | description: "Add one or more members to a set",
355 | example: "SADD myset Hello World",
356 | },
357 | {
358 | name: "SMEMBERS key",
359 | description: "Get all members of a set",
360 | example: "SMEMBERS myset",
361 | },
362 | {
363 | name: "SISMEMBER key member",
364 | description: "Check if a value is in the set",
365 | example: "SISMEMBER myset Hello",
366 | },
367 | {
368 | name: "SREM key member [member ...]",
369 | description: "Remove one or more members from a set",
370 | example: "SREM myset World",
371 | },
372 | {
373 | name: "SCARD key",
374 | description: "Get the number of members in a set",
375 | example: "SCARD myset",
376 | },
377 | {
378 | name: "SINTER key [key ...]",
379 | description: "Perform set intersection",
380 | example: "SINTER set1 set2",
381 | },
382 | {
383 | name: "SUNION key [key ...]",
384 | description: "Perform set union",
385 | example: "SUNION set1 set2",
386 | },
387 | {
388 | name: "SDIFF key [key ...]",
389 | description: "Perform set difference",
390 | example: "SDIFF set1 set2",
391 | },
392 | ],
393 | useCase: `
394 | Consider a music streaming service where you want to recommend new artists to users based on their listening history:
395 |
396 | 1. For each user, maintain a set of artist IDs they've listened to.
397 | 2. When a user listens to a new artist, add the artist ID to their set using SADD.
398 | 3. To find common artists between two users, use SINTER.
399 | 4. To recommend new artists, you could use SDIFF between a user's set and a set of popular artists they haven't listened to yet.
400 |
401 | This approach allows for quick comparisons between user preferences and efficient generation of recommendations, which is crucial for a personalized user experience in a music streaming platform.
402 | `,
403 | },
404 | challenges: [
405 | {
406 | id: 1,
407 | prompt: "Create a set 'colors' with 'red', 'blue', and 'green'",
408 | answer: "SADD colors red blue green",
409 | type: "command",
410 | },
411 | {
412 | id: 2,
413 | prompt: "Check if 'yellow' is in the 'colors' set",
414 | answer: "SISMEMBER colors yellow",
415 | type: "command",
416 | },
417 | {
418 | id: 3,
419 | prompt: "Add 'blue' to 'colors' again (notice it won't add a duplicate)",
420 | answer: "SADD colors blue",
421 | type: "command",
422 | },
423 | {
424 | id: 4,
425 | prompt: "Remove 'green' from the 'colors' set",
426 | answer: "SREM colors green",
427 | type: "command",
428 | },
429 | {
430 | id: 5,
431 | prompt: "Get the number of colors in the 'colors' set",
432 | answer: "SCARD colors",
433 | type: "command",
434 | },
435 | {
436 | id: 6,
437 | prompt: "Create a new set 'primary_colors' with 'red', 'blue', and 'yellow', then find the intersection with 'colors'",
438 | answer: "SADD primary_colors red blue yellow\nSINTER colors primary_colors",
439 | type: "command",
440 | },
441 | ],
442 | },
443 | {
444 | slug: "hashes",
445 | title: "Hashes in Redis",
446 | content: {
447 | overview: `
448 | Redis Hashes are maps between string fields and string values. They're perfect for:
449 | • Representing objects
450 | • Storing user profiles or configurations
451 | • Tracking multiple related pieces of data
452 | • Caching database rows or API responses
453 | `,
454 | keyCommands: [
455 | {
456 | name: "HSET key field value [field value ...]",
457 | description: "Set field(s) in a hash",
458 | example: 'HSET user:1 name "John Doe" email john@example.com',
459 | },
460 | {
461 | name: "HGET key field",
462 | description: "Get the value of a field",
463 | example: "HGET user:1 name",
464 | },
465 | {
466 | name: "HGETALL key",
467 | description: "Get all fields and values in the hash",
468 | example: "HGETALL user:1",
469 | },
470 | {
471 | name: "HDEL key field [field ...]",
472 | description: "Delete field(s) from the hash",
473 | example: "HDEL user:1 email",
474 | },
475 | {
476 | name: "HEXISTS key field",
477 | description: "Check if a field exists in the hash",
478 | example: "HEXISTS user:1 name",
479 | },
480 | {
481 | name: "HKEYS key",
482 | description: "Get all field names in the hash",
483 | example: "HKEYS user:1",
484 | },
485 | {
486 | name: "HVALS key",
487 | description: "Get all values in the hash",
488 | example: "HVALS user:1",
489 | },
490 | {
491 | name: "HINCRBY key field increment",
492 | description: "Increment the integer value of a field by a number",
493 | example: "HINCRBY user:1 age 1",
494 | },
495 | ],
496 | useCase: `
497 | Imagine you're building an e-commerce platform and need to store product information:
498 |
499 | 1. Each product can be represented as a hash, with the key being "product:{id}".
500 | 2. Use HSET to store various attributes of the product (name, price, stock, etc.).
501 | 3. Use HGET to quickly retrieve specific attributes without loading the entire product data.
502 | 4. Use HGETALL to get all product details when needed.
503 | 5. Use HINCRBY to manage stock levels or track the number of views/purchases.
504 |
505 | This structure allows for efficient storage and retrieval of product data, easy updates to individual fields, and atomic operations on numeric values, which is crucial for maintaining accurate inventory in a high-traffic e-commerce system.
506 | `,
507 | },
508 | challenges: [
509 | {
510 | id: 1,
511 | prompt: "Create a hash 'user:1' with fields 'name' (John Doe) and 'email' (john@example.com)",
512 | answer: 'HSET user:1 name "John Doe" email john@example.com',
513 | type: "command",
514 | },
515 | {
516 | id: 2,
517 | prompt: "Get the 'name' of 'user:1'",
518 | answer: "HGET user:1 name",
519 | type: "command",
520 | },
521 | {
522 | id: 3,
523 | prompt: "Add an 'age' field with value '30' to 'user:1'",
524 | answer: "HSET user:1 age 30",
525 | type: "command",
526 | },
527 | {
528 | id: 4,
529 | prompt: "Retrieve all fields and values of 'user:1'",
530 | answer: "HGETALL user:1",
531 | type: "command",
532 | },
533 | {
534 | id: 5,
535 | prompt: "Check if 'user:1' has a 'phone' field",
536 | answer: "HEXISTS user:1 phone",
537 | type: "command",
538 | },
539 | {
540 | id: 6,
541 | prompt: "Increment the 'age' of 'user:1' by 1",
542 | answer: "HINCRBY user:1 age 1",
543 | type: "command",
544 | },
545 | ],
546 | },
547 | {
548 | slug: "sorted-sets",
549 | title: "Sorted Sets in Redis",
550 | content: {
551 | overview: `
552 | Redis Sorted Sets are sets where each member has an associated score. They're useful for:
553 | 1. Leaderboards and ranking systems
554 | 2. Priority queues
555 | 3. Time-series data
556 | 4. Managing queues with priority
557 | `,
558 | keyCommands: [
559 | {
560 | name: "ZADD key score member [score member ...]",
561 | description: "Add member(s) with scores to a sorted set",
562 | example: "ZADD leaderboard 100 Alice 95 Bob 97 Charlie",
563 | },
564 | {
565 | name: "ZRANGE key start stop [WITHSCORES]",
566 | description: "Retrieve members by index range (low to high score)",
567 | example: "ZRANGE leaderboard 0 -1 WITHSCORES",
568 | },
569 | {
570 | name: "ZREVRANGE key start stop [WITHSCORES]",
571 | description: "Retrieve members by index range (high to low score)",
572 | example: "ZREVRANGE leaderboard 0 -1 WITHSCORES",
573 | },
574 | {
575 | name: "ZRANK key member",
576 | description: "Get the rank of a member (by low to high score)",
577 | example: "ZRANK leaderboard Bob",
578 | },
579 | {
580 | name: "ZREVRANK key member",
581 | description: "Get the rank of a member (by high to low score)",
582 | example: "ZREVRANK leaderboard Bob",
583 | },
584 | {
585 | name: "ZSCORE key member",
586 | description: "Get the score of a member",
587 | example: "ZSCORE leaderboard Alice",
588 | },
589 | {
590 | name: "ZREM key member [member ...]",
591 | description: "Remove member(s) from the sorted set",
592 | example: "ZREM leaderboard Bob",
593 | },
594 | {
595 | name: "ZINCRBY key increment member",
596 | description: "Increment the score of a member",
597 | example: "ZINCRBY leaderboard 5 Alice",
598 | },
599 | ],
600 | useCase: `
601 | Consider a real-time analytics system for a popular website:
602 |
603 | 1. Use a sorted set for each metric you want to track (e.g., page views, unique visitors).
604 | 2. The score can be the timestamp, and the member can be the page URL or user ID.
605 | 3. Use ZADD to add new data points as they come in.
606 | 4. Use ZRANGE with WITHSCORES to get data for a specific time range.
607 | 5. Use ZCOUNT to get the number of events in a time range.
608 | 6. Use ZREMRANGEBYSCORE to remove old data and maintain a rolling window of recent data.
609 |
610 | This structure allows for efficient storage and retrieval of time-series data, easy querying of recent events, and automatic ordering by timestamp, which is crucial for real-time analytics and reporting.
611 | `,
612 | },
613 | challenges: [
614 | {
615 | id: 1,
616 | prompt: "Create a sorted set 'leaderboard' with members: Alice (score: 100), Bob (score: 85), Charlie (score: 95)",
617 | answer: "ZADD leaderboard 100 Alice 85 Bob 95 Charlie",
618 | type: "command",
619 | },
620 | {
621 | id: 2,
622 | prompt: "Get the top 2 players from the 'leaderboard'",
623 | answer: "ZREVRANGE leaderboard 0 1",
624 | type: "command",
625 | },
626 | {
627 | id: 3,
628 | prompt: "Get Bob's rank in the 'leaderboard' (0-based, high to low score)",
629 | answer: "ZREVRANK leaderboard Bob",
630 | type: "command",
631 | },
632 | {
633 | id: 4,
634 | prompt: "Get Charlie's score",
635 | answer: "ZSCORE leaderboard Charlie",
636 | type: "command",
637 | },
638 | {
639 | id: 5,
640 | prompt: "Increase Alice's score by 15 points",
641 | answer: "ZINCRBY leaderboard 15 Alice",
642 | type: "command",
643 | },
644 | {
645 | id: 6,
646 | prompt: "Get all scores and members, ordered from lowest to highest score",
647 | answer: "ZRANGE leaderboard 0 -1 WITHSCORES",
648 | type: "command",
649 | },
650 | ],
651 | },
652 | ];
653 |
--------------------------------------------------------------------------------
/src/utils/problems.ts:
--------------------------------------------------------------------------------
1 | export interface TestCase {
2 | name: string;
3 | run: (functions: any) => Promise;
4 | }
5 |
6 | export interface Problem {
7 | slug: string;
8 | title: string;
9 | description: string;
10 | initialCode: string;
11 | solutionCode: string;
12 | hints: string[];
13 | exampleUsage: string;
14 | difficulty: string;
15 | testCases: TestCase[];
16 | }
17 |
18 | export const problems: Problem[] = [
19 | {
20 | slug: "basic-caching",
21 | title: "Basic Caching",
22 | description:
23 | "Implement a caching system using Upstash Redis with two functions: `setCacheValue(key, value)` to store a key-value pair, and `getCacheValue(key)` to retrieve a value for a given key.",
24 | initialCode: `import { Redis } from '@upstash/redis'
25 |
26 | const client = new Redis({
27 | url: process.env.UPSTASH_REDIS_URL,
28 | token: process.env.UPSTASH_REDIS_TOKEN
29 | })
30 |
31 | /* DO NOT MODIFY THE CODE ABOVE */
32 |
33 | async function setCacheValue(key, value) {
34 | // Implement setCacheValue function
35 | }
36 |
37 | async function getCacheValue(key) {
38 | // Implement getCacheValue function
39 | }`,
40 | solutionCode: `import { Redis } from '@upstash/redis'
41 |
42 | const client = new Redis({
43 | url: process.env.UPSTASH_REDIS_URL,
44 | token: process.env.UPSTASH_REDIS_TOKEN
45 | })
46 |
47 | /* DO NOT MODIFY THE CODE ABOVE */
48 |
49 | async function setCacheValue(key, value) {
50 | return await client.set(key, value);
51 | }
52 |
53 | async function getCacheValue(key) {
54 | return await client.get(key);
55 | }`,
56 | hints: [
57 | "Use the `client.set` method to store a value in Redis.",
58 | "Use the `client.get` method to retrieve a value from Redis.",
59 | "Remember to use `await` with Redis methods as they return Promises.",
60 | "The `setCacheValue` function should return the result of the set operation.",
61 | "The `getCacheValue` function may return `null` if the key does not exist.",
62 | ],
63 | exampleUsage: `// Set cache value
64 | await setCacheValue("user1", "John Doe")
65 | // OK
66 |
67 | // Get cache value
68 | const value = await getCacheValue("user1")
69 | console.log(value) // "John Doe"
70 |
71 | // Get non-existent value
72 | const nullValue = await getCacheValue("user2")
73 | console.log(nullValue) // null`,
74 | difficulty: "easy",
75 | testCases: [
76 | {
77 | name: "Set and get a value",
78 | run: async ({ setCacheValue, getCacheValue }) => {
79 | await setCacheValue("user1", "John Doe");
80 | const result = await getCacheValue("user1");
81 | return result === "John Doe";
82 | },
83 | },
84 | {
85 | name: "Get a non-existent value",
86 | run: async ({ getCacheValue }) => {
87 | const result = await getCacheValue("nonexistent");
88 | return result === null;
89 | },
90 | },
91 | {
92 | name: "Update an existing value",
93 | run: async ({ setCacheValue, getCacheValue }) => {
94 | await setCacheValue("user1", "John Doe");
95 | await setCacheValue("user1", "Jane Doe");
96 | const result = await getCacheValue("user1");
97 | return result === "Jane Doe";
98 | },
99 | },
100 | ],
101 | },
102 | {
103 | slug: "list-operations",
104 | title: "List Operations",
105 | description:
106 | "Implement two functions for managing a task queue: `addTask(task)` to add a task to the end of the queue, and `getNextTask()` to retrieve and remove the first task from the queue.",
107 | initialCode: `import { Redis } from '@upstash/redis'
108 |
109 | const client = new Redis({
110 | url: process.env.UPSTASH_REDIS_URL,
111 | token: process.env.UPSTASH_REDIS_TOKEN
112 | })
113 |
114 | /* DO NOT MODIFY THE CODE ABOVE */
115 |
116 | async function addTask(task) {
117 | // Implement addTask function
118 | }
119 |
120 | async function getNextTask() {
121 | // Implement getNextTask function
122 | }`,
123 | solutionCode: `import { Redis } from '@upstash/redis'
124 |
125 | const client = new Redis({
126 | url: process.env.UPSTASH_REDIS_URL,
127 | token: process.env.UPSTASH_REDIS_TOKEN
128 | })
129 |
130 | /* DO NOT MODIFY THE CODE ABOVE */
131 |
132 | async function addTask(task) {
133 | return await client.rpush('tasks', task);
134 | }
135 |
136 | async function getNextTask() {
137 | return await client.lpop('tasks');
138 | }`,
139 | hints: [
140 | "Use the `client.rpush` method to append a task to the end of the list.",
141 | "Use the `client.lpop` method to remove and return the first task from the list.",
142 | "The list of tasks is stored under the key 'tasks'.",
143 | "Be aware that `lpop` will return `null` if the list is empty.",
144 | ],
145 | exampleUsage: `// Add tasks to the queue
146 | await addTask("Buy groceries")
147 | await addTask("Walk the dog")
148 |
149 | // Get the next task
150 | const nextTask = await getNextTask()
151 | console.log(nextTask) // "Buy groceries"
152 |
153 | // Get the next task again
154 | const nextTask2 = await getNextTask()
155 | console.log(nextTask2) // "Walk the dog"
156 |
157 | // Try to get a task from an empty queue
158 | const emptyQueueTask = await getNextTask()
159 | console.log(emptyQueueTask) // null`,
160 | difficulty: "easy",
161 | testCases: [
162 | {
163 | name: "Add and get a task",
164 | run: async ({ addTask, getNextTask }) => {
165 | await addTask("Test task");
166 | const result = await getNextTask();
167 | return result === "Test task";
168 | },
169 | },
170 | {
171 | name: "Get from empty queue",
172 | run: async ({ getNextTask }) => {
173 | const result = await getNextTask();
174 | return result === null;
175 | },
176 | },
177 | {
178 | name: "Add multiple tasks and get them in order",
179 | run: async ({ addTask, getNextTask }) => {
180 | await addTask("Task 1");
181 | await addTask("Task 2");
182 | const result1 = await getNextTask();
183 | const result2 = await getNextTask();
184 | return result1 === "Task 1" && result2 === "Task 2";
185 | },
186 | },
187 | ],
188 | },
189 | {
190 | slug: "hash-operations",
191 | title: "Hash Operations",
192 | description:
193 | "Implement two functions for managing user profiles: `setUserProfile(userId, field, value)` to set a field in a user's profile, and `getUserProfile(userId)` to retrieve all fields of a user's profile.",
194 | initialCode: `import { Redis } from '@upstash/redis'
195 |
196 | const client = new Redis({
197 | url: process.env.UPSTASH_REDIS_URL,
198 | token: process.env.UPSTASH_REDIS_TOKEN
199 | })
200 |
201 | /* DO NOT MODIFY THE CODE ABOVE */
202 |
203 | async function setUserProfile(userId, field, value) {
204 | // Implement setUserProfile function
205 | }
206 |
207 | async function getUserProfile(userId) {
208 | // Implement getUserProfile function
209 | }`,
210 | solutionCode: `import { Redis } from '@upstash/redis'
211 |
212 | const client = new Redis({
213 | url: process.env.UPSTASH_REDIS_URL,
214 | token: process.env.UPSTASH_REDIS_TOKEN
215 | })
216 |
217 | /* DO NOT MODIFY THE CODE ABOVE */
218 |
219 | async function setUserProfile(userId, field, value) {
220 | return await client.hset(\`user:\${userId}\`, field, value);
221 | }
222 |
223 | async function getUserProfile(userId) {
224 | return await client.hgetall(\`user:\${userId}\`);
225 | }`,
226 | hints: [
227 | "Use the `client.hset` method to set a field in a hash.",
228 | "Use the `client.hgetall` method to retrieve all fields and values of a hash.",
229 | "Follow a consistent key pattern for user profiles, such as 'user:{userId}'.",
230 | "The `getUserProfile` function returns an object with all the fields and values for a user.",
231 | ],
232 | exampleUsage: `// Set user profile fields
233 | await setUserProfile("user1", "name", "John Doe")
234 | await setUserProfile("user1", "email", "john@example.com")
235 |
236 | // Get the complete user profile
237 | const profile = await getUserProfile("user1")
238 | console.log(profile)
239 | // Output: { name: "John Doe", email: "john@example.com" }
240 |
241 | // Try to get a non-existent user profile
242 | const emptyProfile = await getUserProfile("user2")
243 | console.log(emptyProfile) // null`,
244 | difficulty: "easy",
245 | testCases: [
246 | {
247 | name: "Set and get a user profile",
248 | run: async ({ setUserProfile, getUserProfile }) => {
249 | await setUserProfile("user1", "name", "John Doe");
250 | await setUserProfile("user1", "email", "john@example.com");
251 | const profile = await getUserProfile("user1");
252 | return profile.name === "John Doe" && profile.email === "john@example.com";
253 | },
254 | },
255 | {
256 | name: "Get a non-existent user profile",
257 | run: async ({ getUserProfile }) => {
258 | const profile = await getUserProfile("nonexistent");
259 | return profile === null;
260 | },
261 | },
262 | {
263 | name: "Update an existing profile field",
264 | run: async ({ setUserProfile, getUserProfile }) => {
265 | await setUserProfile("user1", "name", "John Doe");
266 | await setUserProfile("user1", "name", "Jane Doe");
267 | const profile = await getUserProfile("user1");
268 | return profile.name === "Jane Doe";
269 | },
270 | },
271 | ],
272 | },
273 |
274 | {
275 | slug: "expiring-cache",
276 | title: "Expiring Cache",
277 | description:
278 | "Implement an expiring cache using Upstash Redis. Create two functions: `setWithExpiry(key, value, expiryInSeconds)` to set a value with an expiration time, and `getWithExpiry(key)` to retrieve a value if it hasn't expired.",
279 | initialCode: `import { Redis } from '@upstash/redis'
280 |
281 | const client = new Redis({
282 | url: process.env.UPSTASH_REDIS_URL,
283 | token: process.env.UPSTASH_REDIS_TOKEN
284 | })
285 |
286 | /* DO NOT MODIFY THE CODE ABOVE */
287 |
288 | async function setWithExpiry(key, value, expiryInSeconds) {
289 | // Implement setWithExpiry function
290 | }
291 |
292 | async function getWithExpiry(key) {
293 | // Implement getWithExpiry function
294 | }
295 |
296 | // Test your implementation
297 | async function main() {
298 | await setWithExpiry('user:1', JSON.stringify({ name: 'John' }), 5)
299 | console.log(await getWithExpiry('user:1'))
300 | await new Promise(resolve => setTimeout(resolve, 6000))
301 | console.log(await getWithExpiry('user:1'))
302 | }
303 |
304 | main().catch(console.error)`,
305 | solutionCode: `import { Redis } from '@upstash/redis'
306 |
307 | const client = new Redis({
308 | url: process.env.UPSTASH_REDIS_URL,
309 | token: process.env.UPSTASH_REDIS_TOKEN
310 | })
311 |
312 | /* DO NOT MODIFY THE CODE ABOVE */
313 |
314 | async function setWithExpiry(key, value, expiryInSeconds) {
315 | return await client.set(key, value, { ex: expiryInSeconds })
316 | }
317 |
318 | async function getWithExpiry(key) {
319 | const value = await client.get(key)
320 | return value ? JSON.parse(value) : null
321 | }
322 |
323 | async function main() {
324 | await setWithExpiry('user:1', JSON.stringify({ name: 'John' }), 5)
325 | console.log(await getWithExpiry('user:1'))
326 | await new Promise(resolve => setTimeout(resolve, 6000))
327 | console.log(await getWithExpiry('user:1'))
328 | }
329 |
330 | main().catch(console.error)`,
331 | hints: [
332 | "Use the 'ex' option with client.set to set expiration time.",
333 | "Remember to stringify objects before storing and parse them after retrieval.",
334 | "The getWithExpiry function should return null for expired or non-existent keys.",
335 | "No need to manually check expiration in getWithExpiry, Redis handles it automatically.",
336 | ],
337 | exampleUsage: `// Set a value with 10 seconds expiry
338 | await setWithExpiry('session:123', JSON.stringify({ userId: 'user1' }), 10)
339 |
340 | // Get the value immediately
341 | console.log(await getWithExpiry('session:123')) // { userId: 'user1' }
342 |
343 | // Wait for 11 seconds
344 | await new Promise(resolve => setTimeout(resolve, 11000))
345 |
346 | // Try to get the expired value
347 | console.log(await getWithExpiry('session:123')) // null`,
348 | difficulty: "medium",
349 | testCases: [
350 | {
351 | name: "Set and get a value before expiration",
352 | run: async ({ setWithExpiry, getWithExpiry }) => {
353 | await setWithExpiry("test:1", JSON.stringify({ data: "test" }), 2);
354 | const result = await getWithExpiry("test:1");
355 | return result && result.data === "test";
356 | },
357 | },
358 | {
359 | name: "Get an expired value",
360 | run: async ({ setWithExpiry, getWithExpiry }) => {
361 | await setWithExpiry("test:2", JSON.stringify({ data: "test" }), 1);
362 | await new Promise((resolve) => setTimeout(resolve, 1100));
363 | const result = await getWithExpiry("test:2");
364 | return result === null;
365 | },
366 | },
367 | {
368 | name: "Get a non-existent key",
369 | run: async ({ getWithExpiry }) => {
370 | const result = await getWithExpiry("nonexistent");
371 | return result === null;
372 | },
373 | },
374 | ],
375 | },
376 | {
377 | slug: "counter-with-reset",
378 | title: "Counter with Reset",
379 | description:
380 | "Implement a counter system with a reset functionality using Upstash Redis. Create three functions: `incrementCounter(key)` to increment a counter, `getCounter(key)` to get the current count, and `resetCounter(key)` to reset the counter to zero.",
381 | initialCode: `import { Redis } from '@upstash/redis'
382 |
383 | const client = new Redis({
384 | url: process.env.UPSTASH_REDIS_URL,
385 | token: process.env.UPSTASH_REDIS_TOKEN,
386 | })
387 |
388 | /* DO NOT MODIFY THE CODE ABOVE */
389 |
390 | async function incrementCounter(key) {
391 | // Implement incrementCounter function
392 | }
393 |
394 | async function getCounter(key) {
395 | // Implement getCounter function
396 | }
397 |
398 | async function resetCounter(key) {
399 | // Implement resetCounter function
400 | }`,
401 | solutionCode: `import { Redis } from '@upstash/redis'
402 |
403 | const client = new Redis({
404 | url: process.env.UPSTASH_REDIS_URL,
405 | token: process.env.UPSTASH_REDIS_TOKEN,
406 | })
407 |
408 | /* DO NOT MODIFY THE CODE ABOVE */
409 |
410 | async function incrementCounter(key) {
411 | const currentValue = await client.get(key);
412 | const newValue = (parseInt(currentValue) || 0) + 1;
413 | await client.set(key, newValue.toString());
414 | return newValue;
415 | }
416 |
417 | async function getCounter(key) {
418 | const value = await client.get(key);
419 | return value ? parseInt(value) : 0;
420 | }
421 |
422 | async function resetCounter(key) {
423 | await client.set(key, '0');
424 | return 0;
425 | }`,
426 | hints: [
427 | "Use client.get and client.set to implement increment functionality.",
428 | "Remember to parse the string returned by client.get to an integer.",
429 | "Use client.set with a value of 0 to reset the counter.",
430 | "Handle the case where the counter doesn't exist in getCounter and incrementCounter.",
431 | ],
432 | exampleUsage: `// Increment the 'pageviews' counter
433 | await incrementCounter('pageviews')
434 | await incrementCounter('pageviews')
435 | await incrementCounter('pageviews')
436 |
437 | // Get the current count
438 | const views = await getCounter('pageviews')
439 | console.log('Page views:', views) // 3
440 |
441 | // Reset the counter
442 | await resetCounter('pageviews')
443 | console.log('After reset:', await getCounter('pageviews')) // 0`,
444 | difficulty: "medium",
445 | testCases: [
446 | {
447 | name: "Increment and get counter",
448 | run: async ({ incrementCounter, getCounter }) => {
449 | await incrementCounter("test1");
450 | const value = await getCounter("test1");
451 | return value === 1;
452 | },
453 | },
454 | {
455 | name: "Multiple increments",
456 | run: async ({ incrementCounter, getCounter }) => {
457 | await incrementCounter("test2");
458 | await incrementCounter("test2");
459 | await incrementCounter("test2");
460 | const value = await getCounter("test2");
461 | return value === 3;
462 | },
463 | },
464 | {
465 | name: "Reset counter",
466 | run: async ({ incrementCounter, resetCounter, getCounter }) => {
467 | await incrementCounter("test3");
468 | await incrementCounter("test3");
469 | await resetCounter("test3");
470 | const value = await getCounter("test3");
471 | return value === 0;
472 | },
473 | },
474 | {
475 | name: "Get non-existent counter",
476 | run: async ({ getCounter }) => {
477 | const value = await getCounter("nonexistent");
478 | return value === 0;
479 | },
480 | },
481 | {
482 | name: "Increment after reset",
483 | run: async ({ incrementCounter, resetCounter, getCounter }) => {
484 | await incrementCounter("test4");
485 | await incrementCounter("test4");
486 | await resetCounter("test4");
487 | await incrementCounter("test4");
488 | const value = await getCounter("test4");
489 | return value === 1;
490 | },
491 | },
492 | ],
493 | },
494 | ];
495 |
496 | export function getProblemBySlug(slug: string): Problem | undefined {
497 | return problems.find((problem) => problem.slug === slug);
498 | }
499 |
500 | export function getNextProblemSlug(currentSlug: string): string | null {
501 | const currentIndex = problems.findIndex((problem) => problem.slug === currentSlug);
502 | if (currentIndex < problems.length - 1) {
503 | return problems[currentIndex + 1].slug;
504 | }
505 | return null;
506 | }
507 |
508 | export function getPreviousProblemSlug(currentSlug: string): string | null {
509 | const currentIndex = problems.findIndex((problem) => problem.slug === currentSlug);
510 | if (currentIndex > 0) {
511 | return problems[currentIndex - 1].slug;
512 | }
513 | return null;
514 | }
515 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" },
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" },
69 | },
70 | },
71 | animation: {
72 | "accordion-down": "accordion-down 0.2s ease-out",
73 | "accordion-up": "accordion-up 0.2s ease-out",
74 | },
75 | },
76 | },
77 | plugins: [require("tailwindcss-animate")],
78 | } satisfies Config
79 |
80 | export default config
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------