Skip to content

Commit b560328

Browse files
committed
Adding a few games and puttig the filters in, also unifiying the Article and Game interfaces to make it easier to display the cards
1 parent 361770d commit b560328

18 files changed

+1459
-129
lines changed

app/blogs/page.tsx

+15-10
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
import * as React from "react";
22

33
import { CH1 } from "@/components/custom-typo";
4-
import { blogs, } from "@/config/blogs"
4+
import { blogs } from "@/config/blogs";
55
import { BlogComponent } from "@/components/blog-component";
6-
import { Article } from "@/types/article";
7-
import { sortBlogsByDate} from "@/utils/blogsUtils"
6+
import { sortBlogsByDate } from "@/utils/blogsUtils";
7+
import { UnifiedContent } from "@/types/unifiedContent";
88

99
export default function HomeBlog() {
10-
let filteredBlogs: Article[] = sortBlogsByDate(blogs);
10+
const filteredBlogs: UnifiedContent[] = [
11+
...sortBlogsByDate(blogs).map((article) => ({
12+
type: "article" as const,
13+
content: article,
14+
})),
15+
];
1116

12-
return (
13-
<div>
14-
<CH1 text="Latest articles" />
15-
<BlogComponent blogs={filteredBlogs}/>
16-
</div>
17-
);
17+
return (
18+
<div>
19+
<CH1 text="Latest articles" />
20+
<BlogComponent contents={filteredBlogs} />
21+
</div>
22+
);
1823
}

app/games/page.tsx

+13-9
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import * as React from "react";
22

33
import { CH1, SubTitle } from "@/components/custom-typo";
4-
import { games } from "@/config/games"
5-
import { GameComponent } from "@/components/game-component";
4+
import { games } from "@/config/games";
5+
import { UnifiedContent } from "@/types/unifiedContent";
6+
import { BlogComponent } from "@/components/blog-component";
67

78
export default function HomeBlog() {
8-
return (
9-
<div>
10-
<CH1 text="Python Games" />
11-
<SubTitle text="Learn Python by playing games" />
12-
<GameComponent games={games} />
13-
</div>
14-
);
9+
const unifiedContent: UnifiedContent[] = [
10+
...games.map(game => ({ type: 'game' as const, content: game })),
11+
];
12+
return (
13+
<div>
14+
<CH1 text="Python Games" />
15+
<SubTitle text="Learn Python by playing games" />
16+
<BlogComponent contents={unifiedContent} showFilters={true} />
17+
</div>
18+
);
1519
}

app/page.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,18 @@ import { sortBlogsByDate } from "@/utils/blogsUtils";
66
import { CH1, SubTitle } from "@/components/custom-typo";
77
import ControllerIcon from "@/components/ui/controller-icon";
88
import { Button } from "@/components/ui/button";
9+
import { UnifiedContent } from "@/types/unifiedContent";
10+
import { games } from "@/config/games";
911

1012
export default function Home() {
1113
let filteredBlogs: Article[] = sortBlogsByDate(blogs);
1214
filteredBlogs.slice(0, 4);
15+
16+
const unifiedContent: UnifiedContent[] = [
17+
...games.map(game => ({ type: 'game' as const, content: game })),
18+
...blogs.map(article => ({ type: 'article' as const, content: article }))
19+
];
20+
1321
return (
1422
<div className="md:px-2 lg:px-4">
1523
<a
@@ -35,7 +43,7 @@ export default function Home() {
3543
<CH1 text="Welcome to LearnPython.Today!" />
3644
<SubTitle text="🐍 Dive into Python with ease!" />
3745
<AboutHome />
38-
<BlogComponent blogs={filteredBlogs} />
46+
<BlogComponent contents={unifiedContent} showFilters={true} />
3947
</div>
4048
);
4149
}

components/blog-component.tsx

+79-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,84 @@
1-
import { BlogCard } from "@/components/card-component";
2-
import { Article } from "@/types/article";
1+
"use client";
2+
import React, { useState, useMemo } from "react";
3+
import { UnifiedCard } from "@/components/card-component";
4+
import {
5+
UnifiedContent,
6+
isGame,
7+
isArticle,
8+
sortLevel,
9+
} from "@/types/unifiedContent";
10+
11+
export function BlogComponent({
12+
contents,
13+
showFilters = false,
14+
}: {
15+
contents: UnifiedContent[];
16+
showFilters?: boolean;
17+
}) {
18+
const [searchTerm, setSearchTerm] = useState("");
19+
const [contentType, setContentType] = useState("all");
20+
const [sortBy, setSortBy] = useState("title");
21+
22+
const filteredAndSortedContents = useMemo(() => {
23+
return contents
24+
.filter((c) => {
25+
const matchesSearch =
26+
c.content.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
27+
c.content.synopsis.toLowerCase().includes(searchTerm.toLowerCase());
28+
const matchesType =
29+
contentType === "all" ||
30+
(contentType === "game" && isGame(c)) ||
31+
(contentType === "article" && isArticle(c));
32+
return matchesSearch && matchesType;
33+
})
34+
.sort((a, b) => {
35+
if (sortBy === "title") {
36+
return a.content.title.localeCompare(b.content.title);
37+
} else if (sortBy === "level") {
38+
return sortLevel(a, b);
39+
} else if (sortBy === "level_desc") {
40+
return -1 * sortLevel(a, b);
41+
}
42+
return 0;
43+
});
44+
}, [contents, searchTerm, contentType, sortBy]);
345

4-
export function BlogComponent(props: { blogs: Article[] }) {
546
return (
6-
<div className="flex flex-wrap flex-row">
7-
{props.blogs.map((article: Article) => (
8-
<BlogCard key={article.title} article={article} />
9-
))}
47+
<div className="space-y-4">
48+
{showFilters && (
49+
<div className="flex flex-wrap gap-4 items-center">
50+
<input
51+
type="text"
52+
placeholder="Search..."
53+
value={searchTerm}
54+
onChange={(e) => setSearchTerm(e.target.value)}
55+
className="w-full sm:w-auto p-2 border rounded"
56+
/>
57+
<select
58+
value={contentType}
59+
onChange={(e) => setContentType(e.target.value)}
60+
className="w-full sm:w-auto p-2 border rounded"
61+
>
62+
<option value="all">All Types</option>
63+
<option value="game">Games</option>
64+
<option value="article">Articles</option>
65+
</select>
66+
<select
67+
value={sortBy}
68+
onChange={(e) => setSortBy(e.target.value)}
69+
className="w-full sm:w-auto p-2 border rounded"
70+
>
71+
<option value="title">Sort by Title</option>
72+
<option value="level">Sort by Level</option>
73+
<option value="level_desc">Sort by Level (DESC)</option>
74+
</select>
75+
</div>
76+
)}
77+
<div className="grid gap-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
78+
{filteredAndSortedContents.map((c: UnifiedContent) => (
79+
<UnifiedCard key={c.content.title} content={c} />
80+
))}
81+
</div>
1082
</div>
1183
);
1284
}

components/card-component.tsx

+43-25
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,59 @@
1+
"use client";
2+
13
import * as React from "react";
2-
import { Article } from "@/types/article";
4+
import { UnifiedContent, isGame, isArticle } from "@/types/unifiedContent";
35
import { TagComponent } from "@/components/tag-component";
4-
6+
import ControllerIcon from "@/components/ui/controller-icon";
57
import {
68
Card,
79
CardContent,
810
CardFooter,
911
CardHeader,
1012
CardTitle,
1113
} from "@/components/ui/card";
14+
import { GameLevel } from "./game-level-component";
15+
16+
export function UnifiedCard(props: { content: UnifiedContent }) {
17+
const { content } = props;
18+
const item = content.content;
19+
20+
if (!item) return null;
21+
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 : '';
1227

13-
export function BlogCard(props: { article: Article }) {
14-
const article = props.article;
15-
if (!article) return;
1628
return (
1729
<Card
18-
className={`
19-
${article.starred ? "shadow-lg shadow-green-500/50" : ""}
20-
w-full lg:w-[calc(50%-1rem)] xl:w-[calc(33.333%-1rem)]
21-
m-1 flex flex-col justify-between bg-background
22-
transition-all duration-300 hover:shadow-md
23-
`}
30+
className={`${isStarred ? "shadow-lg shadow-green-500/50" : ""}
31+
flex flex-col
32+
justify-between bg-background cursor-pointer relative `}
2433
>
25-
<CardHeader>
26-
<CardTitle className="truncate pb-1">{article.title}</CardTitle>
27-
<div className="flex">
28-
{article.tags.map((tag) => (
29-
<TagComponent key={tag.text} tag={tag} />
30-
))}
31-
</div>
32-
</CardHeader>
33-
<CardContent>{article.synopsis}</CardContent>
34-
<CardFooter className="flex justify-end">
35-
<a className="cursor-pointer" href={`/blogs/${article.href}`}>
36-
Go to the article &rarr;
37-
</a>
38-
</CardFooter>
34+
<a href={`/${isGame(content) ? 'games' : 'blogs'}/${href}`} className="hover:translate-y-1">
35+
<CardHeader>
36+
<CardTitle className="truncate pb-1 flex max-w-full">
37+
{isGame(content) && <ControllerIcon />} {title}
38+
</CardTitle>
39+
<div className="flex">
40+
<div>
41+
{tags.map((tag) => (
42+
<TagComponent key={tag.text} tag={tag} />
43+
))}
44+
</div>
45+
{isGame(content) && (
46+
<div className="absolute top-1 right-2">
47+
<GameLevel small={true} level={content.content.level} />
48+
</div>
49+
)}
50+
</div>
51+
</CardHeader>
52+
<CardContent>{synopsis}</CardContent>
53+
<CardFooter className="flex cursor-pointer justify-end">
54+
{isGame(content) ? "Let's Play!" : "Read More"} &rarr;
55+
</CardFooter>
56+
</a>
3957
</Card>
4058
);
4159
}

components/game-card-component.tsx

-50
This file was deleted.

components/game-component.tsx

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import * as React from "react";
2-
import { GameCard } from "@/components/game-card-component";
32
import { Game } from "@/types/game";
3+
import { UnifiedContent } from "@/types/unifiedContent";
4+
import { BlogComponent } from "@/components/blog-component";
45

56
export function GameComponent(props: { games: Game[] }) {
7+
const unifiedContent: UnifiedContent[] = [
8+
...props.games.map((game) => ({ type: "game" as const, content: game })),
9+
];
610
return (
711
<div className="flex flex-wrap flex-row">
8-
{props.games.map((game: Game) => (
9-
<GameCard key={game.title} game={game} />
10-
))}
12+
<BlogComponent contents={unifiedContent} />
1113
</div>
1214
);
1315
}

components/tag-component.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Tag } from "@/types/tag";
33
import { Badge } from "@/components/ui/badge";
44

55
export function TagComponent(props: { tag: Tag }) {
6+
if (!props || !props.tag || !props.tag.text) return;
67
return (
78
<Badge className="cursor-pointer mr-1" variant="secondary">
89
{props.tag.text}

0 commit comments

Comments
 (0)