From f736f9f41f706105605b86224532e25c741dfde3 Mon Sep 17 00:00:00 2001 From: junaiddshaukat Date: Sun, 25 Oct 2026 23:52:09 +0500 Subject: [PATCH] feat(dwosi): add DWOSI landing page Add /dwosi, the landing page for the Dev Weekends Open Source Initiative, with its own header, footer and design tokens so it stands apart from the rest of the site. DSOC stays at /dsoc and is presented as the mentored program inside DWOSI. - Showcases Buildment, Pathment, Community Cloud, Inroad, GoalSlot and the Content Control Platform, with a real screenshot where one exists - Dark by default with its own light/dark toggle, independent of the site theme; a head script applies a saved choice before first paint - Living contribution-graph background on canvas that pauses off-screen and draws a still frame for reduced motion - Real proof only: published Dev Weekends numbers, plus DSOC impact stories from the database once they are approved in the admin - Built on shadcn/ui; adds the Accordion component (@radix-ui/react-accordion 1.3.1) and a can-hover Tailwind variant - Hides the global header and footer on /dwosi and adds it to the sitemap Co-Authored-By: Claude Opus 5.5 --- app/dwosi/components/browser-frame.tsx | 27 ++ app/dwosi/components/contribution-field.tsx | 235 +++++++++++ app/dwosi/components/cta.tsx | 89 ++++ app/dwosi/components/dsoc-stories.tsx | 79 ++++ app/dwosi/components/dwosi-footer.tsx | 89 ++++ app/dwosi/components/dwosi-header.tsx | 83 ++++ app/dwosi/components/dwosi-logo.tsx | 32 ++ app/dwosi/components/project-cards.tsx | 118 ++++++ app/dwosi/components/section-heading.tsx | 29 ++ app/dwosi/components/theme-toggle.tsx | 34 ++ app/dwosi/data.ts | 181 +++++++++ app/dwosi/dwosi.css | 294 ++++++++++++++ app/dwosi/layout.tsx | 18 + app/dwosi/page.tsx | 323 +++++++++++++++ app/dwosi/theme.ts | 9 + app/layout.tsx | 3 + app/sitemap.ts | 1 + components/footer.tsx | 4 +- components/site-header.tsx | 3 +- components/ui/accordion.tsx | 57 +++ package-lock.json | 423 ++++++++++++++++++++ package.json | 3 +- tailwind.config.ts | 27 +- 23 files changed, 2156 insertions(+), 5 deletions(-) create mode 100644 app/dwosi/components/browser-frame.tsx create mode 100644 app/dwosi/components/contribution-field.tsx create mode 100644 app/dwosi/components/cta.tsx create mode 100644 app/dwosi/components/dsoc-stories.tsx create mode 100644 app/dwosi/components/dwosi-footer.tsx create mode 100644 app/dwosi/components/dwosi-header.tsx create mode 100644 app/dwosi/components/dwosi-logo.tsx create mode 100644 app/dwosi/components/project-cards.tsx create mode 100644 app/dwosi/components/section-heading.tsx create mode 100644 app/dwosi/components/theme-toggle.tsx create mode 100644 app/dwosi/data.ts create mode 100644 app/dwosi/dwosi.css create mode 100644 app/dwosi/layout.tsx create mode 100644 app/dwosi/page.tsx create mode 100644 app/dwosi/theme.ts create mode 100644 components/ui/accordion.tsx diff --git a/app/dwosi/components/browser-frame.tsx b/app/dwosi/components/browser-frame.tsx new file mode 100644 index 0000000..cdefa92 --- /dev/null +++ b/app/dwosi/components/browser-frame.tsx @@ -0,0 +1,27 @@ +import type { ReactNode } from "react" + +import { cn } from "@/lib/utils" + +/** Minimal browser window used to present real product screenshots consistently. */ +export function BrowserFrame({ url, children, className }: { url?: string; children: ReactNode; className?: string }) { + return ( +
+ +
{children}
+
+ ) +} diff --git a/app/dwosi/components/contribution-field.tsx b/app/dwosi/components/contribution-field.tsx new file mode 100644 index 0000000..f9f79bb --- /dev/null +++ b/app/dwosi/components/contribution-field.tsx @@ -0,0 +1,235 @@ +"use client" + +import { useEffect, useRef } from "react" + +import { cn } from "@/lib/utils" +import { DWOSI_THEME_ATTR } from "../theme" + +/** + * A living, pixel-art contribution graph drawn on a canvas. Cells "commit" + * at random and fade out, and light up under the cursor on precise pointers. + * Quiet behind the content (the `focus` ellipse), pauses when off-screen and + * draws a single static frame for people who prefer reduced motion. + */ + +interface ContributionFieldProps { + className?: string + /** Quiet area behind the content, as fractions of the canvas size */ + focus?: { x: number; y: number; rx: number; ry: number } + /** Fade the top edge too (the bottom edge always fades) */ + fadeTop?: boolean +} + +const CELL = 10 +const GAP = 4 +const PITCH = CELL + GAP +const COMMITS_PER_CELL_PER_SECOND = 0.01 +const DECAY_SECONDS = 1.4 +const FRAME_MS = 1000 / 30 + +const smoothstep = (edge0: number, edge1: number, x: number) => { + const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0))) + return t * t * (3 - 2 * t) +} + +/** Seeded random so the resting pattern is stable between renders */ +function seededRandom(seed: number) { + return () => { + seed = (seed + 0x6d2b79f5) | 0 + let t = Math.imul(seed ^ (seed >>> 15), 1 | seed) + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t + return ((t ^ (t >>> 14)) >>> 0) / 4294967296 + } +} + +export function ContributionField({ + className, + focus = { x: 0.5, y: 0.45, rx: 0.34, ry: 0.42 }, + fadeTop = false, +}: ContributionFieldProps) { + const canvasRef = useRef(null) + const { x: focusX, y: focusY, rx: focusRx, ry: focusRy } = focus + + useEffect(() => { + const canvas = canvasRef.current + const host = canvas?.parentElement + const ctx = canvas?.getContext("2d") + if (!canvas || !host || !ctx) return + + const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches + const finePointer = window.matchMedia("(hover: hover) and (pointer: fine)").matches + + let width = 0 + let height = 0 + let cols = 0 + let rows = 0 + let originX = 0 + let quiet = new Float32Array(0) + let edge = new Float32Array(0) + let weight = new Float32Array(0) + let rest = new Float32Array(0) + let heat = new Float32Array(0) + let delay = new Float32Array(0) + let brandColor = "" + let emptyColor = "" + let emptyAlpha = 0.05 + let frame = 0 + let lastFrame = 0 + let start = performance.now() + let onScreen = true + + const readColors = () => { + const root = document.documentElement + const styles = getComputedStyle(root) + brandColor = `hsl(${styles.getPropertyValue("--brand").trim()})` + emptyColor = `hsl(${styles.getPropertyValue("--foreground").trim()})` + emptyAlpha = root.getAttribute(DWOSI_THEME_ATTR) === "light" ? 0.05 : 0.07 + } + + const layout = () => { + const rect = host.getBoundingClientRect() + width = rect.width + height = rect.height + const dpr = Math.min(window.devicePixelRatio || 1, 2) + canvas.width = Math.round(width * dpr) + canvas.height = Math.round(height * dpr) + ctx.setTransform(dpr, 0, 0, dpr, 0, 0) + + cols = Math.ceil(width / PITCH) + rows = Math.ceil(height / PITCH) + originX = (width - (cols * PITCH - GAP)) / 2 + const count = cols * rows + quiet = new Float32Array(count) + edge = new Float32Array(count) + weight = new Float32Array(count) + rest = new Float32Array(count) + heat = new Float32Array(count) + delay = new Float32Array(count) + + // On narrow screens the content spans the full width, so widen the quiet area + const rx = width < 640 ? Math.max(focusRx, 0.6) : focusRx + const random = seededRandom(cols * 7919 + rows) + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const i = row * cols + col + const x = (originX + col * PITCH + CELL / 2) / width + const y = (row * PITCH + CELL / 2) / height + const distance = Math.hypot((x - focusX) / rx, (y - focusY) / focusRy) + // `quiet` dims cells behind the content, `edge` fades the section edges to nothing + quiet[i] = 0.06 + 0.94 * smoothstep(0.6, 1.25, distance) + edge[i] = smoothstep(1, 0.6, y) * (fadeTop ? smoothstep(0, 0.3, y) : 1) + weight[i] = quiet[i] * edge[i] + + // Resting levels, like a contribution graph: mostly empty, a few busy days + const roll = random() + rest[i] = roll > 0.975 ? 0.8 : roll > 0.94 ? 0.55 : roll > 0.88 ? 0.3 : 0 + delay[i] = distance * 0.35 + random() * 0.15 + } + } + } + + const draw = (now: number) => { + const elapsed = (now - start) / 1000 + ctx.clearRect(0, 0, width, height) + + // Two passes so the fill color only changes once per frame + for (let pass = 0; pass < 2; pass++) { + ctx.fillStyle = pass === 0 ? emptyColor : brandColor + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const i = row * cols + col + const reveal = reduceMotion ? 1 : smoothstep(delay[i], delay[i] + 0.25, elapsed) + if (reveal <= 0) continue + + const level = Math.max(rest[i] * weight[i], heat[i]) + const lit = level >= 0.04 + if (lit !== (pass === 1)) continue + + ctx.globalAlpha = lit + ? (0.15 + 0.75 * level) * (0.25 + 0.75 * quiet[i]) * edge[i] * reveal + : emptyAlpha * (0.4 + 0.6 * quiet[i]) * edge[i] * reveal + ctx.fillRect(originX + col * PITCH, row * PITCH, CELL, CELL) + } + } + } + ctx.globalAlpha = 1 + } + + const step = (seconds: number) => { + const decay = Math.exp(-seconds / DECAY_SECONDS) + const chance = COMMITS_PER_CELL_PER_SECOND * seconds + for (let i = 0; i < heat.length; i++) { + heat[i] *= decay + if (Math.random() < chance * weight[i]) heat[i] = Math.max(heat[i], 0.5 + Math.random() * 0.5) + } + } + + const loop = (now: number) => { + frame = requestAnimationFrame(loop) + if (!onScreen || now - lastFrame < FRAME_MS) return + step(Math.min((now - lastFrame) / 1000, 0.1)) + lastFrame = now + draw(now) + } + + const onPointerMove = (event: PointerEvent) => { + const rect = canvas.getBoundingClientRect() + const col = Math.floor((event.clientX - rect.left - originX) / PITCH) + const row = Math.floor((event.clientY - rect.top) / PITCH) + for (let dr = -1; dr <= 1; dr++) { + for (let dc = -1; dc <= 1; dc++) { + const r = row + dr + const c = col + dc + if (r < 0 || c < 0 || r >= rows || c >= cols) continue + const i = r * cols + c + heat[i] = Math.max(heat[i], dr === 0 && dc === 0 ? 1 : dr === 0 || dc === 0 ? 0.6 : 0.35) + } + } + } + + readColors() + layout() + + const resizeObserver = new ResizeObserver(() => { + layout() + if (reduceMotion) draw(performance.now()) + }) + resizeObserver.observe(host) + + const themeObserver = new MutationObserver(() => { + readColors() + if (reduceMotion) draw(performance.now()) + }) + themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: [DWOSI_THEME_ATTR] }) + + const visibilityObserver = new IntersectionObserver(([entry]) => { + onScreen = entry.isIntersecting + }) + visibilityObserver.observe(canvas) + + if (reduceMotion) { + draw(performance.now()) + } else { + start = performance.now() + lastFrame = start + frame = requestAnimationFrame(loop) + if (finePointer) host.addEventListener("pointermove", onPointerMove) + } + + return () => { + cancelAnimationFrame(frame) + resizeObserver.disconnect() + themeObserver.disconnect() + visibilityObserver.disconnect() + host.removeEventListener("pointermove", onPointerMove) + } + }, [focusX, focusY, focusRx, focusRy, fadeTop]) + + return ( +