From 9663a18360206094e397a1bc43a4f2863500ef89 Mon Sep 17 00:00:00 2001 From: praveenzsp Date: Sun, 23 Nov 2025 23:21:16 +0530 Subject: [PATCH 01/21] added back navbar to layout file --- apps/web/src/app/(main)/(landing)/layout.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/src/app/(main)/(landing)/layout.tsx b/apps/web/src/app/(main)/(landing)/layout.tsx index 1ca3e604..600f2a89 100644 --- a/apps/web/src/app/(main)/(landing)/layout.tsx +++ b/apps/web/src/app/(main)/(landing)/layout.tsx @@ -1,8 +1,10 @@ import React from 'react' +import Navbar from '@/components/landing-sections/navbar' const Layout = ({ children }: { children: React.ReactNode }) => { return (
+ {children}
) From 605875aaca18a1579e67c82d64a836904e9c6eb9 Mon Sep 17 00:00:00 2001 From: praveenzsp Date: Sun, 23 Nov 2025 23:22:43 +0530 Subject: [PATCH 02/21] lint fix --- apps/web/src/app/(main)/(landing)/layout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/(main)/(landing)/layout.tsx b/apps/web/src/app/(main)/(landing)/layout.tsx index 600f2a89..3a5bdbe5 100644 --- a/apps/web/src/app/(main)/(landing)/layout.tsx +++ b/apps/web/src/app/(main)/(landing)/layout.tsx @@ -1,10 +1,10 @@ -import React from 'react' import Navbar from '@/components/landing-sections/navbar' +import React from 'react' const Layout = ({ children }: { children: React.ReactNode }) => { return (
- + {children}
) From d7dcb6f000878b590e9db4f968fb445333aa1d5b Mon Sep 17 00:00:00 2001 From: Lucifer-0612 Date: Sat, 22 Nov 2025 20:15:53 +0530 Subject: [PATCH 03/21] fix: typos in blog titles --- apps/web/src/data/blogs.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/src/data/blogs.ts b/apps/web/src/data/blogs.ts index c41a087d..18799681 100644 --- a/apps/web/src/data/blogs.ts +++ b/apps/web/src/data/blogs.ts @@ -51,25 +51,25 @@ export const blogs: BlogPost[] = [ }, { date: "02-12-23", - linkText: "why you should do open source?", + linkText: "Why you should do open source?", link: "https://x.com/ajeetunc/status/1987490955298230369?s=20", tag: "engineering", }, { date: "10-11-25", - linkText: "ugly execution wins", + linkText: "Ugly execution wins", link: "https://x.com/ajeetunc/status/1987931607102341182?s=20", tag: "misc", }, { date: "08-11-25", - linkText: "why you shouldn't register a company?", + linkText: "Why you shouldn't register a company?", link: "https://x.com/ajeetunc/status/1987125877985968217?s=20", tag: "startup", }, { date: "08-11-25", - linkText: "tiny habits that changed my life", + linkText: "Tiny habits that changed my life", link: "https://x.com/ajeetunc/status/1987043154974154762?s=20", tag: "misc", }, @@ -81,7 +81,7 @@ export const blogs: BlogPost[] = [ }, { date: "16-11-25", - linkText: "snapshot of my life so far", + linkText: "Snapshot of my life so far", link: "https://x.com/ajeetunc/status/1989355142081065468?s=20", tag: "misc", }, From f7d8ba3844025588b869258bf1eec316d1f82a3b Mon Sep 17 00:00:00 2001 From: Lucifer-0612 Date: Mon, 24 Nov 2025 15:59:52 +0530 Subject: [PATCH 04/21] fix: normalize blog titles to lowercase --- apps/web/src/data/blogs.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/src/data/blogs.ts b/apps/web/src/data/blogs.ts index 18799681..c41a087d 100644 --- a/apps/web/src/data/blogs.ts +++ b/apps/web/src/data/blogs.ts @@ -51,25 +51,25 @@ export const blogs: BlogPost[] = [ }, { date: "02-12-23", - linkText: "Why you should do open source?", + linkText: "why you should do open source?", link: "https://x.com/ajeetunc/status/1987490955298230369?s=20", tag: "engineering", }, { date: "10-11-25", - linkText: "Ugly execution wins", + linkText: "ugly execution wins", link: "https://x.com/ajeetunc/status/1987931607102341182?s=20", tag: "misc", }, { date: "08-11-25", - linkText: "Why you shouldn't register a company?", + linkText: "why you shouldn't register a company?", link: "https://x.com/ajeetunc/status/1987125877985968217?s=20", tag: "startup", }, { date: "08-11-25", - linkText: "Tiny habits that changed my life", + linkText: "tiny habits that changed my life", link: "https://x.com/ajeetunc/status/1987043154974154762?s=20", tag: "misc", }, @@ -81,7 +81,7 @@ export const blogs: BlogPost[] = [ }, { date: "16-11-25", - linkText: "Snapshot of my life so far", + linkText: "snapshot of my life so far", link: "https://x.com/ajeetunc/status/1989355142081065468?s=20", tag: "misc", }, From b1e0fd8dbeb809f520239f33d7110b6a23c711b4 Mon Sep 17 00:00:00 2001 From: apsinghdev Date: Sun, 30 Nov 2025 21:50:00 +0530 Subject: [PATCH 05/21] chore: extend offer --- apps/web/src/app/(main)/(landing)/pricing/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/(main)/(landing)/pricing/page.tsx b/apps/web/src/app/(main)/(landing)/pricing/page.tsx index 02892299..f925a710 100644 --- a/apps/web/src/app/(main)/(landing)/pricing/page.tsx +++ b/apps/web/src/app/(main)/(landing)/pricing/page.tsx @@ -496,7 +496,7 @@ const SecondaryPricingCard = ({ callbackUrl }: { callbackUrl: string }) => {

(~ ₹4,410 INR)

- Discounted till 30 December + Discounted till 10 January
From 34b63be6fa286f985e98e284fb2bf6f753e1de0e Mon Sep 17 00:00:00 2001 From: Aman Raj <113578582+huamanraj@users.noreply.github.com> Date: Tue, 25 Nov 2025 23:25:07 +0530 Subject: [PATCH 06/21] feat: OSS programs added with data --- apps/web/package.json | 1 + .../dashboard/oss-programs/[slug]/page.tsx | 3 - .../oss-programs/[slug]/program-styles.css | 2 +- .../(main)/dashboard/oss-programs/page.tsx | 1 - .../components/oss-programs/ProgramCard.tsx | 1 - apps/web/src/data/oss-programs/index.ts | 3 - pnpm-lock.yaml | 213 ++++++++++++++++++ 7 files changed, 215 insertions(+), 9 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index c302d2b6..03a03a62 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -28,6 +28,7 @@ "framer-motion": "^11.15.0", "geist": "^1.5.1", "gray-matter": "^4.0.3", + "jsdom": "^27.2.0", "lucide-react": "^0.456.0", "marked": "^17.0.0", "next": "16.0.10", diff --git a/apps/web/src/app/(main)/dashboard/oss-programs/[slug]/page.tsx b/apps/web/src/app/(main)/dashboard/oss-programs/[slug]/page.tsx index 24371621..91f4f18e 100644 --- a/apps/web/src/app/(main)/dashboard/oss-programs/[slug]/page.tsx +++ b/apps/web/src/app/(main)/dashboard/oss-programs/[slug]/page.tsx @@ -79,15 +79,12 @@ export default async function ProgramPage({ params: Promise<{ slug: string }>; }) { const { slug } = await params; - - // Lazy load only the program we need const program = await getProgramBySlug(slug); if (!program) { notFound(); } - // Pre-render all markdown sections at build time (server-side) const sectionsWithHtml = program.sections.map((section) => ({ ...section, contentHtml: renderMarkdown(section.bodyMarkdown), diff --git a/apps/web/src/app/(main)/dashboard/oss-programs/[slug]/program-styles.css b/apps/web/src/app/(main)/dashboard/oss-programs/[slug]/program-styles.css index 4dc629c2..435fead9 100644 --- a/apps/web/src/app/(main)/dashboard/oss-programs/[slug]/program-styles.css +++ b/apps/web/src/app/(main)/dashboard/oss-programs/[slug]/program-styles.css @@ -163,4 +163,4 @@ /* If Duration/Stipend are emphasized text at start of lines */ .what-section p strong { color: white; -} \ No newline at end of file +} diff --git a/apps/web/src/app/(main)/dashboard/oss-programs/page.tsx b/apps/web/src/app/(main)/dashboard/oss-programs/page.tsx index 038cda78..383a05d8 100644 --- a/apps/web/src/app/(main)/dashboard/oss-programs/page.tsx +++ b/apps/web/src/app/(main)/dashboard/oss-programs/page.tsx @@ -4,7 +4,6 @@ import ProgramsList from "./ProgramsList"; export const revalidate = 3600; export default async function Page() { - // load programs and tags in parallel on server const [programs, tags] = await Promise.all([loadAllPrograms(), getAllTags()]); return ; diff --git a/apps/web/src/components/oss-programs/ProgramCard.tsx b/apps/web/src/components/oss-programs/ProgramCard.tsx index 9fa5e065..3205e049 100644 --- a/apps/web/src/components/oss-programs/ProgramCard.tsx +++ b/apps/web/src/components/oss-programs/ProgramCard.tsx @@ -42,5 +42,4 @@ function ProgramCard({ program }: ProgramCardProps) { ); } -// Memoize to prevent unnecessary re-renders during filtering/searching export default React.memo(ProgramCard); diff --git a/apps/web/src/data/oss-programs/index.ts b/apps/web/src/data/oss-programs/index.ts index 841a6fd2..815fe1d1 100644 --- a/apps/web/src/data/oss-programs/index.ts +++ b/apps/web/src/data/oss-programs/index.ts @@ -1,8 +1,6 @@ -// src/data/oss-programs/index.ts import { cache } from "react"; import type { Program } from "./types"; -// Lazy program loaders - reduces initial bundle and parsing time const programLoaders: Record Promise<{ default?: Program } & Record>> = { "google-summer-of-code": () => import("./programs/google-summer-of-code"), "outreachy": () => import("./programs/outreachy"), @@ -108,5 +106,4 @@ async function getAllTagsImpl(): Promise { } } -// Wrap with React cache() for request-scoped memoization export const getAllTags = cache(getAllTagsImpl); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3aa88864..774cce39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,6 +184,9 @@ importers: gray-matter: specifier: ^4.0.3 version: 4.0.3 + jsdom: + specifier: ^27.2.0 + version: 27.2.0 lucide-react: specifier: ^0.456.0 version: 0.456.0(react@18.3.1) @@ -338,6 +341,9 @@ importers: packages: + '@acemir/cssom@0.9.24': + resolution: {integrity: sha512-5YjgMmAiT2rjJZU7XK1SNI7iqTy92DpaYVgG6x63FxkJ11UpYfLndHJATtinWJClAXiOlW9XWaUyAQf8pMrQPg==} + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -346,6 +352,15 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@asamuzakjp/css-color@4.1.0': + resolution: {integrity: sha512-9xiBAtLn4aNsa4mDnpovJvBn72tNEIACyvlqaNJ+ADemR+yeMJWnBudOi2qGDviJa7SwcDOU/TRh5dnET7qk0w==} + + '@asamuzakjp/dom-selector@6.7.4': + resolution: {integrity: sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -2044,6 +2059,9 @@ packages: resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} engines: {node: '>=10.0.0'} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -2262,6 +2280,10 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -2277,6 +2299,10 @@ packages: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} + data-urls@6.0.0: + resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} + engines: {node: '>=20'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -3160,6 +3186,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -3422,6 +3452,15 @@ packages: jsbn@1.1.0: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + jsdom@27.2.0: + resolution: {integrity: sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@0.5.0: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true @@ -3554,6 +3593,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.2: + resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -3585,6 +3628,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -4193,6 +4239,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + require-package-name@2.0.1: resolution: {integrity: sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==} @@ -4554,6 +4604,9 @@ packages: peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.11.11: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4601,6 +4654,13 @@ packages: title-case@2.1.1: resolution: {integrity: sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==} + tldts-core@7.0.19: + resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} + + tldts@7.0.19: + resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} + hasBin: true + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -4613,9 +4673,17 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -4833,6 +4901,10 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -4842,6 +4914,22 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@8.0.0: + resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} + engines: {node: '>=20'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -4892,6 +4980,25 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -4959,6 +5066,8 @@ packages: snapshots: + '@acemir/cssom@0.9.24': {} + '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': @@ -4966,6 +5075,24 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 + '@asamuzakjp/css-color@4.1.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 11.2.2 + + '@asamuzakjp/dom-selector@6.7.4': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.2 + + '@asamuzakjp/nwsapi@2.3.9': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -6677,6 +6804,10 @@ snapshots: basic-ftp@5.0.5: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + binary-extensions@2.3.0: {} bl@4.1.0: @@ -6922,6 +7053,11 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + cssesc@3.0.0: {} csstype@3.2.3: {} @@ -6930,6 +7066,11 @@ snapshots: data-uri-to-buffer@6.0.2: {} + data-urls@6.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -8156,6 +8297,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -8424,6 +8569,33 @@ snapshots: jsbn@1.1.0: {} + jsdom@27.2.0: + dependencies: + '@acemir/cssom': 0.9.24 + '@asamuzakjp/dom-selector': 6.7.4 + cssstyle: 5.3.3 + data-urls: 6.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + jsesc@0.5.0: {} jsesc@3.1.0: {} @@ -8549,6 +8721,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.2.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -8573,6 +8747,8 @@ snapshots: math-intrinsics@1.1.0: {} + mdn-data@2.12.2: {} + media-typer@0.3.0: {} merge-descriptors@1.0.3: {} @@ -9219,6 +9395,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + require-package-name@2.0.1: {} resolve-dir@1.0.1: @@ -9658,6 +9836,8 @@ snapshots: react: 18.3.1 use-sync-external-store: 1.5.0(react@18.3.1) + symbol-tree@3.2.4: {} + synckit@0.11.11: dependencies: '@pkgr/core': 0.2.9 @@ -9729,6 +9909,12 @@ snapshots: no-case: 2.3.2 upper-case: 1.1.3 + tldts-core@7.0.19: {} + + tldts@7.0.19: + dependencies: + tldts-core: 7.0.19 + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -9739,6 +9925,10 @@ snapshots: toidentifier@1.0.1: {} + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.19 + tr46@0.0.3: {} ts-api-utils@1.4.3(typescript@5.9.3): @@ -9970,6 +10160,10 @@ snapshots: vary@1.1.2: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + wcwidth@1.0.1: dependencies: defaults: 1.0.4 @@ -9978,6 +10172,19 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@8.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.0 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -10056,6 +10263,12 @@ snapshots: wrappy@1.0.2: {} + ws@8.18.3: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + y18n@5.0.8: {} yallist@3.1.1: {} From 0563959479a822faaf8060f6e4d0860b88645f4a Mon Sep 17 00:00:00 2001 From: apsinghdev Date: Sat, 29 Nov 2025 14:58:45 +0530 Subject: [PATCH 07/21] fix: fix jsdom esmodule requirement err --- apps/web/package.json | 2 +- pnpm-lock.yaml | 213 ------------------------------------------ 2 files changed, 1 insertion(+), 214 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 03a03a62..86924687 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -28,7 +28,7 @@ "framer-motion": "^11.15.0", "geist": "^1.5.1", "gray-matter": "^4.0.3", - "jsdom": "^27.2.0", + "sanitize-html": "^2.11.0", "lucide-react": "^0.456.0", "marked": "^17.0.0", "next": "16.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 774cce39..3aa88864 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,9 +184,6 @@ importers: gray-matter: specifier: ^4.0.3 version: 4.0.3 - jsdom: - specifier: ^27.2.0 - version: 27.2.0 lucide-react: specifier: ^0.456.0 version: 0.456.0(react@18.3.1) @@ -341,9 +338,6 @@ importers: packages: - '@acemir/cssom@0.9.24': - resolution: {integrity: sha512-5YjgMmAiT2rjJZU7XK1SNI7iqTy92DpaYVgG6x63FxkJ11UpYfLndHJATtinWJClAXiOlW9XWaUyAQf8pMrQPg==} - '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -352,15 +346,6 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@asamuzakjp/css-color@4.1.0': - resolution: {integrity: sha512-9xiBAtLn4aNsa4mDnpovJvBn72tNEIACyvlqaNJ+ADemR+yeMJWnBudOi2qGDviJa7SwcDOU/TRh5dnET7qk0w==} - - '@asamuzakjp/dom-selector@6.7.4': - resolution: {integrity: sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==} - - '@asamuzakjp/nwsapi@2.3.9': - resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} - '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -2059,9 +2044,6 @@ packages: resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} engines: {node: '>=10.0.0'} - bidi-js@1.0.3: - resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} - binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -2280,10 +2262,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - css-tree@3.1.0: - resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -2299,10 +2277,6 @@ packages: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} - data-urls@6.0.0: - resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} - engines: {node: '>=20'} - data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -3186,10 +3160,6 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -3452,15 +3422,6 @@ packages: jsbn@1.1.0: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} - jsdom@27.2.0: - resolution: {integrity: sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - peerDependencies: - canvas: ^3.0.0 - peerDependenciesMeta: - canvas: - optional: true - jsesc@0.5.0: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true @@ -3593,10 +3554,6 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.2: - resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} - engines: {node: 20 || >=22} - lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -3628,9 +3585,6 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - mdn-data@2.12.2: - resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} - media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -4239,10 +4193,6 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - require-package-name@2.0.1: resolution: {integrity: sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==} @@ -4604,9 +4554,6 @@ packages: peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - symbol-tree@3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - synckit@0.11.11: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4654,13 +4601,6 @@ packages: title-case@2.1.1: resolution: {integrity: sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==} - tldts-core@7.0.19: - resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} - - tldts@7.0.19: - resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} - hasBin: true - tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -4673,17 +4613,9 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - tough-cookie@6.0.0: - resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} - engines: {node: '>=16'} - tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@6.0.0: - resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} - engines: {node: '>=20'} - ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -4901,10 +4833,6 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - w3c-xmlserializer@5.0.0: - resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} - engines: {node: '>=18'} - wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -4914,22 +4842,6 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@8.0.0: - resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} - engines: {node: '>=20'} - - whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} - - whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} - - whatwg-url@15.1.0: - resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} - engines: {node: '>=20'} - whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -4980,25 +4892,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - xml-name-validator@5.0.0: - resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} - engines: {node: '>=18'} - - xmlchars@2.2.0: - resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -5066,8 +4959,6 @@ packages: snapshots: - '@acemir/cssom@0.9.24': {} - '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': @@ -5075,24 +4966,6 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@asamuzakjp/css-color@4.1.0': - dependencies: - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 - lru-cache: 11.2.2 - - '@asamuzakjp/dom-selector@6.7.4': - dependencies: - '@asamuzakjp/nwsapi': 2.3.9 - bidi-js: 1.0.3 - css-tree: 3.1.0 - is-potential-custom-element-name: 1.0.1 - lru-cache: 11.2.2 - - '@asamuzakjp/nwsapi@2.3.9': {} - '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -6804,10 +6677,6 @@ snapshots: basic-ftp@5.0.5: {} - bidi-js@1.0.3: - dependencies: - require-from-string: 2.0.2 - binary-extensions@2.3.0: {} bl@4.1.0: @@ -7053,11 +6922,6 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-tree@3.1.0: - dependencies: - mdn-data: 2.12.2 - source-map-js: 1.2.1 - cssesc@3.0.0: {} csstype@3.2.3: {} @@ -7066,11 +6930,6 @@ snapshots: data-uri-to-buffer@6.0.2: {} - data-urls@6.0.0: - dependencies: - whatwg-mimetype: 4.0.0 - whatwg-url: 15.1.0 - data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -8297,10 +8156,6 @@ snapshots: dependencies: safer-buffer: 2.1.2 - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - ieee754@1.2.1: {} ignore@5.3.2: {} @@ -8569,33 +8424,6 @@ snapshots: jsbn@1.1.0: {} - jsdom@27.2.0: - dependencies: - '@acemir/cssom': 0.9.24 - '@asamuzakjp/dom-selector': 6.7.4 - cssstyle: 5.3.3 - data-urls: 6.0.0 - decimal.js: 10.6.0 - html-encoding-sniffer: 4.0.0 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - is-potential-custom-element-name: 1.0.1 - parse5: 8.0.0 - saxes: 6.0.0 - symbol-tree: 3.2.4 - tough-cookie: 6.0.0 - w3c-xmlserializer: 5.0.0 - webidl-conversions: 8.0.0 - whatwg-encoding: 3.1.1 - whatwg-mimetype: 4.0.0 - whatwg-url: 15.1.0 - ws: 8.18.3 - xml-name-validator: 5.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - jsesc@0.5.0: {} jsesc@3.1.0: {} @@ -8721,8 +8549,6 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.2.2: {} - lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -8747,8 +8573,6 @@ snapshots: math-intrinsics@1.1.0: {} - mdn-data@2.12.2: {} - media-typer@0.3.0: {} merge-descriptors@1.0.3: {} @@ -9395,8 +9219,6 @@ snapshots: require-directory@2.1.1: {} - require-from-string@2.0.2: {} - require-package-name@2.0.1: {} resolve-dir@1.0.1: @@ -9836,8 +9658,6 @@ snapshots: react: 18.3.1 use-sync-external-store: 1.5.0(react@18.3.1) - symbol-tree@3.2.4: {} - synckit@0.11.11: dependencies: '@pkgr/core': 0.2.9 @@ -9909,12 +9729,6 @@ snapshots: no-case: 2.3.2 upper-case: 1.1.3 - tldts-core@7.0.19: {} - - tldts@7.0.19: - dependencies: - tldts-core: 7.0.19 - tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -9925,10 +9739,6 @@ snapshots: toidentifier@1.0.1: {} - tough-cookie@6.0.0: - dependencies: - tldts: 7.0.19 - tr46@0.0.3: {} ts-api-utils@1.4.3(typescript@5.9.3): @@ -10160,10 +9970,6 @@ snapshots: vary@1.1.2: {} - w3c-xmlserializer@5.0.0: - dependencies: - xml-name-validator: 5.0.0 - wcwidth@1.0.1: dependencies: defaults: 1.0.4 @@ -10172,19 +9978,6 @@ snapshots: webidl-conversions@3.0.1: {} - webidl-conversions@8.0.0: {} - - whatwg-encoding@3.1.1: - dependencies: - iconv-lite: 0.6.3 - - whatwg-mimetype@4.0.0: {} - - whatwg-url@15.1.0: - dependencies: - tr46: 6.0.0 - webidl-conversions: 8.0.0 - whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -10263,12 +10056,6 @@ snapshots: wrappy@1.0.2: {} - ws@8.18.3: {} - - xml-name-validator@5.0.0: {} - - xmlchars@2.2.0: {} - y18n@5.0.8: {} yallist@3.1.1: {} From 69a71618c380b017bcb75fb6aeb016135187a626 Mon Sep 17 00:00:00 2001 From: Aman Raj <113578582+huamanraj@users.noreply.github.com> Date: Sat, 29 Nov 2025 19:30:23 +0530 Subject: [PATCH 08/21] fix: ui repsnsiveness and design --- apps/web/package.json | 1 - .../dashboard/oss-programs/[slug]/program-styles.css | 2 +- pnpm-lock.yaml | 8 ++++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 86924687..c302d2b6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -28,7 +28,6 @@ "framer-motion": "^11.15.0", "geist": "^1.5.1", "gray-matter": "^4.0.3", - "sanitize-html": "^2.11.0", "lucide-react": "^0.456.0", "marked": "^17.0.0", "next": "16.0.10", diff --git a/apps/web/src/app/(main)/dashboard/oss-programs/[slug]/program-styles.css b/apps/web/src/app/(main)/dashboard/oss-programs/[slug]/program-styles.css index 435fead9..4dc629c2 100644 --- a/apps/web/src/app/(main)/dashboard/oss-programs/[slug]/program-styles.css +++ b/apps/web/src/app/(main)/dashboard/oss-programs/[slug]/program-styles.css @@ -163,4 +163,4 @@ /* If Duration/Stipend are emphasized text at start of lines */ .what-section p strong { color: white; -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3aa88864..28142191 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1479,6 +1479,9 @@ packages: '@types/sanitize-html@2.16.0': resolution: {integrity: sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==} + '@types/sanitize-html@2.16.0': + resolution: {integrity: sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==} + '@types/semver@7.7.0': resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} @@ -6045,6 +6048,10 @@ snapshots: dependencies: htmlparser2: 8.0.2 + '@types/sanitize-html@2.16.0': + dependencies: + htmlparser2: 8.0.2 + '@types/semver@7.7.0': {} '@types/send@0.17.5': @@ -7349,6 +7356,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1) transitivePeerDependencies: - supports-color From ee54a2d93431fcdac2f309d159b351b4bdaf92a8 Mon Sep 17 00:00:00 2001 From: Aman Raj <113578582+huamanraj@users.noreply.github.com> Date: Thu, 11 Dec 2025 21:54:26 +0530 Subject: [PATCH 09/21] feat: Implement testimonials management with Redis caching --- apps/api/.env.example | 3 + apps/api/package.json | 2 +- apps/api/prisma/schema.prisma | 12 + apps/api/src/routers/_app.ts | 2 + apps/api/src/routers/testimonial.ts | 96 ++++++ apps/api/tsconfig.json | 7 +- apps/web/next.config.js | 12 + apps/web/package.json | 3 + .../(main)/dashboard/pro/dashboard/page.tsx | 22 +- apps/web/src/app/(main)/testimonials/page.tsx | 200 +++++++++++ .../app/(main)/testimonials/submit/page.tsx | 319 ++++++++++++++++++ apps/web/src/data/testimonials.ts | 40 +++ packages/shared/package.json | 4 + packages/shared/types/index.ts | 3 +- packages/shared/types/redis.ts | 148 ++++++++ 15 files changed, 858 insertions(+), 15 deletions(-) create mode 100644 apps/api/src/routers/testimonial.ts create mode 100644 apps/web/src/app/(main)/testimonials/page.tsx create mode 100644 apps/web/src/app/(main)/testimonials/submit/page.tsx create mode 100644 apps/web/src/data/testimonials.ts create mode 100644 packages/shared/types/redis.ts diff --git a/apps/api/.env.example b/apps/api/.env.example index 12e24a40..3deafe1e 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -30,3 +30,6 @@ ZEPTOMAIL_TOKEN=zeptomail-token # key for the encryption # can be created by running this: echo "$(openssl rand -hex 32)opensox$(openssl rand -hex 16)" ENCRYPTION_KEY=encryption-key + + +REDIS_URL=redis://localhost:6379 \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index 0c6722ac..99e58cf9 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -42,4 +42,4 @@ "prisma": { "seed": "tsx prisma/seed.ts" } -} +} \ No newline at end of file diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 2bf9695f..3a6a0300 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -42,6 +42,18 @@ model User { accounts Account[] payments Payment[] subscriptions Subscription[] + testimonial Testimonial? +} + +model Testimonial { + id String @id @default(cuid()) + userId String @unique + content String + name String + avatar String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model Account { diff --git a/apps/api/src/routers/_app.ts b/apps/api/src/routers/_app.ts index 782b4361..dde653c5 100644 --- a/apps/api/src/routers/_app.ts +++ b/apps/api/src/routers/_app.ts @@ -4,6 +4,7 @@ import { userRouter } from "./user.js"; import { projectRouter } from "./projects.js"; import { authRouter } from "./auth.js"; import { paymentRouter } from "./payment.js"; +import { testimonialRouter } from "./testimonial.js"; import { z } from "zod"; const testRouter = router({ @@ -21,6 +22,7 @@ export const appRouter = router({ project: projectRouter, auth: authRouter, payment: paymentRouter, + testimonial: testimonialRouter, }); export type AppRouter = typeof appRouter; diff --git a/apps/api/src/routers/testimonial.ts b/apps/api/src/routers/testimonial.ts new file mode 100644 index 00000000..c4c0184b --- /dev/null +++ b/apps/api/src/routers/testimonial.ts @@ -0,0 +1,96 @@ +import { router, protectedProcedure, publicProcedure } from "../trpc.js"; +import { z } from "zod"; +import { userService } from "../services/user.service.js"; +import { TRPCError } from "@trpc/server"; +import { redisCache } from "@opensox/shared"; + +// Cache key for all testimonials +const TESTIMONIALS_CACHE_KEY = "testimonials:all"; +// Cache TTL: 5 minutes (300 seconds) +const TESTIMONIALS_CACHE_TTL = 300; + +export const testimonialRouter = router({ + getAll: publicProcedure.query(async ({ ctx }: any) => { + // Try to get from cache first + const cached = await redisCache.get(TESTIMONIALS_CACHE_KEY); + if (cached) { + console.log("Testimonials served from cache"); + return cached; + } + console.log("[testimonials] cache MISS", TESTIMONIALS_CACHE_KEY); + + // If not in cache, fetch from database + const testimonials = await ctx.db.prisma.testimonial.findMany({ + orderBy: { + createdAt: 'desc', + }, + }); + + // Store in cache for future requests + await redisCache.set(TESTIMONIALS_CACHE_KEY, testimonials, TESTIMONIALS_CACHE_TTL); + console.log("Testimonials fetched from database and cached"); + + return testimonials; + }), + + getMyTestimonial: protectedProcedure.query(async ({ ctx }: any) => { + const userId = ctx.user.id; + + // Check subscription + const { isPaidUser } = await userService.checkSubscriptionStatus(ctx.db.prisma, userId); + + if (!isPaidUser) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Only premium users can submit testimonials", + }); + } + + const testimonial = await ctx.db.prisma.testimonial.findUnique({ + where: { userId }, + }); + + return { + testimonial, + }; + }), + + submit: protectedProcedure + .input(z.object({ + name: z.string().min(1, "Name is required").max(40, "Name must be at most 40 characters"), + content: z.string().min(10, "Testimonial must be at least 10 characters").max(1000, "Testimonial must be at most 1000 characters"), + avatar: z.string().url("Invalid avatar URL"), + })) + .mutation(async ({ ctx, input }: any) => { + const userId = ctx.user.id; + + const { isPaidUser } = await userService.checkSubscriptionStatus(ctx.db.prisma, userId); + if (!isPaidUser) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Only premium users can submit testimonials", + }); + } + + const result = await ctx.db.prisma.testimonial.upsert({ + where: { userId }, + create: { + userId, + name: input.name, + content: input.content, + avatar: input.avatar, + }, + update: { + name: input.name, + content: input.content, + avatar: input.avatar, + } + }); + + // Invalidate cache after testimonial submission + await redisCache.del(TESTIMONIALS_CACHE_KEY); + console.log("Testimonials cache invalidated after submission"); + + return result; + }), +}); diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index aa78dc6f..e73cdc6c 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -4,7 +4,6 @@ // File Layout "rootDir": "./src", "outDir": "./dist", - // Environment Settings // See also https://aka.ms/tsconfig/module "module": "nodenext", @@ -13,16 +12,13 @@ // "lib": ["esnext"], // "types": ["node"], // and npm install -D @types/node - // Other Outputs "sourceMap": true, "declaration": true, "declarationMap": true, - // Stricter Typechecking Options "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, - // Style Options // "noImplicitReturns": true, // "noImplicitOverride": true, @@ -30,7 +26,6 @@ // "noUnusedParameters": true, // "noFallthroughCasesInSwitch": true, // "noPropertyAccessFromIndexSignature": true, - // Recommended Options "strict": true, "jsx": "react-jsx", @@ -38,7 +33,7 @@ "isolatedModules": true, "noUncheckedSideEffectImports": true, "moduleDetection": "force", - "skipLibCheck": true, + "skipLibCheck": true }, "include": [ "src/**/*" diff --git a/apps/web/next.config.js b/apps/web/next.config.js index dc6d8137..c021acb8 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -14,6 +14,18 @@ const nextConfig = { protocol: "https", hostname: "img.youtube.com", }, + { + protocol: "https", + hostname: "i.pravatar.cc", + }, + { + protocol: "https", + hostname: "picsum.photos", + }, + { + protocol: "https", + hostname: "standardcoldpressedoil.com", + }, ], }, experimental: { diff --git a/apps/web/package.json b/apps/web/package.json index c302d2b6..9ff6779e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@heroicons/react": "^2.1.5", + "@hookform/resolvers": "^5.2.2", "@opensox/shared": "workspace:*", "@radix-ui/react-accordion": "^1.2.1", "@radix-ui/react-checkbox": "^1.1.2", @@ -36,12 +37,14 @@ "posthog-js": "^1.203.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.68.0", "react-qr-code": "^2.0.18", "react-tweet": "^3.2.1", "sanitize-html": "^2.11.0", "superjson": "^2.2.5", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", + "zod": "^4.1.9", "zustand": "^5.0.1" }, "devDependencies": { diff --git a/apps/web/src/app/(main)/dashboard/pro/dashboard/page.tsx b/apps/web/src/app/(main)/dashboard/pro/dashboard/page.tsx index fae7f9ba..8bf08e4f 100644 --- a/apps/web/src/app/(main)/dashboard/pro/dashboard/page.tsx +++ b/apps/web/src/app/(main)/dashboard/pro/dashboard/page.tsx @@ -110,13 +110,21 @@ export default function ProDashboardPage() { {isPaidUser && (
- +
+ + +
{error &&

{error}

}
)} diff --git a/apps/web/src/app/(main)/testimonials/page.tsx b/apps/web/src/app/(main)/testimonials/page.tsx new file mode 100644 index 00000000..9a1570f0 --- /dev/null +++ b/apps/web/src/app/(main)/testimonials/page.tsx @@ -0,0 +1,200 @@ +"use client"; +import React, { useMemo } from "react"; +import Navbar from "@/components/landing-sections/navbar"; +import Footer from "@/components/landing-sections/footer"; +import Image from "next/image"; + +import { trpc } from "@/lib/trpc"; +import { imageTestimonials } from "@/data/testimonials"; +import { Skeleton } from "@/components/ui/skeleton"; + +type TestimonialBase = { + id: string; + type: "text" | "image"; +}; + +type TextTestimonial = TestimonialBase & { + type: "text"; + content: string; + user: { + name: string; + username?: string; // e.g. @username + avatar: string; + }; +}; + +type ImageTestimonial = TestimonialBase & { + type: "image"; + imageUrl: string; + alt: string; +}; + +type Testimonial = TextTestimonial | ImageTestimonial; + +const TestimonialCard = ({ item }: { item: Testimonial }) => { + if (item.type === "image") { + return ( +
+
+ {item.alt} +
+
+ ); + } + + return ( +
+
+
+ {item.user.name} +
+
+ + {item.user.name} + + {item.user.username && ( + + {item.user.username} + + )} +
+
+

{item.content}

+
+ ); +}; + +const TestimonialsPage = () => { + // Fetch text testimonials from tRPC + const { data: textTestimonialsData, isLoading } = + trpc.testimonial.getAll.useQuery(); + + // Combine text testimonials from backend with image testimonials from data file + const allTestimonials = useMemo(() => { + const textTestimonials: TextTestimonial[] = ( + textTestimonialsData || [] + ).map( + (t: { id: string; content: string; name: string; avatar: string }) => ({ + id: t.id, + type: "text" as const, + content: t.content, + user: { + name: t.name, + avatar: t.avatar, + }, + }) + ); + + // Interleave text and image testimonials for better visual distribution + const combined: Testimonial[] = []; + let imageIndex = 0; + + // Add text testimonials and interleave images every 2-3 items + for (let i = 0; i < textTestimonials.length; i++) { + combined.push(textTestimonials[i]); + + // Add an image every 2-3 text testimonials + if ((i + 1) % 3 === 0 && imageIndex < imageTestimonials.length) { + combined.push(imageTestimonials[imageIndex]); + imageIndex++; + } + } + + // Add any remaining image testimonials at the end + while (imageIndex < imageTestimonials.length) { + combined.push(imageTestimonials[imageIndex]); + imageIndex++; + } + + return combined; + }, [textTestimonialsData]); + + return ( +
+ + +
+ {/* Header */} +
+

+ Loved by Developers +

+

+ See what the community is saying about how Opensox is changing their + open source workflow. +

+
+ + {/* Loading State */} + {isLoading && ( +
+ {/* Text testimonial skeleton */} + {[...Array(8)].map((_, i) => ( +
+
+ +
+ + +
+
+
+ + + +
+
+ ))} + {/* Image testimonial skeleton */} + {[...Array(3)].map((_, i) => ( +
+ +
+ ))} +
+ )} + + {/* Masonry/Bento Grid */} + {!isLoading && ( +
+ {allTestimonials.map((testimonial) => ( + + ))} +
+ )} + + {/* Empty State */} + {!isLoading && allTestimonials.length === 0 && ( +
+

+ No testimonials yet. Be the first to share your experience! +

+
+ )} +
+ +
+
+
+
+ ); +}; + +export default TestimonialsPage; diff --git a/apps/web/src/app/(main)/testimonials/submit/page.tsx b/apps/web/src/app/(main)/testimonials/submit/page.tsx new file mode 100644 index 00000000..c40344c1 --- /dev/null +++ b/apps/web/src/app/(main)/testimonials/submit/page.tsx @@ -0,0 +1,319 @@ +"use client"; + +import React, { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { trpc } from "@/lib/trpc"; +import { useSubscription } from "@/hooks/useSubscription"; +import { useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Label } from "@/components/ui/label"; +import Image from "next/image"; +import Navbar from "@/components/landing-sections/navbar"; +import Footer from "@/components/landing-sections/footer"; +import { Loader2 } from "lucide-react"; +import { useSession } from "next-auth/react"; + +/** + * Schema for testimonial submission + */ +const formSchema = z.object({ + name: z + .string() + .min(1, "Name is required") + .max(40, "Name must be at most 40 characters"), + content: z + .string() + .min(10, "Testimonial must be at least 10 characters") + .max(1000, "Testimonial must be at most 1000 characters"), +}); + +type FormValues = z.infer; + +export default function SubmitTestimonialPage() { + const router = useRouter(); + const { data: session, status: sessionStatus } = useSession(); + const { isPaidUser, isLoading: isSubscriptionLoading } = useSubscription(); + + // Fetch existing testimonial data for editing + const { data, isLoading: isDataLoading } = ( + trpc as any + ).testimonial.getMyTestimonial.useQuery(undefined, { + enabled: !!isPaidUser, + retry: false, + refetchOnWindowFocus: false, + }); + + const utils = trpc.useUtils(); + + const submitMutation = (trpc as any).testimonial.submit.useMutation({ + onSuccess: async () => { + // Invalidate the testimonials cache on the frontend + await utils.testimonial.getAll.invalidate(); + // Redirect to testimonials page + router.push("/testimonials"); + }, + onError: (error: any) => { + alert("Error submitting testimonial: " + error.message); + }, + }); + + const { + register, + handleSubmit, + reset, + watch, + formState: { errors, isSubmitting }, + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + content: "", + }, + }); + + const nameValue = watch("name"); + const contentValue = watch("content"); + + const displayAvatar = data?.testimonial?.avatar || session?.user?.image; + + // Effect to populate form when data/session is available + useEffect(() => { + if (data?.testimonial) { + reset({ + name: data.testimonial.name, + content: data.testimonial.content, + }); + } else if (session?.user) { + reset({ + name: session.user.name || "", + content: "", + }); + } + }, [data, session, reset]); + + const onSubmit = (values: FormValues) => { + if (!displayAvatar) { + alert("Profile picture not found. Please log in again."); + return; + } + + submitMutation.mutate({ + ...values, + avatar: displayAvatar, + }); + }; + + // Loading State + if ( + sessionStatus === "loading" || + isSubscriptionLoading || + (isPaidUser && isDataLoading) + ) { + return ( +
+
+ +

Loading...

+
+
+ ); + } + + // Not Logged In State + if (sessionStatus === "unauthenticated") { + return ( +
+ +
+
+ 👤 +
+

Login Required

+

+ You need to be logged in to submit a testimonial. Please log in to + your account to continue. +

+
+ + +
+
+
+
+
+
+ ); + } + + // Access Denied State (Logged in but not paid) + if (!isPaidUser) { + return ( +
+ +
+
+ 🔒 +
+

Premium Feature

+

+ This feature is exclusively for premium users. Please upgrade your + plan to submit a testimonial. +

+
+ + +
+
+
+
+
+
+ ); + } + + return ( +
+ + +
+
+
+

+ Submit Testimonial +

+

+ Share your experience with the community. +

+
+ +
+
+ {/* Access Warning / Info */} + {data?.testimonial && ( +
+ You have already submitted a testimonial. Updating this form + will modify your existing testimonial. +
+ )} + + {/* Profile Picture and Display Name in Same Row */} +
+ +
+
+ {displayAvatar ? ( + Profile Picture { + (e.target as HTMLImageElement).src = + `https://i.pravatar.cc/150?u=error`; + }} + /> + ) : ( +
+ No Img +
+ )} +
+
+ +
+ {errors.name && ( +

+ {errors.name.message} +

+ )} +

+ {nameValue?.length || 0}/40 +

+
+
+
+
+ + {/* Content Field */} +
+ +