Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^24.5.1",
"prisma": "^5.22.0",
"prisma": "^6.19.0",
"tsx": "^4.20.3",
"typescript": "^5.9.2"
},
"dependencies": {
"@octokit/graphql": "^9.0.1",
"@opensox/shared": "workspace:*",
"@prisma/client": "^5.22.0",
"@prisma/client": "^6.19.0",
"@trpc/server": "^11.5.1",
"cors": "^2.8.5",
"dotenv": "^16.5.0",
Expand Down
7 changes: 6 additions & 1 deletion apps/web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ const nextConfig = {
protocol: "https",
hostname: "avatars.githubusercontent.com",
},
{
protocol: "https",
hostname: "images.pexels.com",
pathname: "/**", // optional but recommended
},
],
},
};

module.exports = nextConfig;
module.exports = nextConfig;
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@
"posthog-js": "^1.203.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^10.1.0",
"react-qr-code": "^2.0.18",
"react-tweet": "^3.2.1",
"remark-gfm": "^4.0.1",
"superjson": "^2.2.5",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
Expand Down
135 changes: 135 additions & 0 deletions apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"use client";

import { NEWSLETTERS } from "@/data/newsletters";
import NewsletterContent from "@/components/newsletters/NewsletterContent";
import { CalendarIcon, ArrowLeftIcon, ClockIcon, BookmarkIcon } from "@heroicons/react/24/outline";
import { BookmarkIcon as BookmarkSolid } from "@heroicons/react/24/solid";
import Link from "next/link";
import { useState } from "react";

export default function Page({ params }: { params: { slug: string } }) {
const n = NEWSLETTERS.find((x) => x.slug === params.slug);
const [isSaved, setIsSaved] = useState(false);

if (!n) {
return (
<div className="w-full min-h-screen bg-black flex items-center justify-center">
<div className="text-center">
<h1 className="text-xl font-light text-white mb-8">Newsletter not found</h1>
<Link
href="/dashboard/newsletters"
className="inline-flex items-center gap-2 text-zinc-400 hover:text-white transition-colors text-sm font-light"
>
<ArrowLeftIcon className="w-3 h-3" />
Back to newsletters
</Link>
</div>
</div>
);
}

const wordCount = n.body.split(/\s+/).length;
const readingTime = Math.ceil(wordCount / 200);

return (
<main className="min-h-screen bg-black">

{/* Back Navigation */}
<div className="border-b border-zinc-900 sticky top-0 bg-black/80 backdrop-blur-xl z-40">
<div className="max-w-3xl mx-auto px-6 py-6">
<Link
href="/dashboard/newsletters"
className="inline-flex items-center gap-3 text-zinc-500 hover:text-white transition-colors text-sm font-light"
>
<ArrowLeftIcon className="w-3 h-3" />
Newsletters
</Link>
</div>
</div>

{/* Article Content */}
<article className="max-w-3xl mx-auto px-6 py-16">

{/* Article Header */}
<header className="mb-20">
{n.featured && (
<div className="mb-8">
<span className="text-xs tracking-[0.2em] uppercase text-zinc-600 font-light">
Featured
</span>
</div>
)}

<h1 className="text-4xl lg:text-5xl font-light tracking-tight text-white mb-8 leading-[1.1]">
{n.title}
</h1>

<p className="text-lg text-zinc-400 mb-12 leading-relaxed font-light">
{n.excerpt}
</p>

{/* Meta Information */}
<div className="flex items-center justify-between py-6 border-y border-zinc-900">
<div className="flex items-center gap-8 text-zinc-500 text-sm font-light">
<div className="flex items-center gap-2">
<CalendarIcon className="w-3.5 h-3.5" />
<span>{new Date(n.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
})}</span>
</div>

<div className="flex items-center gap-2">
<ClockIcon className="w-3.5 h-3.5" />
<span>{readingTime} min</span>
</div>
</div>

{/* Save Button */}
<button
onClick={() => setIsSaved(!isSaved)}
className={`
flex items-center gap-2 transition-all duration-300 text-sm font-light
${isSaved
? 'text-white'
: 'text-zinc-500 hover:text-white'
}
`}
>
{isSaved ? (
<BookmarkSolid className="w-4 h-4" />
) : (
<BookmarkIcon className="w-4 h-4" />
)}
<span>{isSaved ? 'Saved' : 'Save'}</span>
</button>
</div>
</header>

{/* Article Body */}
<div className="prose prose-invert max-w-none">
<div className="text-zinc-300 leading-[1.8] text-base font-light">
<NewsletterContent body={n.body} />
</div>
</div>

{/* Article Footer */}
<footer className="mt-32 pt-12 border-t border-zinc-900">
<div className="flex items-center justify-between">
<div className="text-zinc-600 text-sm font-light">
OpenSox Team
</div>

<Link
href="/dashboard/newsletters"
className="text-zinc-400 hover:text-white text-sm font-light transition-colors"
>
More newsletters →
</Link>
</div>
</footer>
</article>
</main>
);
}
9 changes: 9 additions & 0 deletions apps/web/src/app/(main)/dashboard/newsletters/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import NewsletterContainer from '@/components/newsletters/NewsletterContainer'

export default function Page() {
return (
<div className="min-h-screen bg-[#101010]">
<NewsletterContainer />
</div>
)
}
11 changes: 11 additions & 0 deletions apps/web/src/components/dashboard/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
StarIcon,
HeartIcon,
EnvelopeIcon,
NewspaperIcon,
} from "@heroicons/react/24/outline";
import { useShowSidebar } from "@/store/useShowSidebar";
import { signOut } from "next-auth/react";
Expand Down Expand Up @@ -138,6 +139,16 @@ export default function Sidebar() {
icon={<SparklesIcon className="size-5" />}
collapsed={isCollapsed}
/>
<Link
href="newsletters"
className={getSidebarLinkClassName(pathname, "/newsletters")}
>
<SidebarItem
itemName="Newsletters"
icon={<NewspaperIcon className="size-5" />}
collapsed={isCollapsed}
/>
</Link>
Comment on lines +142 to +151
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix the inconsistent route path for Newsletters.

The href "newsletters" uses a relative path, while other sidebar items use absolute paths like "/dashboard/home" and "/dashboard/projects" (lines 30, 35). This inconsistency can cause navigation issues and breaks the established pattern.

Apply this diff to use an absolute path:

         <Link
-          href="newsletters"
+          href="/dashboard/newsletters"
           className={getSidebarLinkClassName(pathname, "/newsletters")}
         >

Also update the className computation to use the full path:

         <Link
           href="/dashboard/newsletters"
-          className={getSidebarLinkClassName(pathname, "/newsletters")}
+          className={getSidebarLinkClassName(pathname, "/dashboard/newsletters")}
         >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Link
href="newsletters"
className={getSidebarLinkClassName(pathname, "/newsletters")}
>
<SidebarItem
itemName="Newsletters"
icon={<NewspaperIcon className="size-5" />}
collapsed={isCollapsed}
/>
</Link>
<Link
href="/dashboard/newsletters"
className={getSidebarLinkClassName(pathname, "/dashboard/newsletters")}
>
<SidebarItem
itemName="Newsletters"
icon={<NewspaperIcon className="size-5" />}
collapsed={isCollapsed}
/>
</Link>
🤖 Prompt for AI Agents
In apps/web/src/components/dashboard/Sidebar.tsx around lines 142 to 151, the
Newsletters link uses a relative href ("newsletters") and passes "/newsletters"
to getSidebarLinkClassName, breaking the absolute path pattern used elsewhere;
change the href to the absolute route "/dashboard/newsletters" and update the
className call to use getSidebarLinkClassName(pathname,
"/dashboard/newsletters") so the link and active-class computation follow the
same full-path convention.

<SidebarItem
itemName="Opensox premium"
onclick={premiumClickHandler}
Expand Down
129 changes: 129 additions & 0 deletions apps/web/src/components/newsletters/NewsletterContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"use client";

import { useState, useMemo } from "react";
import NewsletterList from "./NewsletterList";
import { MagnifyingGlassIcon, CalendarIcon, ChevronDownIcon } from "@heroicons/react/24/outline";

export default function NewsletterContainer() {
const [q, setQ] = useState("");
const [selectedMonth, setSelectedMonth] = useState<string>("all");
const [isDropdownOpen, setIsDropdownOpen] = useState(false);

const monthFilters = useMemo(() => {
const months = [
{ value: "all", label: "All months" },
{ value: "2025-11", label: "November 2025" },
{ value: "2025-10", label: "October 2025" },
{ value: "2025-09", label: "September 2025" },
];
return months;
}, []);

const selectedFilterLabel = monthFilters.find(filter => filter.value === selectedMonth)?.label || "All months";

return (
<div className="w-full min-h-screen bg-black">
<div className="max-w-5xl mx-auto px-6 py-20">

{/* Header */}
<div className="mb-4">
<div className="inline-block mb-6">
<span className="text-xs tracking-[0.2em] uppercase text-zinc-500 font-light">
Pro Access
</span>
</div>

<h1 className="text-6xl lg:text-7xl font-light tracking-tight text-white mb-8 leading-[0.95]">
Newsletters
</h1>

<p className="text-lg text-zinc-400 max-w-xl font-light leading-relaxed">
Curated insights and platform updates
</p>
</div>

{/* Controls */}
<div className="flex flex-col lg:flex-row gap-3 mb-16">

{/* Search */}
<div className="relative flex-1">
<MagnifyingGlassIcon className="absolute left-0 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-600" />
<input
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="Search"
className="w-full pl-7 pr-4 py-3 bg-transparent border-b border-zinc-800 text-white placeholder:text-zinc-600 focus:outline-none focus:border-zinc-600 transition-colors text-sm font-light"
/>
</div>

{/* Filter */}
<div className="relative lg:w-48">
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="w-full flex items-center justify-between pl-7 pr-4 py-3 bg-transparent border-b border-zinc-800 text-white hover:border-zinc-600 transition-colors group"
>
<CalendarIcon className="absolute left-0 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-600 group-hover:text-zinc-400 transition-colors" />
<span className="text-sm font-light">{selectedFilterLabel}</span>
<ChevronDownIcon
className={`w-3 h-3 text-zinc-600 transition-all duration-300 ${
isDropdownOpen ? 'rotate-180' : ''
}`}
/>
</button>

{isDropdownOpen && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setIsDropdownOpen(false)}
/>

<div className="absolute top-full right-0 mt-3 w-64 bg-zinc-950 border border-zinc-800 z-20 overflow-hidden">
{monthFilters.map((filter) => (
<button
key={filter.value}
onClick={() => {
setSelectedMonth(filter.value);
setIsDropdownOpen(false);
}}
className={`w-full text-left px-6 py-4 text-sm transition-all font-light ${
selectedMonth === filter.value
? 'text-white bg-zinc-900'
: 'text-zinc-400 hover:text-white hover:bg-zinc-900/50'
}`}
>
{filter.label}
</button>
))}
</div>
</>
)}
</div>
</div>

{/* Active Filter */}
{selectedMonth !== "all" && (
<div className="flex items-center gap-4 mb-12 pb-12 border-b border-zinc-900">
<span className="text-xs text-zinc-600 font-light tracking-wider">FILTERED BY</span>
<button
onClick={() => setSelectedMonth("all")}
className="text-sm text-zinc-400 hover:text-white transition-colors font-light"
>
{selectedFilterLabel} ×
</button>
</div>
)}

{/* Newsletter List */}
<NewsletterList query={q} monthFilter={selectedMonth} />

{/* Footer */}
<div className="text-center pt-12 mt-12 border-t border-zinc-900">
<p className="text-sm text-zinc-600 font-light tracking-wide">
More insights coming soon
</p>
</div>
</div>
</div>
);
}
Loading