diff --git a/public/assets/icons/resonate_branding_dark.svg b/public/assets/icons/resonate_branding_dark.svg new file mode 100644 index 0000000..dd00b29 --- /dev/null +++ b/public/assets/icons/resonate_branding_dark.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/icons/resonate_branding_light.svg b/public/assets/icons/resonate_branding_light.svg new file mode 100644 index 0000000..0fb8636 --- /dev/null +++ b/public/assets/icons/resonate_branding_light.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/[locale]/globals.css b/src/app/[locale]/globals.css index 842e5aa..d6b44d7 100644 --- a/src/app/[locale]/globals.css +++ b/src/app/[locale]/globals.css @@ -14,7 +14,7 @@ :root { /* Surface Colors */ --background: #F5F5F5; - --nav-background: rgba(245, 245, 245, 0.8); /* Added for glassmorphism */ + --nav-background: rgb(245, 245, 245, 0.8); /* Added for glassmorphism */ /* Text Colors */ --foreground: #1C1C1C; @@ -36,13 +36,22 @@ --button-primary-hover-bg: var(--color-neutral-50); --button-primary-hover-border: #121212; --button-subtle-bg: #9797971e; + + /* Background Tile Colors */ + --tile-bg: rgb(227, 226, 246); + --tile-stroke: rgb(80, 80, 80, 0.1); + --line-color: rgb(0, 0, 0, 0.03); + + /* Footer Colors */ + --footer-headings: rgb(0, 0, 0, 0.9); + --footer-text: rgb(0, 0, 0, 0.75); } /* DARK MODE */ .dark { /* Surface Colors */ --background: #101010; - --nav-background: rgba(16, 16, 16, 0.8); /* Added for glassmorphism */ + --nav-background: rgb(16, 16, 16, 0.8); /* Added for glassmorphism */ /* Text Colors */ --foreground: #D3D3D3; @@ -64,6 +73,15 @@ --button-primary-hover-bg: var(--color-neutral-100); --button-primary-hover-border: var(--color-neutral-100); --button-subtle-bg: #bbbbbb0e; + + /* Background Tile Colors */ + --tile-bg: rgb(30, 29, 28); + --tile-stroke: rgb(80, 80, 80, 0.5); + --line-color: rgb(0, 0, 0, 0.12); + + /* Footer Colors */ + --footer-headings: rgb(255, 255, 255); + --footer-text: rgb(255, 255, 255, 0.5); } /* --- GLOBAL STYLES --- */ @@ -96,5 +114,4 @@ body { /* Dark Mode: Icon flips to White */ :is(.dark .theme-icon) { filter: invert(1) brightness(100); -} - +} \ No newline at end of file diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index 67f2d5a..3fd359a 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useTranslations } from 'next-intl'; +import Footer from '@/components/ui/Footer'; import dynamic from 'next/dynamic'; @@ -12,9 +13,17 @@ import dynamic from 'next/dynamic'; export default function Home() { const t = useTranslations('Home'); return ( -
-

{t('title')}

-

{t('subtitle')}

+
+ {/* Main content */} +
+

{t('title')}

+

{t('subtitle')}

+
+ + {/* Footer*/} +
+
+
); } diff --git a/src/components/ui/BackgroundTile.tsx b/src/components/ui/BackgroundTile.tsx new file mode 100644 index 0000000..80a4e48 --- /dev/null +++ b/src/components/ui/BackgroundTile.tsx @@ -0,0 +1,249 @@ +"use client"; + +import { useEffect, useRef } from "react"; + +// Use this component in below shown way and adjust the height according to your use case. +/* +
+ +
+*/ + +// Stable random values generated once at module level +const PHASES = Array.from({ length: 6 }, () => Math.random() * Math.PI * 2); +const FREQS = Array.from({ length: 6 }, (_, i) => 0.3 + i * 0.13); + +// ── SPEED CONFIGURATION ─────────────────────────────────────────── +// Change this! 0.5 is subtle, 5.0 is incredibly fast. +const TRANSLATION_SPEED = 0.75; + +// Vertical squiggling speed +const UNDULATION_SPEED = 0.055; +// ────────────────────────────────────────────────────────────────── + +const BackgroundTile = () => { + const canvasRef = useRef(null); + const rafRef = useRef(null); + const lineColorRef = useRef(""); + const isRunning = useRef(false); + const timeRef = useRef(0); + const ampRef = useRef(0); + const ampSmoothed = useRef(0); + + // Forward translation trackers + const xOffsetRef = useRef(0); + const targetXSpeed = useRef(0); + const currentXSpeed = useRef(0); + + const stopTimer = useRef | null>(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const updateLineColor = () => { + lineColorRef.current = getComputedStyle(document.documentElement) + .getPropertyValue("--line-color") + .trim(); + }; + updateLineColor(); + + const themeObserver = new MutationObserver((mutations) => { + for (const m of mutations) { + if (m.attributeName === "class" || m.attributeName === "data-theme") { + updateLineColor(); + } + } + }); + themeObserver.observe(document.documentElement, { attributes: true }); + + const setSize = () => { + const dpr = window.devicePixelRatio || 1; + canvas.width = canvas.offsetWidth * dpr; + canvas.height = canvas.offsetHeight * dpr; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + }; + setSize(); + + const render = () => { + const w = canvas.offsetWidth; + const h = canvas.offsetHeight; + const amp = ampSmoothed.current; + const t = timeRef.current; + + ctx.clearRect(0, 0, w, h); + + const N = 6; + const spacing = w / (N - 1); + const patternWidth = N * spacing; + + const xOffset = ((xOffsetRef.current % patternWidth) + patternWidth) % patternWidth; + + const pts = []; + for (let i = -N * 2; i <= N * 2; i++) { + const idx = ((i % N) + N) % N; + const w1 = Math.sin(t * FREQS[idx] + PHASES[idx]) * 0.50; + const w2 = Math.sin(t * FREQS[idx] * 0.43 + PHASES[idx] * 1.77) * 0.30; + const w3 = Math.cos(t * FREQS[idx] * 0.27 + PHASES[idx] * 0.85) * 0.20; + + pts.push({ + x: i * spacing + xOffset, + y: h * 0.5 + (w1 + w2 + w3) * h * amp * 0.95, + }); + } + + ctx.beginPath(); + ctx.moveTo(pts[0].x, pts[0].y); + + for (let i = 0; i < pts.length - 1; i++) { + const p0 = pts[Math.max(0, i - 1)]; + const p1 = pts[i]; + const p2 = pts[i + 1]; + const p3 = pts[Math.min(pts.length - 1, i + 2)]; + const k = 0.5; + ctx.bezierCurveTo( + p1.x + (p2.x - p0.x) * k / 3, + p1.y + (p2.y - p0.y) * k / 3, + p2.x - (p3.x - p1.x) * k / 3, + p2.y - (p3.y - p1.y) * k / 3, + p2.x, p2.y, + ); + } + + const gradient = ctx.createLinearGradient(0, 0, w, 0); + gradient.addColorStop(0, "transparent"); + gradient.addColorStop(0.12, lineColorRef.current); + gradient.addColorStop(0.88, lineColorRef.current); + gradient.addColorStop(1, "transparent"); + + ctx.strokeStyle = gradient; + ctx.lineWidth = 14; + ctx.lineJoin = "round"; + ctx.lineCap = "round"; + ctx.stroke(); + }; + + render(); + + const ro = new ResizeObserver(() => { setSize(); render(); }); + ro.observe(canvas); + + const tick = () => { + // Lerp toward target, then decay target — order matters + currentXSpeed.current += (targetXSpeed.current - currentXSpeed.current) * 0.15; + targetXSpeed.current *= 0.85; + xOffsetRef.current += currentXSpeed.current; + + const targetAmp = ampRef.current; + + if (!isRunning.current) { + ampSmoothed.current += (targetAmp - ampSmoothed.current) * 0.18; + + if (Math.abs(ampSmoothed.current - targetAmp) > 0.001 || Math.abs(currentXSpeed.current) > 0.01) { + render(); + rafRef.current = requestAnimationFrame(tick); + } else { + ampSmoothed.current = targetAmp; + render(); + rafRef.current = null; + } + return; + } + + timeRef.current += UNDULATION_SPEED; + ampSmoothed.current += (targetAmp - ampSmoothed.current) * 0.18; + + render(); + rafRef.current = requestAnimationFrame(tick); + }; + + const startLoop = () => { + if (!isRunning.current) { + isRunning.current = true; + if (rafRef.current === null) rafRef.current = requestAnimationFrame(tick); + } + }; + + const stopLoop = () => { + isRunning.current = false; + }; + + let lastWheelTime = 0; + let lastScrollY = typeof window !== "undefined" ? window.scrollY : 0; + + const triggerScroll = (amp: number) => { + ampRef.current = amp; + startLoop(); + if (stopTimer.current) clearTimeout(stopTimer.current); + stopTimer.current = setTimeout(stopLoop, 80); + }; + + const onWheel = (e: WheelEvent) => { + lastWheelTime = Date.now(); + const vel = Math.min(Math.abs(e.deltaY) / 60, 1); + triggerScroll(0.18 + vel * 0.74); + + // Direct, readable speed — TRANSLATION_SPEED is 1:1 with px/frame + targetXSpeed.current = Math.sign(e.deltaY) * vel * TRANSLATION_SPEED * 8; + + if (rafRef.current === null) rafRef.current = requestAnimationFrame(tick); + }; + + const onScroll = () => { + const currentScrollY = window.scrollY; + const deltaY = currentScrollY - lastScrollY; + lastScrollY = currentScrollY; + + if (Math.abs(deltaY) > 0) { + targetXSpeed.current = Math.sign(deltaY) * Math.min(Math.abs(deltaY) / 60, 1) * TRANSLATION_SPEED * 8; + if (rafRef.current === null) rafRef.current = requestAnimationFrame(tick); + } + + if (Date.now() - lastWheelTime < 100) return; + triggerScroll(0.55); + }; + + window.addEventListener("wheel", onWheel, { passive: true }); + window.addEventListener("scroll", onScroll, { passive: true }); + + return () => { + stopLoop(); + ro.disconnect(); + themeObserver.disconnect(); + if (rafRef.current) cancelAnimationFrame(rafRef.current); + if (stopTimer.current) clearTimeout(stopTimer.current); + window.removeEventListener("wheel", onWheel); + window.removeEventListener("scroll", onScroll); + }; + }, []); + + return ( +
+ +
+ ); +}; + +export default BackgroundTile; \ No newline at end of file diff --git a/src/components/ui/Footer.tsx b/src/components/ui/Footer.tsx new file mode 100644 index 0000000..6376958 --- /dev/null +++ b/src/components/ui/Footer.tsx @@ -0,0 +1,110 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import BackgroundTile from "./BackgroundTile"; +import Image from "next/image"; +import { useTheme } from "next-themes"; +import { useTranslations } from "next-intl"; + +// Defined structure with translation keys and actual URLs +const NAV_CONFIG = [ + { + titleKey: "Resources.title", + links: [ + { labelKey: "Resources.links.website", href: "https://github.com/AOSSIE-Org/Resonate-Website" }, + { labelKey: "Resources.links.app", href: "https://github.com/AOSSIE-Org/Resonate" }, + { labelKey: "Resources.links.backend", href: "https://github.com/AOSSIE-Org/Resonate-Backend" }, + ], + }, + { + titleKey: "AboutUs.title", + links: [ + { labelKey: "AboutUs.links.aossie", href: "https://aossie.org" }, + { labelKey: "AboutUs.links.otherProjects", href: "https://aossie.org/projects" }, + ], + }, + { + titleKey: "Social.title", + links: [ + { labelKey: "Social.links.discord", href: "https://discord.gg/hjUhu33uAn" }, + { labelKey: "Social.links.youtube", href: "https://www.youtube.com/@AOSSIE-Org" }, + { labelKey: "Social.links.twitter", href: "https://twitter.com/aossie_org" }, + { labelKey: "Social.links.linkedin", href: "https://www.linkedin.com/company/aossie/" }, + ], + }, +]; + +const Footer = () => { + const { theme } = useTheme(); + const [mounted, setMounted] = useState(false); + const t = useTranslations("Footer"); + + useEffect(() => { + setMounted(true); + }, []); + + const brandingSrc = + !mounted + ? "/assets/icons/resonate_branding_light.svg" + : theme === "dark" + ? "/assets/icons/resonate_branding_dark.svg" + : "/assets/icons/resonate_branding_light.svg"; + + return ( +
+ {/* Background wrapper */} +
+
+ +
+ + {/* Footer content */} +
+ + {/* Brand */} + Resonate Branding + + {/* Nav columns */} + +
+
+ + {/* Bottom bar */} +
+
{t("copyright")}
+
{t("madeWith")}
+
+
+ ); +}; + +export default Footer; \ No newline at end of file diff --git a/src/messages/en.json b/src/messages/en.json index 9cdaa80..c2ccb97 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -2,5 +2,33 @@ "Home": { "title": "Resonate Website", "subtitle": "Visual Overhaul in Progress..." + }, + "Footer": { + "Resources": { + "title": "Resources", + "links": { + "website": "Resonate Website", + "app": "Resonate App", + "backend": "Resonate Backend" + } + }, + "AboutUs": { + "title": "About Us", + "links": { + "aossie": "AOSSIE", + "otherProjects": "Other Projects" + } + }, + "Social": { + "title": "Social", + "links": { + "discord": "Discord", + "youtube": "YouTube", + "twitter": "X (Twitter)", + "linkedin": "LinkedIn" + } + }, + "copyright": "© 2026 Resonate", + "madeWith": "Made with 🤍 by AOSSIE Community" } -} +} \ No newline at end of file diff --git a/src/messages/hi.json b/src/messages/hi.json index eadaa63..c8a6975 100644 --- a/src/messages/hi.json +++ b/src/messages/hi.json @@ -2,5 +2,33 @@ "Home": { "title": "रेज़ोनेट वेबसाइट", "subtitle": "विज़ुअल बदलाव पर काम चल रहा है..." + }, + "Footer": { + "Resources": { + "title": "संसाधन", + "links": { + "website": "रेज़ोनेट वेबसाइट", + "app": "रेज़ोनेट ऐप", + "backend": "रेज़ोनेट बैकएंड" + } + }, + "AboutUs": { + "title": "हमारे बारे में", + "links": { + "aossie": "AOSSIE (ओसी)", + "otherProjects": "अन्य प्रोजेक्ट्स" + } + }, + "Social": { + "title": "सोशल", + "links": { + "discord": "डिस्कॉर्ड", + "youtube": "यूट्यूब", + "twitter": "एक्स (ट्विटर)", + "linkedin": "लिंक्डइन" + } + }, + "copyright": "© 2026 रेज़ोनेट", + "madeWith": "AOSSIE समुदाय द्वारा 🤍 से निर्मित" } -} +} \ No newline at end of file