diff --git a/apps/api/src/routers/_app.ts b/apps/api/src/routers/_app.ts index 782b436..57a0581 100644 --- a/apps/api/src/routers/_app.ts +++ b/apps/api/src/routers/_app.ts @@ -5,6 +5,7 @@ import { projectRouter } from "./projects.js"; import { authRouter } from "./auth.js"; import { paymentRouter } from "./payment.js"; import { z } from "zod"; +import { newsletterRouter } from "./newsletter.js"; const testRouter = router({ test: publicProcedure @@ -20,7 +21,7 @@ export const appRouter = router({ user: userRouter, project: projectRouter, auth: authRouter, - payment: paymentRouter, + newsletter: newsletterRouter, }); export type AppRouter = typeof appRouter; diff --git a/apps/api/src/routers/newsletter.ts b/apps/api/src/routers/newsletter.ts new file mode 100644 index 0000000..1dc6616 --- /dev/null +++ b/apps/api/src/routers/newsletter.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; +import { router, publicProcedure } from "../trpc.js"; +import { newsletterService } from "../services/newsletter.service.js"; + +export const newsletterRouter = router({ + list: publicProcedure + .input(z.object({ search: z.string().optional() }).optional()) + .query(async ({ input }) => newsletterService.list(input?.search)), + + bySlug: publicProcedure + .input(z.object({ slug: z.string() })) + .query(async ({ input }) => newsletterService.bySlug(input.slug)), +}); \ No newline at end of file diff --git a/apps/api/src/services/newsletter.service.ts b/apps/api/src/services/newsletter.service.ts new file mode 100644 index 0000000..ab003f0 --- /dev/null +++ b/apps/api/src/services/newsletter.service.ts @@ -0,0 +1,45 @@ +import { Prisma } from "@prisma/client"; +import dbClient from "../prisma.js"; + +const { prisma } = dbClient; + +export const newsletterService = { + list: async (search?: string) => { + const where: Prisma.NewsletterIssueWhereInput | undefined = search + ? { + OR: [ + { title: { contains: search, mode: "insensitive" } }, + { summary: { contains: search, mode: "insensitive" } }, + { tags: { hasSome: search.split(" ").filter(Boolean) } }, + ], + } + : undefined; + + return prisma.newsletterIssue.findMany({ + ...(where && { where }), + orderBy: { publishedAt: "desc" }, + select: { + id: true, + slug: true, + title: true, + summary: true, + publishedAt: true, + readTime: true, + heroMediaUrl: true, + heroMediaType: true, + tags: true, + }, + }); + }, + + bySlug: async (slug: string) => { + return prisma.newsletterIssue.findUnique({ + where: { slug }, + include: { + sections: { + orderBy: { order: "asc" }, + }, + }, + }); + }, +}; \ No newline at end of file diff --git a/apps/web/next.config.js b/apps/web/next.config.js index fdebc20..cda1bae 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -6,6 +6,10 @@ const nextConfig = { protocol: "https", hostname: "avatars.githubusercontent.com", }, + { + protocol: "https", + hostname: "assets.opensox.dev", + }, ], }, }; diff --git a/apps/web/package.json b/apps/web/package.json index 2abd594..789fb1d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -32,6 +32,7 @@ "posthog-js": "^1.203.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-player": "^3.3.3", "react-qr-code": "^2.0.18", "react-tweet": "^3.2.1", "superjson": "^2.2.5", diff --git a/apps/web/public/images/Screenshot-2025-10-22.png b/apps/web/public/images/Screenshot-2025-10-22.png new file mode 100644 index 0000000..c407c73 Binary files /dev/null and b/apps/web/public/images/Screenshot-2025-10-22.png differ diff --git a/apps/web/public/images/welcome.jpg b/apps/web/public/images/welcome.jpg new file mode 100644 index 0000000..d466df0 Binary files /dev/null and b/apps/web/public/images/welcome.jpg differ diff --git a/apps/web/src/app/(main)/dashboard/layout.tsx b/apps/web/src/app/(main)/dashboard/layout.tsx index 8bb4abb..2bfbd5e 100644 --- a/apps/web/src/app/(main)/dashboard/layout.tsx +++ b/apps/web/src/app/(main)/dashboard/layout.tsx @@ -17,14 +17,14 @@ export default function DashboardLayout({
-
+
{showFilters && } -
{children}
+
{children}
); diff --git a/apps/web/src/app/(main)/dashboard/newsletters/[slug]/NewsletterDetailClient.tsx b/apps/web/src/app/(main)/dashboard/newsletters/[slug]/NewsletterDetailClient.tsx new file mode 100644 index 0000000..2d25210 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/[slug]/NewsletterDetailClient.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { Suspense, lazy } from "react"; +import Link from "next/link"; +import { useNewsletterDetail } from "@/app/api/newsletter"; +import { NewsletterIssue } from "@/data/newsletters"; +import { NewsletterHero } from "@/components/newsletter/NewsletterHero"; +import { NewsletterSectionRenderer } from "@/components/newsletter/NewsletterSectionRenderer"; +import { EngagementBar } from "../engagementBar"; + +const LazyNewsletterSectionRenderer = lazy(() => + import("@/components/newsletter/NewsletterSectionRenderer").then((mod) => ({ + default: mod.NewsletterSectionRenderer, + })) +); + +type NewsletterDetailClientProps = { + slug: string; + fallback: NewsletterIssue; +}; + +function SectionsSkeleton() { + return ( +
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+ ))} +
+ ); +} + +function NavigationButtons() { + return ( +
+ + Home + + + Pricing + + + Community + +
+ ); +} + +export function NewsletterDetailClient({ slug, fallback }: NewsletterDetailClientProps) { + const { data, isLoading } = useNewsletterDetail(slug); + const issue = data ?? fallback; + + if (!issue) return null; + + return ( +
+ {/* @ts-ignore */} + } /> + + }> + {/* @ts-ignore */} + + +
+ ); +} \ No newline at end of file diff --git a/apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx b/apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx new file mode 100644 index 0000000..a807743 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx @@ -0,0 +1,19 @@ +import { notFound } from "next/navigation"; +import { newsletterIssues } from "@/data/newsletters"; +import { NewsletterDetailClient } from "./NewsletterDetailClient"; + +interface PageProps { + params: Promise<{ slug: string }>; +} + + +export default async function NewsletterDetail({ params }: PageProps) { + const { slug } = await params; + const fallback = newsletterIssues.find((issue) => issue.slug === slug); + + if (!fallback) { + notFound(); + } + + return ; +} \ No newline at end of file diff --git a/apps/web/src/app/(main)/dashboard/newsletters/engagementBar.tsx b/apps/web/src/app/(main)/dashboard/newsletters/engagementBar.tsx new file mode 100644 index 0000000..93e6f0a --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/engagementBar.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { useCallback, useState } from "react"; + +interface EngagementBarProps { + slug: string; +} + +export type { EngagementBarProps }; + +export function EngagementBar({ slug }: EngagementBarProps) { + const link = `/dashboard/newsletters/${slug}`; + const [copied, setCopied] = useState(false); + + const handleShare = useCallback(async () => { + if (typeof window === "undefined") return; + const absoluteUrl = `${window.location.origin}${link}`; + + // Try native share API first + if (navigator.share) { + try { + await navigator.share({ + url: absoluteUrl, + }); + return; + } catch (error) { + console.error("Share failed:", error); + } + } + + // Fallback to copy to clipboard + try { + await navigator.clipboard.writeText(absoluteUrl); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (error) { + console.error("Unable to copy link", error); + } + }, [link]); + + return ( + + ); +} \ No newline at end of file diff --git a/apps/web/src/app/(main)/dashboard/newsletters/page.tsx b/apps/web/src/app/(main)/dashboard/newsletters/page.tsx new file mode 100644 index 0000000..d1d9f9a --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/page.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { useMemo, useState, Suspense, lazy } from "react"; +import { useNewsletterList } from "@/app/api/newsletter"; +import { newsletterIssues as mockIssues, type NewsletterIssue } from "@/data/newsletters"; + +const LazyNewsletterCard = lazy(() => + import("@/components/newsletter/NewsletterCard").then((mod) => ({ + default: mod.NewsletterCard, + })) +); + +function CardSkeleton() { + return ( +
+ ); +} + +function NewsletterCardList({ issues }: { issues: NewsletterIssue[] }) { + return ( + <> + {issues.map((issue: NewsletterIssue) => ( + }> + + + ))} + + ); +} + +export default function NewsletterIndex() { + const [query, setQuery] = useState(""); + const { data, isLoading } = useNewsletterList(query) as { + data: NewsletterIssue[] | undefined; + isLoading: boolean; + }; + const issues = data?.length ? data : mockIssues; + + const filteredIssues = useMemo(() => { + const normalized = query.trim().toLowerCase(); + if (!normalized) return issues as NewsletterIssue[]; + + return (issues as NewsletterIssue[]).filter((issue) => + `${issue.title} ${issue.summary}` + .toLowerCase() + .includes(normalized) + ); + }, [issues, query]); + + return ( +
+
+

Newsletter

+ {query && ( +
+ setQuery(e.target.value)} + placeholder="Search newsletters" + disabled={isLoading} + className="w-full bg-transparent text-sm text-ox-white placeholder:text-ox-gray-light focus:outline-none" + /> +
+ )} +
+
+ {isLoading ? ( + + ) : filteredIssues.length > 0 ? ( + + ) : ( + + )} +
+
+ ); +} + +function SkeletonList() { + return ( +
+ {[1, 2, 3].map((key) => ( +
+ ))} +
+ ); +} + +function EmptyState({ query }: { query: string }) { + return ( +
+

No matches found

+

+ We couldn’t find any newsletters for “{query}”. Try a different keyword or clear the search + filter. +

+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/app/SessionWrapper.tsx b/apps/web/src/app/SessionWrapper.tsx index 43e406e..8f6be48 100644 --- a/apps/web/src/app/SessionWrapper.tsx +++ b/apps/web/src/app/SessionWrapper.tsx @@ -11,5 +11,7 @@ export function SessionWrapper({ children: ReactNode; session: Session | null; }) { - return {children}; + return ( + {children} + ); } diff --git a/apps/web/src/app/api/newsletter.ts b/apps/web/src/app/api/newsletter.ts new file mode 100644 index 0000000..c1e5182 --- /dev/null +++ b/apps/web/src/app/api/newsletter.ts @@ -0,0 +1,25 @@ +"use client"; + +import { newsletterIssues } from "@/data/newsletters"; +import { trpc } from "@/lib/trpc"; + +export const useNewsletterList = (search?: string) => { + // Use mock data immediately for instant load + const filteredData = search + ? newsletterIssues.filter((issue) => + `${issue.title} ${issue.summary}` + .toLowerCase() + .includes(search.toLowerCase()) + ) + : newsletterIssues; + + return { + data: filteredData, + isLoading: false, + error: null, + }; +}; + +export const useNewsletterDetail = (slug: string) => { + return trpc.newsletter.bySlug.useQuery({ slug }, { enabled: Boolean(slug) }); +}; \ No newline at end of file diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 6ae57e0..b4db592 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -73,4 +73,5 @@ html { scroll-behavior: smooth; + scrollbar-gutter: stable; } \ No newline at end of file diff --git a/apps/web/src/components/dashboard/Sidebar.tsx b/apps/web/src/components/dashboard/Sidebar.tsx index d9f3156..f2ebcc4 100644 --- a/apps/web/src/components/dashboard/Sidebar.tsx +++ b/apps/web/src/components/dashboard/Sidebar.tsx @@ -9,18 +9,22 @@ import { useShowSidebar } from "@/store/useShowSidebar"; import { signOut } from "next-auth/react"; const SIDEBAR_ROUTES = [ - { - path: "/dashboard/home", - label: "Home", + { + path: "/dashboard/home", + label: "Home", }, - { - path: "/dashboard/projects", - label: "Projects", + { + path: "/dashboard/projects", + label: "Projects", + }, + { + path: "/dashboard/newsletters", + label: "Newsletter", }, ]; const getSidebarLinkClassName = (currentPath: string, routePath: string) => { - const isActive = currentPath === routePath; + const isActive = currentPath.startsWith(routePath); return `${isActive ? "text-ox-purple" : "text-ox-white"}`; }; diff --git a/apps/web/src/components/newsletter/NewsletterCard.tsx b/apps/web/src/components/newsletter/NewsletterCard.tsx new file mode 100644 index 0000000..f65e5e0 --- /dev/null +++ b/apps/web/src/components/newsletter/NewsletterCard.tsx @@ -0,0 +1,116 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { useCallback, useMemo } from "react"; +import { NewsletterIssue } from "@/data/newsletters"; + +interface NewsletterCardProps { + issue: NewsletterIssue; +} + +export function NewsletterCard({ issue }: NewsletterCardProps) { + + const formattedDate = useMemo( + () => + new Date(issue.publishedAt).toLocaleDateString(undefined, { + month: "short", + day: "numeric", + year: "numeric", + }), + [issue.publishedAt] + ); + + const sharePath = useMemo(() => `/dashboard/newsletters/${issue.slug}`, [issue.slug]); + + const handleShare = useCallback( + async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (typeof window === "undefined") return; + const absoluteUrl = `${window.location.origin}${sharePath}`; + + if (navigator.share) { + try { + await navigator.share({ + url: absoluteUrl, + }); + return; + } catch { + /* fall back to clipboard */ + } + } + + if (navigator.clipboard) { + try { + await navigator.clipboard.writeText(absoluteUrl); + } catch (error) { + console.error("Unable to copy link", error); + } + } + }, + [issue.summary, issue.title, sharePath] + ); + + return ( +
+
+ +

{issue.title}

+
+ {formattedDate} + · + {issue.readTime} +
+ + +
+
+ ); +} + +function MediaPreview({ issue }: NewsletterCardProps) { + if (!issue.heroMediaUrl) { + return null; + } + + if (issue.heroMediaType === "video") { + return ( +
+
+ ); + } + + return ( +
+ {issue.title} +
+ ); +} \ No newline at end of file diff --git a/apps/web/src/components/newsletter/NewsletterHeader.tsx b/apps/web/src/components/newsletter/NewsletterHeader.tsx new file mode 100644 index 0000000..0d4799f --- /dev/null +++ b/apps/web/src/components/newsletter/NewsletterHeader.tsx @@ -0,0 +1,88 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { cn } from "@/lib/utils"; +import { InputHTMLAttributes } from "react"; + +const NAV_LINKS = [ + { href: "/dashboard/home", label: "Home" }, + { href: "/pricing", label: "Pricing" }, + { href: "https://discord.gg/nC9x4hef", label: "Community", external: true }, +]; + +type Props = { + title: string; + subtitle?: string; + searchTerm?: string; + onSearch?: (value: string) => void; + actions?: React.ReactNode; + searchPlaceholder?: string; +} & Pick, "disabled">; + +export function NewsletterHeader({ + title, + subtitle, + searchTerm = "", + onSearch, + actions, + disabled, + searchPlaceholder = "Search newsletters", +}: Props) { + const pathname = usePathname(); + const handleSearchChange = (event: React.ChangeEvent) => { + onSearch?.(event.target.value); + }; + + return ( +
+
+
+

{title}

+ {subtitle && ( +

{subtitle}

+ )} +
+
+ {NAV_LINKS.map((link) => { + const isActive = !link.external && pathname.startsWith(link.href); + return ( + + {link.label} + + ); + })} + {actions} +
+
+ {onSearch && ( +
+
+ +
+
+ Premium + Issue #122 + September · October +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/apps/web/src/components/newsletter/NewsletterHero.tsx b/apps/web/src/components/newsletter/NewsletterHero.tsx new file mode 100644 index 0000000..ef4a223 --- /dev/null +++ b/apps/web/src/components/newsletter/NewsletterHero.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { useMemo } from "react"; +import { NewsletterIssue } from "@/data/newsletters"; + +interface NewsletterHeroProps { + issue: NewsletterIssue; + shareButton?: React.ReactNode; +} + +export function NewsletterHero({ issue, shareButton }: NewsletterHeroProps) { + const formattedDate = useMemo( + () => + new Date(issue.publishedAt).toLocaleDateString(undefined, { + weekday: "short", + month: "short", + day: "numeric", + year: "numeric", + }), + [issue.publishedAt] + ); + + return ( +
+
+
+
+ {formattedDate} + · {issue.readTime} +
+

{issue.title}

+

{issue.summary}

+
+ {shareButton &&
{shareButton}
} +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/components/newsletter/NewsletterSectionRenderer.tsx b/apps/web/src/components/newsletter/NewsletterSectionRenderer.tsx new file mode 100644 index 0000000..8219dd8 --- /dev/null +++ b/apps/web/src/components/newsletter/NewsletterSectionRenderer.tsx @@ -0,0 +1,101 @@ +"use client"; + +import Image from "next/image"; +import dynamic from "next/dynamic"; +import { NewsletterSection } from "@/data/newsletters"; + +const ReactPlayer = dynamic( + () => import("react-player").then((mod) => mod.default), + { + ssr: false, + loading: () => null, + } +) as any; + +interface NewsletterSectionRendererProps { + sections: NewsletterSection[]; +} + +export function NewsletterSectionRenderer({ sections }: NewsletterSectionRendererProps) { + return ( +
+ {sections.map((section, index) => { + if (section.type === "text") { + return ( +
+

{section.heading}

+

{section.body}

+
+ ); + } + + if (section.type === "media" && section.src && section.mediaSrc) { + return ( +
+

{section.heading}

+
+
+ {section.alt +
+
+ +
+
+

{section.body}

+
+ ); + } + + if ((section.type === "image" || section.type === "video") && section.src) { + return ( +
+

{section.heading}

+ {section.type === "video" ? ( +
+ +
+ ) : ( +
+ {section.alt +
+ )} +

{section.body}

+
+ ); + } + + return null; + })} +
+ ); +} \ No newline at end of file diff --git a/apps/web/src/data/newsletters.ts b/apps/web/src/data/newsletters.ts new file mode 100644 index 0000000..09d887b --- /dev/null +++ b/apps/web/src/data/newsletters.ts @@ -0,0 +1,147 @@ +export type NewsletterSection = { + type: "text" | "image" | "video" | "media"; + heading: string; + body: string; + src?: string; + alt?: string; + mediaSrc?: string; + mediaType?: "image" | "video"; + mediaAlt?: string; +}; + +export type NewsletterIssue = { + slug: string; + title: string; + summary: string; + publishedAt: string; + readTime: string; + heroMediaUrl: string; + heroMediaType: "image" | "video"; + sections: NewsletterSection[]; +}; + +export const newsletterIssues: NewsletterIssue[] = [ + { + slug: "newsletter-122-week-1", + title: "OpenSox Premium Newsletter #122 – Week 1", + summary: + "Kick-off highlights covering new OSS bounties, premium AMA recaps, and tool updates.", + publishedAt: "2025-09-28", + readTime: "6 min read", + heroMediaUrl: "/images/Screenshot-2025-10-22.png", + heroMediaType: "image", + sections: [ + { + type: "text", + heading: "Editor Note", + body: + "We welcomed 120 new premium builders and launched the bounty tracker that lets you filter rewards by language and project health.", + }, + { + type: "media", + heading: "Project Spotlight & Automation Demo", + body: + "Glance at the redesigned contribution flow for OpenSox Sheets with live lint checks. Product engineers walk through the new PR triage bot that landed merged PRs 32% faster.", + src: "/images/welcome.jpg", + alt: "Screenshot of the OpenSox Sheets contribution flow", + mediaSrc: "/videos/os-demo.mp4", + mediaType: "video", + }, + ], + }, + { + slug: "newsletter-122-week-2", + title: "OpenSox Premium Newsletter #122 – Week 2", + summary: + "Contributor success stories, funding alerts, and community playbooks from last week.", + publishedAt: "2025-10-05", + readTime: "7 min read", + heroMediaUrl: "/images/Screenshot-2025-10-22.png", + heroMediaType: "image", + sections: [ + { + type: "text", + heading: "In Case You Missed It", + body: + "Premium members unlocked three new grants and paired up through the mentor roulette. Catch all of the links in the recap.", + }, + { + type: "image", + heading: "Photo Recap", + body: + "Scenes from the Bangalore premium jam featuring lightning talks and code clinics.", + src: "https://assets.opensox.dev/newsletters/122-week2-gallery.jpg", + alt: "Developers collaborating during the OpenSox Bangalore meetup", + }, + { + type: "text", + heading: "Contributor Interview", + body: + "Sneha shares how she automated QA for 30+ repositories and dropped the test runtime by 18 minutes.", + }, + ], + }, + { + slug: "newsletter-122-week-3", + title: "OpenSox Premium Newsletter #122 – Week 3", + summary: + "Roadmap deep dives, beta launch previews, and curated resources for premium builders.", + publishedAt: "2025-10-12", + readTime: "8 min read", + heroMediaUrl: "https://assets.opensox.dev/newsletters/122-week3-hero.mp4", + heroMediaType: "video", + sections: [ + { + type: "text", + heading: "Roadmap Deep Dive", + body: + "Discover what is landing next: initiative tracker rollouts, AI pairing tools, and the observability stack for maintainers.", + }, + { + type: "video", + heading: "Beta Launch Walkthrough", + body: + "Product leads screen-share the newest workspace analytics shipping to premium members in November.", + src: "https://assets.opensox.dev/newsletters/122-week3-beta.mp4", + }, + { + type: "text", + heading: "Resource Pack", + body: + "Slide decks, RFC templates, and reusable GitHub Actions to boost review throughput.", + }, + ], + }, + { + slug: "newsletter-122-week-4", + title: "OpenSox Premium Newsletter #122 – Week 4", + summary: + "Month-in-review featuring leaderboards, upcoming events, and premium-only office hours.", + publishedAt: "2025-10-19", + readTime: "5 min read", + heroMediaUrl: "https://assets.opensox.dev/newsletters/122-week4-hero.jpg", + heroMediaType: "image", + sections: [ + { + type: "text", + heading: "Month In Review", + body: + "We wrapped with 480 merged PRs and featured maintainers who mentored the new cohort.", + }, + { + type: "image", + heading: "Contributor Leaderboard", + body: + "Celebrate the top contributors and track how premium streaks unlock extra support hours.", + src: "https://assets.opensox.dev/newsletters/122-week4-leaderboard.png", + alt: "Leaderboard graphic highlighting top contributors", + }, + { + type: "text", + heading: "Events Calendar", + body: + "RSVP for November clinics including roadmap AMAs, live pairing rooms, and design critiques.", + }, + ], + }, +]; \ No newline at end of file diff --git a/apps/web/src/types/next-auth.d.ts b/apps/web/src/types/next-auth.d.ts index 22a4535..de7edd6 100644 --- a/apps/web/src/types/next-auth.d.ts +++ b/apps/web/src/types/next-auth.d.ts @@ -10,4 +10,9 @@ declare module "next-auth/jwt" { interface JWT { jwtToken?: string; } -} \ No newline at end of file +} +declare module "react-player/lazy" { + import ReactPlayer from "react-player"; + + export default ReactPlayer; +} \ No newline at end of file diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts index 8358752..948ca30 100644 --- a/apps/web/tailwind.config.ts +++ b/apps/web/tailwind.config.ts @@ -54,7 +54,7 @@ const config: Config = { "ox-purple": "#9455f4", "ox-purple-2": "#7A45C3", "ox-gray": "rgb(75 85 99)", - "ox-white": "text-slate-400", + "ox-white": "#ffffff", "ox-black-1": "#0E0E10", "ox-black-2": "#15161A", },