Skip to content

Commit b6e4500

Browse files
committed
built a newsletter section
1 parent a10539e commit b6e4500

File tree

23 files changed

+858
-13
lines changed

23 files changed

+858
-13
lines changed

apps/api/src/routers/_app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { userRouter } from "./user.js";
44
import { projectRouter } from "./projects.js";
55
import { authRouter } from "./auth.js";
66
import { z } from "zod";
7+
import { newsletterRouter } from "./newsletter.js";
78

89
const testRouter = router({
910
test: publicProcedure
@@ -19,6 +20,7 @@ export const appRouter = router({
1920
user: userRouter,
2021
project: projectRouter,
2122
auth: authRouter,
23+
newsletter: newsletterRouter,
2224
});
2325

2426
export type AppRouter = typeof appRouter;

apps/api/src/routers/newsletter.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { z } from "zod";
2+
import { router, publicProcedure } from "../trpc.js";
3+
import { newsletterService } from "../services/newsletter.service.js";
4+
5+
export const newsletterRouter = router({
6+
list: publicProcedure
7+
.input(z.object({ search: z.string().optional() }).optional())
8+
.query(async ({ input }) => newsletterService.list(input?.search)),
9+
10+
bySlug: publicProcedure
11+
.input(z.object({ slug: z.string() }))
12+
.query(async ({ input }) => newsletterService.bySlug(input.slug)),
13+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Prisma } from "@prisma/client";
2+
import dbClient from "../prisma.js";
3+
4+
const { prisma } = dbClient;
5+
6+
export const newsletterService = {
7+
list: async (search?: string) => {
8+
const where: Prisma.NewsletterIssueWhereInput | undefined = search
9+
? {
10+
OR: [
11+
{ title: { contains: search, mode: "insensitive" } },
12+
{ summary: { contains: search, mode: "insensitive" } },
13+
{ tags: { hasSome: search.split(" ").filter(Boolean) } },
14+
],
15+
}
16+
: undefined;
17+
18+
return prisma.newsletterIssue.findMany({
19+
...(where && { where }),
20+
orderBy: { publishedAt: "desc" },
21+
select: {
22+
id: true,
23+
slug: true,
24+
title: true,
25+
summary: true,
26+
publishedAt: true,
27+
readTime: true,
28+
heroMediaUrl: true,
29+
heroMediaType: true,
30+
tags: true,
31+
},
32+
});
33+
},
34+
35+
bySlug: async (slug: string) => {
36+
return prisma.newsletterIssue.findUnique({
37+
where: { slug },
38+
include: {
39+
sections: {
40+
orderBy: { order: "asc" },
41+
},
42+
},
43+
});
44+
},
45+
};

apps/web/next.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ const nextConfig = {
66
protocol: "https",
77
hostname: "avatars.githubusercontent.com",
88
},
9+
{
10+
protocol: "https",
11+
hostname: "assets.opensox.dev",
12+
},
913
],
1014
},
1115
};

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"posthog-js": "^1.203.1",
3333
"react": "^18.2.0",
3434
"react-dom": "^18.2.0",
35+
"react-player": "^3.3.3",
3536
"react-qr-code": "^2.0.18",
3637
"react-tweet": "^3.2.1",
3738
"tailwind-merge": "^2.5.4",
379 KB
Loading

apps/web/public/images/welcome.jpg

1.32 MB
Loading

apps/web/src/app/(main)/dashboard/layout.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ export default function DashboardLayout({
1717
<div className="flex w-full h-16">
1818
<DashboardHeader></DashboardHeader>
1919
</div>
20-
<div className="flex flex-row w-full">
20+
<div className="flex flex-row w-full overflow-hidden">
2121
{showFilters && <FiltersContainer></FiltersContainer>}
2222
<aside
23-
className={`w-48 md:w-[40%] xl:w-[20%] ${showSidebar ? "block relative" : "hidden"} xl:block`}
23+
className={`flex-shrink-0 w-48 md:w-[40%] xl:w-[20%] ${showSidebar ? "block relative" : "hidden"} xl:block overflow-y-auto`}
2424
>
2525
<Sidebar></Sidebar>
2626
</aside>
27-
<main className="flex-grow">{children}</main>
27+
<main className="flex-grow overflow-y-auto">{children}</main>
2828
</div>
2929
</div>
3030
);
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"use client";
2+
3+
import { Suspense, lazy } from "react";
4+
import Link from "next/link";
5+
import { useNewsletterDetail } from "@/app/api/newsletter";
6+
import { NewsletterIssue } from "@/data/newsletters";
7+
import { NewsletterHero } from "@/components/newsletter/NewsletterHero";
8+
import { NewsletterSectionRenderer } from "@/components/newsletter/NewsletterSectionRenderer";
9+
import { EngagementBar } from "../engagementBar";
10+
11+
const LazyNewsletterSectionRenderer = lazy(() =>
12+
import("@/components/newsletter/NewsletterSectionRenderer").then((mod) => ({
13+
default: mod.NewsletterSectionRenderer,
14+
}))
15+
);
16+
17+
type NewsletterDetailClientProps = {
18+
slug: string;
19+
fallback: NewsletterIssue;
20+
};
21+
22+
function SectionsSkeleton() {
23+
return (
24+
<div className="space-y-8">
25+
{[1, 2, 3].map((i) => (
26+
<div key={i} className="space-y-3 animate-pulse">
27+
<div className="h-8 w-48 rounded-lg bg-ox-black-2" />
28+
<div className="space-y-2">
29+
<div className="h-4 w-full rounded bg-ox-black-2" />
30+
<div className="h-4 w-5/6 rounded bg-ox-black-2" />
31+
</div>
32+
</div>
33+
))}
34+
</div>
35+
);
36+
}
37+
38+
function NavigationButtons() {
39+
return (
40+
<div className="flex gap-3 flex-wrap">
41+
<Link
42+
href="/dashboard/newsletters"
43+
className="rounded-full border border-ox-purple px-6 py-2 text-xs font-semibold uppercase tracking-wide text-ox-purple transition hover:bg-ox-purple hover:text-ox-white"
44+
>
45+
Home
46+
</Link>
47+
<Link
48+
href="/pricing"
49+
className="rounded-full border border-ox-purple px-6 py-2 text-xs font-semibold uppercase tracking-wide text-ox-purple transition hover:bg-ox-purple hover:text-ox-white"
50+
>
51+
Pricing
52+
</Link>
53+
<a
54+
href="https://discord.gg/opensox"
55+
target="_blank"
56+
rel="noopener noreferrer"
57+
className="rounded-full border border-ox-purple px-6 py-2 text-xs font-semibold uppercase tracking-wide text-ox-purple transition hover:bg-ox-purple hover:text-ox-white"
58+
>
59+
Community
60+
</a>
61+
</div>
62+
);
63+
}
64+
65+
export function NewsletterDetailClient({ slug, fallback }: NewsletterDetailClientProps) {
66+
const { data, isLoading } = useNewsletterDetail(slug);
67+
const issue = data ?? fallback;
68+
69+
if (!issue) return null;
70+
71+
return (
72+
<div className="flex flex-col gap-6 p-4 xl:p-6">
73+
{/* @ts-ignore */}
74+
<NewsletterHero issue={issue} shareButton={<EngagementBar slug={slug} />} />
75+
<NavigationButtons />
76+
<Suspense fallback={<SectionsSkeleton />}>
77+
{/* @ts-ignore */}
78+
<LazyNewsletterSectionRenderer sections={issue.sections} />
79+
</Suspense>
80+
</div>
81+
);
82+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { notFound } from "next/navigation";
2+
import { newsletterIssues } from "@/data/newsletters";
3+
import { NewsletterDetailClient } from "./NewsletterDetailClient";
4+
5+
interface PageProps {
6+
params: Promise<{ slug: string }>;
7+
}
8+
9+
10+
export default async function NewsletterDetail({ params }: PageProps) {
11+
const { slug } = await params;
12+
const fallback = newsletterIssues.find((issue) => issue.slug === slug);
13+
14+
if (!fallback) {
15+
notFound();
16+
}
17+
18+
return <NewsletterDetailClient slug={slug} fallback={fallback} />;
19+
}

0 commit comments

Comments
 (0)