Skip to content

Commit 13322b1

Browse files
committed
Themes almost work (with a few bugs); New system themes; Cleaned up code;
1 parent 5c43a0f commit 13322b1

178 files changed

Lines changed: 2843 additions & 1414 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ jobs:
1616
- name: Build packages
1717
run: |
1818
pnpm --filter @courselit/icons build
19+
pnpm --filter @courselit/page-models build
1920
pnpm --filter @courselit/common-models build
2021
pnpm --filter @courselit/utils build
2122
pnpm --filter @courselit/text-editor build
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { Metadata, ResolvingMetadata } from "next";
2+
import { ReactNode } from "react";
3+
import { FetchBuilder } from "@courselit/utils";
4+
import { headers } from "next/headers";
5+
import { getAddressFromHeaders } from "@ui-lib/utils";
6+
import { Course } from "@courselit/common-models";
7+
8+
export async function generateMetadata(
9+
{ params }: { params: { id: string } },
10+
parent: ResolvingMetadata,
11+
): Promise<Metadata> {
12+
const address = getAddressFromHeaders(headers);
13+
const product = await getProduct(params.id, address);
14+
15+
return {
16+
title: `${product ? product.title : "Post not found"} | ${(await parent)?.title?.absolute}`,
17+
};
18+
}
19+
20+
async function getProduct(id: string, address: string): Promise<Course | null> {
21+
const query = `
22+
query ($id: String!) {
23+
product: getCourse(id: $id) {
24+
courseId
25+
title
26+
description
27+
slug
28+
featuredImage {
29+
thumbnail
30+
file
31+
}
32+
creatorName
33+
updatedAt
34+
}
35+
}
36+
`;
37+
const fetch = new FetchBuilder()
38+
.setUrl(`${address}/api/graph`)
39+
.setPayload({ query, variables: { id } })
40+
.setIsGraphQLEndpoint(true)
41+
.build();
42+
try {
43+
const response = await fetch.exec();
44+
return response.product;
45+
} catch (err: any) {
46+
return null;
47+
}
48+
}
49+
50+
export default function Layout({ children }: { children: ReactNode }) {
51+
return children;
52+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"use client";
2+
3+
import { useContext } from "react";
4+
import { Course } from "@courselit/common-models";
5+
import { Section } from "@courselit/page-primitives";
6+
import Post from "./post";
7+
import { ThemeContext } from "@components/contexts";
8+
9+
export default function BlogPost({
10+
params,
11+
}: {
12+
params: { slug: string; id: string };
13+
course: Course;
14+
}) {
15+
const { theme } = useContext(ThemeContext);
16+
17+
return (
18+
<Section theme={theme.theme}>
19+
<div className="flex flex-col gap-4 min-h-[80vh]">
20+
<Post courseId={params.id} />
21+
</div>
22+
</Section>
23+
);
24+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"use client";
2+
3+
import { Breadcrumbs, TextRenderer } from "@courselit/components-library";
4+
import {
5+
Header1,
6+
Subheader2,
7+
Caption,
8+
Text1,
9+
Text2,
10+
} from "@courselit/page-primitives";
11+
import Image from "next/image";
12+
import { BLOG_UPDATED_PREFIX } from "@ui-config/strings";
13+
import { formattedLocaleDate, truncate } from "@ui-lib/utils";
14+
import useProduct from "@/hooks/use-product";
15+
import { AddressContext, ThemeContext } from "@components/contexts";
16+
import { useContext } from "react";
17+
import Link from "next/link";
18+
19+
export default function Post({ courseId }: { courseId: string }) {
20+
const address = useContext(AddressContext);
21+
const { theme } = useContext(ThemeContext);
22+
const { product: post, loaded } = useProduct(courseId, address);
23+
24+
if (!post) {
25+
return null;
26+
}
27+
28+
if (loaded && !post) {
29+
return <Text1>Post not found</Text1>;
30+
}
31+
32+
return (
33+
<>
34+
<Breadcrumbs aria-label="back to blog" className="mb-4">
35+
<Text2 className="cursor-pointer" theme={theme.theme}>
36+
<Link href="/blog">Blog</Link>
37+
</Text2>
38+
<Text2 theme={theme.theme}>{truncate(post.title, 20)}</Text2>
39+
</Breadcrumbs>
40+
<Header1 theme={theme.theme}>{post.title}</Header1>
41+
<div className="flex items-center gap-4">
42+
<Image
43+
src={
44+
post.featuredImage?.file ||
45+
"/courselit_backdrop_square.webp"
46+
}
47+
alt={post.featuredImage?.caption || ""}
48+
width={32}
49+
height={32}
50+
className="rounded-full"
51+
/>
52+
<div className="flex flex-col gap-1">
53+
<Subheader2 theme={theme.theme}>
54+
{post.creatorName}
55+
</Subheader2>
56+
<Caption theme={theme.theme}>
57+
<span className="font-semibold">
58+
{BLOG_UPDATED_PREFIX}:
59+
</span>{" "}
60+
{formattedLocaleDate(post.updatedAt, "long")}
61+
</Caption>
62+
</div>
63+
</div>
64+
{post.description && (
65+
<Text1 theme={theme.theme}>
66+
<TextRenderer json={JSON.parse(post.description)} />
67+
</Text1>
68+
)}
69+
</>
70+
);
71+
}

apps/web/app/(with-contexts)/(with-layout)/blog/blogs-list.tsx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,58 @@
11
"use client";
22

3-
import { useMemo, useState } from "react";
4-
import { SkeletonCard } from "./skeleton-card";
3+
import { useContext, useMemo } from "react";
54
import { BlogContentCard } from "./content-card";
65
import { PaginationControls } from "@components/public/pagination";
76
import { Constants, Course } from "@courselit/common-models";
87
import { useProducts } from "@/hooks/use-products";
8+
import { ProductCardSkeleton } from "@courselit/page-blocks";
9+
import { ThemeContext } from "@components/contexts";
10+
import { BookOpen } from "lucide-react";
11+
import { Button, Subheader1 } from "@courselit/page-primitives";
912

1013
const ITEMS_PER_PAGE = 9;
1114

1215
export function BlogsList({
16+
page,
1317
itemsPerPage = ITEMS_PER_PAGE,
18+
onPageChange,
1419
}: {
20+
page: number;
1521
itemsPerPage?: number;
22+
onPageChange: (page: number) => void;
1623
}) {
17-
const [currentPage, setCurrentPage] = useState(1);
24+
const { theme: uiTheme } = useContext(ThemeContext);
25+
const { theme } = uiTheme;
26+
1827
const filters = useMemo(
1928
() => [Constants.CourseType.BLOG.toUpperCase()],
2029
[],
2130
);
2231
const { products, loading, totalPages } = useProducts(
23-
currentPage,
32+
page,
2433
itemsPerPage,
2534
filters,
2635
true,
2736
);
2837

38+
if (!loading && totalPages && products.length === 0) {
39+
return (
40+
<div className="flex flex-col gap-4 items-center justify-center py-12 text-center">
41+
<BookOpen className="w-12 h-12 text-muted-foreground" />
42+
<Subheader1 theme={theme}>This page is empty.</Subheader1>
43+
<Button size="sm" theme={theme} onClick={() => onPageChange(1)}>
44+
Go to first page
45+
</Button>
46+
</div>
47+
);
48+
}
49+
2950
return (
3051
<div className="space-y-8">
3152
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
3253
{loading
3354
? Array.from({ length: ITEMS_PER_PAGE }).map((_, index) => (
34-
<SkeletonCard key={index} />
55+
<ProductCardSkeleton key={index} theme={theme} />
3556
))
3657
: products.map((product: Course) => (
3758
<BlogContentCard
@@ -41,9 +62,9 @@ export function BlogsList({
4162
))}
4263
</div>
4364
<PaginationControls
44-
currentPage={currentPage}
65+
currentPage={page}
4566
totalPages={Math.ceil(totalPages / itemsPerPage)}
46-
onPageChange={setCurrentPage}
67+
onPageChange={onPageChange}
4768
/>
4869
</div>
4970
);
Lines changed: 62 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,70 @@
1+
import { ThemeContext } from "@components/contexts";
12
import { Course } from "@courselit/common-models";
2-
import {
3-
ContentCard,
4-
ContentCardContent,
5-
ContentCardHeader,
6-
ContentCardImage,
7-
Image,
8-
} from "@courselit/components-library";
3+
import { Image } from "@courselit/components-library";
4+
import { useContext } from "react";
95
import { truncate } from "@ui-lib/utils";
6+
import {
7+
PageCard,
8+
PageCardContent,
9+
PageCardHeader,
10+
PageCardImage,
11+
Subheader1,
12+
} from "@courselit/page-primitives";
13+
import Link from "next/link";
1014

1115
export function BlogContentCard({ product }: { product: Course }) {
16+
const { theme: uiTheme } = useContext(ThemeContext);
17+
const { theme } = uiTheme;
18+
1219
return (
13-
<ContentCard href={`/blog/${product.slug}/${product.courseId}`}>
14-
<ContentCardImage
15-
src={product.featuredImage?.file || ""}
16-
alt={product.title}
17-
/>
18-
<ContentCardContent>
19-
<ContentCardHeader>
20-
{truncate(product.title, 32)}
21-
</ContentCardHeader>
22-
<div className="flex items-center justify-between text-sm text-muted-foreground">
23-
<div className="flex items-center gap-1 ascp">
24-
<Image
25-
src={product.user?.avatar?.thumbnail}
26-
alt={product.user?.name || "User Avatar"}
27-
loading="lazy"
28-
className="!aspect-square rounded-full"
29-
width="w-6"
30-
height="h-6"
31-
/>
32-
<span>
33-
{truncate(product.user?.name || "Unnamed", 30)}
34-
</span>
20+
<PageCard
21+
isLink={true}
22+
className="overflow-hidden"
23+
style={{
24+
backgroundColor: theme?.colors?.background,
25+
color: theme?.colors?.text,
26+
borderColor: theme?.colors?.border,
27+
}}
28+
theme={theme}
29+
>
30+
<Link
31+
href={`/blog/${product.slug}/${product.courseId}`}
32+
style={{
33+
height: "100%",
34+
display: "block",
35+
}}
36+
>
37+
<PageCardImage
38+
src={
39+
product.featuredImage?.file ||
40+
"/courselit_backdrop_square.webp"
41+
}
42+
alt={product.title}
43+
className="aspect-video object-cover"
44+
theme={theme}
45+
/>
46+
<PageCardContent theme={theme}>
47+
<PageCardHeader theme={theme}>
48+
{product.title}
49+
</PageCardHeader>
50+
<div className="flex items-center justify-between text-sm text-muted-foreground">
51+
<div className="flex items-center gap-2 ascp">
52+
<Image
53+
src={product.user?.avatar?.thumbnail}
54+
alt={product.user?.name || "User Avatar"}
55+
loading="lazy"
56+
className="rounded-full"
57+
objectFit="cover"
58+
width="w-8"
59+
height="h-8"
60+
/>
61+
<Subheader1 theme={theme}>
62+
{truncate(product.user?.name || "Unnamed", 20)}
63+
</Subheader1>
64+
</div>
3565
</div>
36-
</div>
37-
</ContentCardContent>
38-
</ContentCard>
66+
</PageCardContent>
67+
</Link>
68+
</PageCard>
3969
);
4070
}
Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,33 @@
1-
import { Suspense } from "react";
1+
"use client";
2+
3+
import { Suspense, useCallback, useContext } from "react";
24
import { BlogsList } from "./blogs-list";
35
import { PAGE_HEADER_ALL_POSTS } from "@ui-config/strings";
6+
import { ThemeContext } from "@components/contexts";
7+
import { Header1, Section } from "@courselit/page-primitives";
8+
import { useRouter, useSearchParams } from "next/navigation";
49

510
export default function BlogsPage() {
11+
const searchParams = useSearchParams();
12+
const page = parseInt(searchParams?.get("page") || "1");
13+
const router = useRouter();
14+
const { theme } = useContext(ThemeContext);
15+
16+
const handlePageChange = useCallback(
17+
(value: number) => {
18+
router.push(`/blog?page=${value}`);
19+
},
20+
[router],
21+
);
22+
623
return (
7-
<div className="container mx-auto px-4 py-8">
8-
<h1 className="text-3xl font-bold mb-8">{PAGE_HEADER_ALL_POSTS}</h1>
9-
<Suspense fallback={<div>Loading...</div>}>
10-
<BlogsList />
11-
</Suspense>
12-
</div>
24+
<Section theme={theme.theme}>
25+
<div className="flex flex-col gap-4 min-h-[80vh]">
26+
<Header1 theme={theme.theme}>{PAGE_HEADER_ALL_POSTS}</Header1>
27+
<Suspense fallback={<div>Loading...</div>}>
28+
<BlogsList page={page} onPageChange={handlePageChange} />
29+
</Suspense>
30+
</div>
31+
</Section>
1332
);
1433
}

0 commit comments

Comments
 (0)