Skip to content

Commit fdf4aff

Browse files
committed
Adding the completion status and making sure we can complete a game and an article
1 parent 0355e53 commit fdf4aff

16 files changed

+481
-64
lines changed

app/blogs/[slug]/page.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
1010
import { siteConfig } from "@/config/site";
1111
import { parse } from "node-html-parser";
1212
import { CopyToClip } from "@/components/copy-to-clip-component";
13+
import { ProgressArticle } from "@/components/progress-article";
1314

1415
interface CustomTag {
1516
type: "code" | "normal";
@@ -32,12 +33,13 @@ export default async function BlogComponent({
3233

3334
const content = await markdownToHtml(article.href);
3435
return (
35-
<div className="px-1 max-w-full items-center md:px-8 flex-col flex">
36+
<div className="px-1 max-w-full items-center relative md:px-8 flex-col flex">
3637
<Head>
3738
<title>{`${siteConfig.name} - ${article.title}`}</title>
3839
<meta name="description">{article.synopsis}</meta>
3940
</Head>
40-
<div className=" max-w-[1400px]">
41+
<ProgressArticle href={article.href} className="fixed top-0 " />
42+
<div className="relative max-w-[1400px]">
4143
{stringToTags(content).map((c, index) =>
4244
c.type == "normal" ? (
4345
<div

components/about-home.tsx

+14-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import * as React from "react";
22
import Image from "next/image";
3+
import ClientSideContent from "@/components/game-score";
4+
import { games } from "@/config/games";
5+
import { blogs } from "@/config/blogs";
36

47
export function AboutHome() {
58
return (
6-
<div className="text-lg py-12 flex px-2">
9+
<div className="text-lg py-12 flex flex-col px-2">
10+
<div>
711
<Image
812
className="rounded-lg bottom-0 right-0 hidden md:block"
913
src={"/low-poly-bg.png"}
@@ -13,11 +17,10 @@ export function AboutHome() {
1317
/>
1418
<div>
1519
<p className="leading-7 max-w-[1000px]">
16-
Explore 5-minute reads on our blog—your
17-
friendly guide in the Python world.
18-
<br/>
19-
Simplify your learning and focus
20-
on what matters most.
20+
Explore 5-minute reads on our blog—your friendly guide in the Python
21+
world.
22+
<br />
23+
Simplify your learning and focus on what matters most.
2124
</p>
2225

2326
<p className="leading-7 mt-3">
@@ -28,6 +31,11 @@ export function AboutHome() {
2831
, but feel free to explore other sections first if you prefer.
2932
</p>
3033
</div>
34+
</div>
35+
<ClientSideContent
36+
totalGames={games.length}
37+
totalArticles={blogs.length}
38+
/>
3139
</div>
3240
);
3341
}

components/blog-component.tsx

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22
import React, { useState, useMemo } from "react";
33
import { UnifiedCard } from "@/components/card-component";
4+
import { useProgressStore } from '@/stores/progress-store';
45
import {
56
UnifiedContent,
67
isGame,
@@ -17,7 +18,9 @@ export function BlogComponent({
1718
}) {
1819
const [searchTerm, setSearchTerm] = useState("");
1920
const [contentType, setContentType] = useState("all");
21+
const [completionStatus, setCompletionStatus] = useState("all");
2022
const [sortBy, setSortBy] = useState("title");
23+
const isContentCompleted = useProgressStore.getState().isContentCompleted;
2124

2225
const filteredAndSortedContents = useMemo(() => {
2326
return contents
@@ -29,7 +32,11 @@ export function BlogComponent({
2932
contentType === "all" ||
3033
(contentType === "game" && isGame(c)) ||
3134
(contentType === "article" && isArticle(c));
32-
return matchesSearch && matchesType;
35+
const matchesCompletion =
36+
completionStatus === "all" ||
37+
(completionStatus === "finished" && isContentCompleted(c.content.href)) ||
38+
(completionStatus === "unfinished" && !isContentCompleted(c.content.href));
39+
return matchesSearch && matchesType && matchesCompletion;
3340
})
3441
.sort((a, b) => {
3542
if (sortBy === "title") {
@@ -41,7 +48,7 @@ export function BlogComponent({
4148
}
4249
return 0;
4350
});
44-
}, [contents, searchTerm, contentType, sortBy]);
51+
}, [contents, searchTerm, contentType, sortBy, completionStatus, isContentCompleted]);
4552

4653
return (
4754
<div className="space-y-4 w-full">
@@ -63,6 +70,15 @@ export function BlogComponent({
6370
<option value="game">Games</option>
6471
<option value="article">Articles</option>
6572
</select>
73+
<select
74+
value={completionStatus}
75+
onChange={(e) => setCompletionStatus(e.target.value)}
76+
className="w-full sm:w-auto p-2 border rounded"
77+
>
78+
<option value="all">Everything</option>
79+
<option value="finished">Finished</option>
80+
<option value="unfinished">Not Yet Done</option>
81+
</select>
6682
<select
6783
value={sortBy}
6884
onChange={(e) => setSortBy(e.target.value)}

components/card-component.tsx

+23-7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as React from "react";
44
import { UnifiedContent, isGame, isArticle } from "@/types/unifiedContent";
55
import { TagComponent } from "@/components/tag-component";
66
import ControllerIcon from "@/components/ui/controller-icon";
7+
import { CheckCircledIcon } from "@radix-ui/react-icons";
78
import {
89
Card,
910
CardContent,
@@ -12,27 +13,36 @@ import {
1213
CardTitle,
1314
} from "@/components/ui/card";
1415
import { GameLevel } from "./game-level-component";
16+
import { useProgressStore } from "@/stores/progress-store";
17+
import { Check } from "lucide-react";
1518

1619
export function UnifiedCard(props: { content: UnifiedContent }) {
20+
const [done, setDone] = React.useState(false);
21+
const { isContentCompleted } = useProgressStore();
1722
const { content } = props;
1823
const item = content.content;
24+
React.useEffect(() => setDone(isContentCompleted(item.href)), [item]);
1925

2026
if (!item) return null;
2127

22-
const isStarred = 'starred' in item ? item.starred : false;
23-
const tags = 'tags' in item ? item.tags : [];
24-
const title = 'title' in item ? item.title : '';
25-
const synopsis = 'synopsis' in item ? item.synopsis : '';
26-
const href = 'href' in item ? item.href : '';
28+
const isStarred = "starred" in item ? item.starred : false;
29+
const tags = "tags" in item ? item.tags : [];
30+
const title = "title" in item ? item.title : "";
31+
const synopsis = "synopsis" in item ? item.synopsis : "";
32+
const href = "href" in item ? item.href : "";
2733

2834
return (
2935
<Card
30-
className={`${isStarred ? "shadow-lg shadow-green-500/50" : ""}
36+
className={`
37+
${isStarred ? "shadow-lg shadow-green-500/50" : ""}
3138
bg-muted
3239
flex flex-col max-w-full w-auto
3340
justify-between cursor-pointer relative `}
3441
>
35-
<a href={`/${isGame(content) ? 'games' : 'blogs'}/${href}`} className="hover:translate-y-1">
42+
<a
43+
href={`/${isGame(content) ? "games" : "blogs"}/${href}`}
44+
className="hover:translate-y-1"
45+
>
3646
<CardHeader>
3747
<CardTitle className="truncate pb-1 flex max-w-full">
3848
{isGame(content) && <ControllerIcon />} {title}
@@ -55,6 +65,12 @@ export function UnifiedCard(props: { content: UnifiedContent }) {
5565
{isGame(content) ? "Let's Play!" : "Read More"} &rarr;
5666
</CardFooter>
5767
</a>
68+
{done && (
69+
<div className="absolute flex items-center text-green-500 left-2 bottom-2">
70+
<Check className=" h-4 w-4" />
71+
<span className="ml-2 font-bold">Done </span>
72+
</div>
73+
)}
5874
</Card>
5975
);
6076
}

components/code-component.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,19 @@ import { reviver } from "@/utils/codeComponentUtils";
2222
import { useReward } from "react-rewards";
2323
import { Button } from "@/components/ui/button";
2424
import { Game } from "@/types/game";
25+
import { useProgressStore } from '@/stores/progress-store';
2526

2627
export function CodeComponent(props: { game: Game }) {
2728
const [foundError, setFoundError] = useState(false);
2829
const [score, setScore] = useState(0);
2930
const [tried, setTried] = useState<CodeLine[]>([]);
3031
const [discovered, setDiscovered] = useState<CodeLine[]>([]);
32+
const {
33+
completeGame,
34+
setCurrentContent,
35+
isContentCompleted,
36+
} = useProgressStore();
37+
3138
const [userSubHint, setUserSubHint] = useState(
3239
<>
3340
<Crosshair1Icon /> <span className="pl-1">Find the error</span>{" "}
@@ -84,6 +91,7 @@ export function CodeComponent(props: { game: Game }) {
8491
}
8592
if (cl.state == StateEnum.CORRECT) {
8693
setScore(score + cl.score);
94+
completeGame(props.game.href);
8795
// Next game in time or last created.
8896
setTimeout(
8997
() =>

components/game-score.tsx

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"use client";
2+
import React, { useEffect, useState } from "react";
3+
import { useProgressStore } from "@/stores/progress-store";
4+
import ControllerIcon from "@/components/ui/controller-icon";
5+
import { Progress } from "@/components/ui/progress";
6+
import { Trophy, BookOpen } from "lucide-react";
7+
8+
const UserProgressWidget = ({
9+
totalGames,
10+
totalArticles,
11+
}: {
12+
totalGames: number;
13+
totalArticles: number;
14+
}) => {
15+
const [completedCounts, setCompletedCounts] = useState({
16+
games: 0,
17+
articles: 0,
18+
});
19+
const [animatedGameProgress, setAnimatedGameProgress] = useState(0);
20+
const [animatedArticleProgress, setAnimatedArticleProgress] = useState(0);
21+
22+
useEffect(() => {
23+
const getCompletedCounts = useProgressStore.getState().getCompletedCounts;
24+
setCompletedCounts(getCompletedCounts());
25+
26+
const unsubscribe = useProgressStore.subscribe(() =>
27+
setCompletedCounts(getCompletedCounts())
28+
);
29+
30+
return () => unsubscribe();
31+
}, []);
32+
33+
useEffect(() => {
34+
const gameProgress = (completedCounts.games / totalGames) * 100;
35+
const articleProgress = (completedCounts.articles / totalArticles) * 100;
36+
37+
const animateProgress = (current: number, target: number, setter: any) => {
38+
if (current < target) {
39+
const next = Math.min(current + 1, target);
40+
setter(next);
41+
setTimeout(() => animateProgress(next, target, setter), 20);
42+
}
43+
};
44+
45+
animateProgress(
46+
animatedGameProgress,
47+
gameProgress,
48+
setAnimatedGameProgress
49+
);
50+
animateProgress(
51+
animatedArticleProgress,
52+
articleProgress,
53+
setAnimatedArticleProgress
54+
);
55+
}, [completedCounts, totalGames, totalArticles, animatedArticleProgress, animatedGameProgress]);
56+
57+
return (
58+
<div className="bg-muted p-4 rounded-xl shadow-md mt-3 md:my-3">
59+
<div className="text-xl font-bold opacity-35 align-middle justify-end flex w-full">
60+
{" "}
61+
Your Progress
62+
<Trophy className="ml-2 text-yellow-500" />
63+
</div>
64+
<div className="space-y-2 grid grid-cols-1 md:grid-cols-2 xl:gap-9">
65+
<div className="self-end">
66+
<div className="flex justify-between items-center mb-2">
67+
<span className="flex items-center">
68+
<ControllerIcon />
69+
Games Completed
70+
</span>
71+
<span className=" ml-2 font-semibold">
72+
{completedCounts.games} / {totalGames}
73+
</span>
74+
</div>
75+
<Progress value={animatedGameProgress} className="h-2" />
76+
</div>
77+
<div className="self-end">
78+
<div className="flex justify-between items-center mb-2">
79+
<span className="flex items-center">
80+
<BookOpen className="mr-2 text-blue-500" />
81+
Articles Read
82+
</span>
83+
<span className="font-semibold">
84+
{completedCounts.articles} / {totalArticles}
85+
</span>
86+
</div>
87+
<Progress value={animatedArticleProgress} className="h-2" />
88+
</div>
89+
</div>
90+
</div>
91+
);
92+
};
93+
94+
export default UserProgressWidget;

components/progress-article.tsx

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"use client";
2+
import React, { useState, useEffect } from "react";
3+
import { useProgressStore } from "@/stores/progress-store";
4+
import { useReward } from "react-rewards";
5+
6+
export function ProgressArticle({ href, className }: { href: string, className?: string }) {
7+
const isContentCompleted = useProgressStore.getState().isContentCompleted;
8+
const completedArticles = useProgressStore.getState().completedArticles;
9+
const completeArticle = useProgressStore.getState().completeArticle;
10+
const [progress, setProgress] = useState(0);
11+
const { reward } = useReward("rewardId", "confetti", {
12+
elementCount: 150,
13+
angle: 210,
14+
});
15+
16+
useEffect(() => {
17+
const handleScroll = () => {
18+
const windowHeight = window.innerHeight;
19+
const documentHeight = document.documentElement.scrollHeight;
20+
const scrollTop = window.scrollY;
21+
const scrollableHeight = documentHeight - windowHeight;
22+
23+
if (progress > 97 && !isContentCompleted(href)) {
24+
if (!isContentCompleted(href)) {
25+
completeArticle(href);
26+
}
27+
reward();
28+
}
29+
30+
if (scrollableHeight > 0) {
31+
const newProgress = (scrollTop / scrollableHeight) * 100;
32+
setProgress(Math.max(progress, Math.min(newProgress, 100)));
33+
} else {
34+
setProgress(100);
35+
reward();
36+
window.removeEventListener("scroll", handleScroll);
37+
}
38+
};
39+
40+
window.addEventListener("scroll", handleScroll);
41+
handleScroll(); // Call once to set initial progress
42+
43+
return () => window.removeEventListener("scroll", handleScroll);
44+
}, [progress, href, completeArticle, isContentCompleted, reward]);
45+
46+
return (
47+
<div id="rewardId" className={`w-full xl:block h-1 bg-gray-200 z-50 ${className}`}>
48+
<div
49+
className=" bg-green-700 h-1 transition-all duration-300 ease-out"
50+
style={{ width: `${progress}%` }}
51+
></div>
52+
</div>
53+
);
54+
}

components/sidebar-component.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { Section } from "@/types/section";
33
import { Article } from "@/types/article";
44
import { ContentTable } from "@/components/content-table";
55
import { TagsGroup } from "@/components/tags-group-component";
6+
import { ProgressArticle } from "@/components/progress-article";
67

78
export function Sidebar(props: { sections: Section[]; article: Article }) {
89
return (
910
<div className="w-3/12 hidden xl:flex justify-center">
10-
<div className="fixed min-w-[300px]">
11+
<div className="fixed min-w-[300px] pr-1">
1112
<TagsGroup article={props.article} />
13+
<ProgressArticle href={props.article.href} />
1214
<ContentTable sections={props.sections} />
1315
</div>
1416
</div>

components/tag-component.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Badge } from "@/components/ui/badge";
55
export function TagComponent(props: { tag: Tag }) {
66
if (!props || !props.tag || !props.tag.text) return;
77
return (
8-
<Badge className="cursor-pointer mr-1" variant="secondary">
8+
<Badge className="cursor-pointer mr-1 bg-background" variant="secondary">
99
{props.tag.text}
1010
</Badge>
1111
);

components/tags-group-component.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { TagComponent } from "@/components/tag-component";
66
export function TagsGroup(props: { article: Article | undefined }) {
77
if (props.article == undefined) return;
88
return (
9-
<div className="w-full py-3 border border-transparent border-b-4 border-b-green-700">
9+
<div className="w-full py-3">
1010
{props.article.tags.map((tag: Tag) => (
1111
<TagComponent tag={tag} key={tag.href} />
1212
))}

0 commit comments

Comments
 (0)