diff --git a/playwright.config.ts b/playwright.config.ts index ff32c13..6703aaf 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,79 +1,27 @@ import { defineConfig, devices } from "@playwright/test"; -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -// import dotenv from 'dotenv'; -// import path from 'path'; -// dotenv.config({ path: path.resolve(__dirname, '.env') }); - -/** - * See https://playwright.dev/docs/test-configuration. - */ export default defineConfig({ testDir: "./__test__", - /* Run tests in files in parallel */ fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, - /* Retry on CI only */ retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: "html", - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://localhost:3000', - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", }, - - /* Configure projects for major browsers */ projects: [ { name: "chromium", use: { ...devices["Desktop Chrome"] }, }, - { name: "firefox", use: { ...devices["Desktop Firefox"] }, }, - { name: "webkit", use: { ...devices["Desktop Safari"] }, }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, ], - - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://localhost:3000', - // reuseExistingServer: !process.env.CI, - // }, }); diff --git a/public/gallary/959256_249.jpg b/public/gallary/959256_249.jpg deleted file mode 100644 index 6f0b551..0000000 Binary files a/public/gallary/959256_249.jpg and /dev/null differ diff --git a/public/gallary/959256_249.webp b/public/gallary/959256_249.webp deleted file mode 100644 index b1c524f..0000000 Binary files a/public/gallary/959256_249.webp and /dev/null differ diff --git a/public/gallary/IMG_3108.webp b/public/gallary/IMG_3108.webp deleted file mode 100644 index e39eeee..0000000 Binary files a/public/gallary/IMG_3108.webp and /dev/null differ diff --git a/public/gallary/IMG_3161.webp b/public/gallary/IMG_3161.webp deleted file mode 100644 index 88bdaa3..0000000 Binary files a/public/gallary/IMG_3161.webp and /dev/null differ diff --git a/public/gallary/IMG_3191.webp b/public/gallary/IMG_3191.webp deleted file mode 100644 index 60c9314..0000000 Binary files a/public/gallary/IMG_3191.webp and /dev/null differ diff --git a/public/gallary/IMG_4467.webp b/public/gallary/IMG_4467.webp deleted file mode 100644 index 346dd28..0000000 Binary files a/public/gallary/IMG_4467.webp and /dev/null differ diff --git a/public/gallary/IMG_4477.webp b/public/gallary/IMG_4477.webp deleted file mode 100644 index 37bb4bc..0000000 Binary files a/public/gallary/IMG_4477.webp and /dev/null differ diff --git a/public/gallary/IMG_8150.webp b/public/gallary/IMG_8150.webp deleted file mode 100644 index 1be533b..0000000 Binary files a/public/gallary/IMG_8150.webp and /dev/null differ diff --git a/public/gallary/IMG_8335.webp b/public/gallary/IMG_8335.webp deleted file mode 100644 index 028c76b..0000000 Binary files a/public/gallary/IMG_8335.webp and /dev/null differ diff --git a/public/gallary/IMG_8413.webp b/public/gallary/IMG_8413.webp deleted file mode 100644 index 304040f..0000000 Binary files a/public/gallary/IMG_8413.webp and /dev/null differ diff --git a/public/gallary/IMG_8497.webp b/public/gallary/IMG_8497.webp deleted file mode 100644 index 61ad074..0000000 Binary files a/public/gallary/IMG_8497.webp and /dev/null differ diff --git a/public/gallary/IMG_8540.webp b/public/gallary/IMG_8540.webp deleted file mode 100644 index 84f105f..0000000 Binary files a/public/gallary/IMG_8540.webp and /dev/null differ diff --git a/public/images/404.jpg b/public/images/404.jpg deleted file mode 100644 index 1309825..0000000 Binary files a/public/images/404.jpg and /dev/null differ diff --git a/public/images/arindam.jpeg b/public/images/arindam.jpeg deleted file mode 100644 index 61ef84e..0000000 Binary files a/public/images/arindam.jpeg and /dev/null differ diff --git a/public/images/arkajit.jpeg b/public/images/arkajit.jpeg deleted file mode 100644 index f04da2c..0000000 Binary files a/public/images/arkajit.jpeg and /dev/null differ diff --git a/public/images/header.png b/public/images/header.png new file mode 100644 index 0000000..d1a4a51 Binary files /dev/null and b/public/images/header.png differ diff --git a/public/images/hero.jpeg b/public/images/hero.jpeg deleted file mode 100644 index 1289927..0000000 Binary files a/public/images/hero.jpeg and /dev/null differ diff --git a/public/images/kolkata-hero.jpg b/public/images/kolkata-hero.jpg deleted file mode 100644 index 4959055..0000000 Binary files a/public/images/kolkata-hero.jpg and /dev/null differ diff --git a/public/images/kolkata_image.jpg b/public/images/kolkata_image.jpg deleted file mode 100644 index 290fcae..0000000 Binary files a/public/images/kolkata_image.jpg and /dev/null differ diff --git a/public/images/kolkata_image_1.png b/public/images/kolkata_image_1.png deleted file mode 100644 index d2bb8c0..0000000 Binary files a/public/images/kolkata_image_1.png and /dev/null differ diff --git a/public/images/priyankar.jpeg b/public/images/priyankar.jpeg deleted file mode 100644 index e23ebce..0000000 Binary files a/public/images/priyankar.jpeg and /dev/null differ diff --git a/public/images/shyam.jpg b/public/images/shyam.jpg deleted file mode 100644 index 60794e6..0000000 Binary files a/public/images/shyam.jpg and /dev/null differ diff --git a/public/images/sujal.jpeg b/public/images/sujal.jpeg deleted file mode 100644 index f9db898..0000000 Binary files a/public/images/sujal.jpeg and /dev/null differ diff --git a/public/images/sulagna.jpeg b/public/images/sulagna.jpeg deleted file mode 100644 index 4be8e28..0000000 Binary files a/public/images/sulagna.jpeg and /dev/null differ diff --git a/public/images/tapas.jpeg b/public/images/tapas.jpeg deleted file mode 100644 index dfca5cc..0000000 Binary files a/public/images/tapas.jpeg and /dev/null differ diff --git a/public/media-kit/react-kolkata-brand-guidelines.md b/public/media-kit/react-kolkata-brand-guidelines.md new file mode 100644 index 0000000..d099d98 --- /dev/null +++ b/public/media-kit/react-kolkata-brand-guidelines.md @@ -0,0 +1,45 @@ +# React Kolkata — Brand Guidelines + +## Who we are + +React Kolkata is a community-driven hub for React developers in Kolkata. Our brand should +feel welcoming, modern, and rooted in the city we call home. Build. Learn. Connect. + +## Our logos + +All logo files can be downloaded from the Media Kit page on reactkolkata.com. + +| File | Use it when | +| ---- | ----------- | +| `react-kolkata-logo.svg` | Default use. Docs, slides, partner pages, print. Scales cleanly at any size. | +| `react-kolkata-logo-light.png` | On dark backgrounds. | +| `react-kolkata-logo-dark.png` | On light backgrounds. | +| `react-kolkata-logo-full-light.png` | Full lockup (mark + wordmark) on dark backgrounds. | +| `react-kolkata-logo-full-dark.png` | Full lockup (mark + wordmark) on light backgrounds. | +| `react-kolkata-social-profile.png` | Social media avatars and profile pictures only. | + +## How to use the logo + +- Give the logo room to breathe. Keep clear space around it at least equal to the height + of the atom mark. +- Use the light version on dark backgrounds and the dark version on light backgrounds. +- Prefer the SVG file whenever possible. It stays sharp at every size. +- The minimum width for the full lockup is 120px on screen and 30mm in print. + +## Please don't + +- Don't stretch, squash, rotate, or rearrange the logo. +- Don't change its colours, add shadows, outlines, or gradients. +- Don't place it on busy photos or backgrounds where it is hard to read. +- Don't use old, redrawn, or unofficial versions of the logo. + +## Typography + +- Display and headlines: Instrument Serif (italic for emphasis words). +- Body and UI text: Inter. +- Code and mono details: Geist Mono. + +## Questions + +Want to feature React Kolkata, sponsor an event, or partner with us? Write to us at +reactkolkata@gmail.com. diff --git a/public/media-kit/react-kolkata-logo-dark.png b/public/media-kit/react-kolkata-logo-dark.png new file mode 100644 index 0000000..a74a9b2 Binary files /dev/null and b/public/media-kit/react-kolkata-logo-dark.png differ diff --git a/public/media-kit/react-kolkata-logo-full-dark.png b/public/media-kit/react-kolkata-logo-full-dark.png new file mode 100644 index 0000000..9e3ccbc Binary files /dev/null and b/public/media-kit/react-kolkata-logo-full-dark.png differ diff --git a/public/media-kit/react-kolkata-logo-full-light.png b/public/media-kit/react-kolkata-logo-full-light.png new file mode 100644 index 0000000..4797a07 Binary files /dev/null and b/public/media-kit/react-kolkata-logo-full-light.png differ diff --git a/public/media-kit/react-kolkata-logo-light.png b/public/media-kit/react-kolkata-logo-light.png new file mode 100644 index 0000000..7512d8e Binary files /dev/null and b/public/media-kit/react-kolkata-logo-light.png differ diff --git a/public/media-kit/react-kolkata-logo.svg b/public/media-kit/react-kolkata-logo.svg new file mode 100644 index 0000000..671fd3d --- /dev/null +++ b/public/media-kit/react-kolkata-logo.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/media-kit/react-kolkata-social-profile.png b/public/media-kit/react-kolkata-social-profile.png new file mode 100644 index 0000000..d7e7331 Binary files /dev/null and b/public/media-kit/react-kolkata-social-profile.png differ diff --git a/public/partners/brave_screenshot_localhost.png b/public/partners/brave_screenshot_localhost.png deleted file mode 100644 index 00c6e0b..0000000 Binary files a/public/partners/brave_screenshot_localhost.png and /dev/null differ diff --git a/public/partners/gdg.png b/public/partners/gdg.png deleted file mode 100644 index d03e360..0000000 Binary files a/public/partners/gdg.png and /dev/null differ diff --git a/public/partners/mmkolkata logo.png b/public/partners/mmkolkata logo.png deleted file mode 100644 index 1453037..0000000 Binary files a/public/partners/mmkolkata logo.png and /dev/null differ diff --git a/public/sponsors/mindwebs.svg b/public/sponsors/mindwebs.svg new file mode 100644 index 0000000..e69c41b --- /dev/null +++ b/public/sponsors/mindwebs.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/sponsors/miro.svg b/public/sponsors/miro.svg new file mode 100644 index 0000000..581e2f3 --- /dev/null +++ b/public/sponsors/miro.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + diff --git a/public/video/poster.mp4 b/public/video/poster.mp4 new file mode 100644 index 0000000..a5ae5e4 Binary files /dev/null and b/public/video/poster.mp4 differ diff --git a/src/app/[locale]/[...notFound]/page.tsx b/src/app/[locale]/[...notFound]/page.tsx deleted file mode 100644 index 7858885..0000000 --- a/src/app/[locale]/[...notFound]/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from "react"; -import Image from "next/image"; -import Link from "next/link"; -import { useTranslations } from "next-intl"; - -const NotFound = () => { - const t = useTranslations("NotFound"); - - return ( -
- 404 background -
-

Oops!

-

- 4 - 4 -

-

- Lost in space… but don't worry, we'll guide you home. -

- - - {t("go_to_home")} - -
-
- ); -}; - -export default NotFound; diff --git a/src/app/[locale]/blog/loading.tsx b/src/app/[locale]/blog/loading.tsx index a2cabf9..9909fa6 100644 --- a/src/app/[locale]/blog/loading.tsx +++ b/src/app/[locale]/blog/loading.tsx @@ -1,26 +1,21 @@ function BlogCardSkeleton() { return ( -
- {/* Image */} -
+
+
- {/* Title */}
-
-
+
+
- {/* Meta */} -
+
- {/* Description */}
-
-
+
+
- {/* Read More Button */} -
+
); } @@ -28,13 +23,11 @@ function BlogCardSkeleton() { export default function BlogLoading() { return (
- {/* Page Header */}
-
-
+
+
- {/* Blog Grid */}
{Array.from({ length: 6 }).map((_, index) => ( diff --git a/src/app/[locale]/blog/page.tsx b/src/app/[locale]/blog/page.tsx index 9ba6160..9951e97 100644 --- a/src/app/[locale]/blog/page.tsx +++ b/src/app/[locale]/blog/page.tsx @@ -4,6 +4,7 @@ import Script from "next/script"; import { getInitialBlogs } from "@/utils/blog"; import { getTranslations } from "next-intl/server"; +import { getLocalizedPath } from "@/config/i18n/navigation"; import BlogList from "@/components/common/blog/BlogList"; import ErrorBoundary from "@/components/common/error-boundary"; @@ -22,9 +23,9 @@ export async function generateMetadata({ title: pageTitle, description: pageDescription, alternates: { - canonical: `/${locale}/blog`, + canonical: getLocalizedPath("/blog", locale), languages: { - en: "/en/blog", + en: "/blog", bn: "/bn/blog", hi: "/hi/blog", es: "/es/blog", @@ -33,7 +34,7 @@ export async function generateMetadata({ openGraph: { title: pageTitle, description: pageDescription, - url: `/${locale}/blog`, + url: getLocalizedPath("/blog", locale), siteName: "React Kolkata", locale: locale, type: "website", diff --git a/src/app/[locale]/code-of-conduct/page.tsx b/src/app/[locale]/code-of-conduct/page.tsx new file mode 100644 index 0000000..9265723 --- /dev/null +++ b/src/app/[locale]/code-of-conduct/page.tsx @@ -0,0 +1,131 @@ +import { Metadata } from "next"; + +import { getLocalizedPath } from "@/config/i18n/navigation"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + + const pageTitle = "Code of Conduct"; + const pageDescription = + "How we look out for each other at React Kolkata, at our events and online."; + + return { + title: pageTitle, + description: pageDescription, + alternates: { + canonical: getLocalizedPath("/code-of-conduct", locale), + }, + openGraph: { + title: pageTitle, + description: pageDescription, + url: getLocalizedPath("/code-of-conduct", locale), + siteName: "React Kolkata", + locale: locale, + type: "website", + }, + twitter: { + card: "summary_large_image", + title: pageTitle, + description: pageDescription, + }, + }; +} + +const sections = [ + { + heading: "The short version", + body: [ + "Be kind. That is really what it comes down to. React Kolkata exists so people can learn React, meet others, and have a good time doing it. Everyone who shows up, whether it is their first meetup or their fiftieth, should feel welcome here.", + ], + }, + { + heading: "How we treat each other", + body: [ + "We welcome people of every background, identity, and experience level. Beginners asking basic questions are just as important to this community as senior engineers giving talks.", + "Give people the benefit of the doubt, listen more than you speak, and remember that there is a human being on the other side of every conversation. A little patience goes a long way, especially with newcomers who are still finding their footing.", + ], + }, + { + heading: "What is never okay", + body: [ + "Harassment of any kind has no place here. That includes offensive comments about someone's gender, identity, appearance, or background, intimidation, unwelcome attention, and deliberately disrupting talks or discussions.", + "If someone tells you that something you said or did made them uncomfortable, please take it seriously. You do not have to agree or have a debate about it. Just stop, and move on gracefully.", + ], + }, + { + heading: "At our events", + body: [ + "Our meetups and workshops are shared spaces. Respect the speakers by listening while they talk and saving questions for the right moment. Respect the venue by cleaning up after yourself. And respect your fellow attendees by giving them space to participate too.", + "If you see someone sitting alone or looking a bit lost, say hello. Most of us remember what it felt like to walk into our first meetup, and a friendly face makes all the difference.", + ], + }, + { + heading: "In our online spaces", + body: [ + "The same rules apply on WhatsApp, Discord, GitHub, and social media. Keep discussions friendly and on topic, do not spam the groups, and do not share anyone's personal details or private messages without their permission.", + ], + }, + { + heading: "If something feels off", + body: [ + "If you experience or witness behaviour that does not sit right with you, please reach out to us at reactkolkata@gmail.com. Tell us what happened in your own words, and we will take it from there.", + "We will keep your message private and handle it with care. You will never get in trouble for raising a genuine concern.", + ], + }, + { + heading: "What happens next", + body: [ + "When a concern is reported, the organisers will look into it quietly and fairly. Depending on what happened, we might have a private word with the person involved, ask them to leave an event, or in serious cases, remove them from the community altogether.", + "Our goal is never punishment for its own sake. It is simply to keep React Kolkata a place where everyone feels safe showing up.", + ], + }, +]; + +export default function CodeOfConductPage() { + return ( +
+
+
+
+
+ +
+

+ Code of Conduct +

+

+ React Kolkata works when people feel safe asking questions, sharing unfinished ideas, + and being themselves. This page is our promise to keep it that way. +

+
+
+ +
+ {sections.map((section) => ( +
+

+ {section.heading} +

+ {section.body.map((paragraph) => ( +

+ {paragraph} +

+ ))} +
+ ))} + +
+

Thank you for being here.

+

+ Every friendly question answered, every talk given, and every newcomer welcomed is what + makes this community what it is. We are glad you are a part of it. +

+
+
+
+ ); +} diff --git a/src/app/[locale]/contributors/page.tsx b/src/app/[locale]/contributors/page.tsx index 0f09450..c6524af 100644 --- a/src/app/[locale]/contributors/page.tsx +++ b/src/app/[locale]/contributors/page.tsx @@ -2,6 +2,7 @@ import { Metadata } from "next"; import ContributorsSection from "@/modules/contributors"; import { getTranslations } from "next-intl/server"; +import { getLocalizedPath } from "@/config/i18n/navigation"; import ErrorBoundary from "@/components/common/error-boundary"; export async function generateMetadata({ @@ -21,19 +22,15 @@ export async function generateMetadata({ openGraph: { title: pageTitle, description: pageDescription, - url: `/${locale}/contributors`, + url: getLocalizedPath("/contributors", locale), siteName: "React Kolkata", locale: locale, type: "website", - // Optional: can add a specific image - // images: ['/images/contributors-og.jpg'], }, twitter: { card: "summary_large_image", title: pageTitle, description: pageDescription, - // Optional: can add a specific image - // images: ['/images/contributors-twitter.jpg'], }, }; } diff --git a/src/app/[locale]/events/events-page-client.tsx b/src/app/[locale]/events/events-page-client.tsx index ad4aa60..75b9ba0 100644 --- a/src/app/[locale]/events/events-page-client.tsx +++ b/src/app/[locale]/events/events-page-client.tsx @@ -1,178 +1,211 @@ "use client"; -import CfpCard from "@/modules/home/(sections)/event/cfp-card"; -import ChampionCard from "@/modules/home/(sections)/event/champion-card"; +import { useMemo, useState } from "react"; +import Link from "next/link"; import EventCard from "@/modules/home/(sections)/event/event-card"; -import EventCardCompact from "@/modules/home/(sections)/event/event-card-compact"; import LumaEmbed from "@/modules/home/(sections)/event/luma-embed"; import { Event, EVENT_STATUS } from "@/types/event"; -import { motion, Variants } from "framer-motion"; import { useTranslations } from "next-intl"; -import { LuCalendar } from "react-icons/lu"; +import { LuArrowUpRight, LuChevronLeft, LuChevronRight } from "react-icons/lu"; import { getEventStatus } from "@/lib/calendar-utils"; -import { FEATURED_LUMA_EVENT_ID } from "@/base/constants/event"; - -const containerVariants: Variants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { - staggerChildren: 0.1, - }, - }, -}; - -const itemVariants: Variants = { - hidden: { y: 20, opacity: 0 }, - visible: { - y: 0, - opacity: 1, - transition: { - type: "spring", - stiffness: 100, - }, - }, -}; +import { cn } from "@/lib/utils"; interface EventsPageClientProps { events: Event[]; } +const WHATSAPP_URL = "https://chat.whatsapp.com/JmCp4Za9ap0DpER0Gd4hAs"; +const LUMA_URL = "https://lu.ma/reactkolkata"; +const PAGE_SIZE = 6; + +function isLive(event: Event): boolean { + const status = getEventStatus(event.startDateTime, event.endDateTime); + return status === EVENT_STATUS.UPCOMING || status === EVENT_STATUS.ONGOING; +} + +function renderEventCard(event: Event) { + return event.id.startsWith("evt-") ? ( + + ) : ( + + ); +} + +function getVisiblePages(page: number, totalPages: number): number[] { + if (totalPages <= 7) { + return Array.from({ length: totalPages }, (_, i) => i + 1); + } + const window = [page - 1, page, page + 1].filter((p) => p > 1 && p < totalPages); + return [1, ...window, totalPages]; +} + +interface PaginationProps { + page: number; + totalPages: number; + sectionId: string; + onChange: (page: number) => void; + prevLabel: string; + nextLabel: string; + pageLabel: (page: number) => string; +} + +function Pagination({ + page, + totalPages, + sectionId, + onChange, + prevLabel, + nextLabel, + pageLabel, +}: PaginationProps) { + if (totalPages <= 1) return null; + + const goTo = (next: number) => { + onChange(next); + document.getElementById(sectionId)?.scrollIntoView({ behavior: "smooth", block: "start" }); + }; + + const buttonClassName = + "inline-flex h-10 min-w-10 items-center justify-center rounded-none border border-white/10 px-2 text-sm font-medium text-slate-300 transition-colors hover:border-white/25 hover:text-white disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:border-white/10 disabled:hover:text-slate-300"; + + return ( + + ); +} + export default function EventsPageClient({ events }: EventsPageClientProps) { const t = useTranslations("Events"); + const [upcomingPage, setUpcomingPage] = useState(1); - const upcomingEvents = events.filter((event) => { - const status = getEventStatus(event.startDateTime, event.endDateTime); - return status === EVENT_STATUS.UPCOMING || status === EVENT_STATUS.ONGOING; - }); + const upcomingEvents = useMemo( + () => + events.filter(isLive).sort((a, b) => +new Date(a.startDateTime) - +new Date(b.startDateTime)), + [events] + ); - const pastEvents = events.filter((event) => { - const status = getEventStatus(event.startDateTime, event.endDateTime); - return status === EVENT_STATUS.PAST; - }); + const upcomingTotalPages = Math.max(1, Math.ceil(upcomingEvents.length / PAGE_SIZE)); + const visibleUpcoming = upcomingEvents.slice( + (upcomingPage - 1) * PAGE_SIZE, + upcomingPage * PAGE_SIZE + ); return ( -
- {/* Hero Section */} -
-
-
-
+
+
+
+
-
- -

- {t("title")} -

-

{t("description")}

-
+
+

+ 0 ? "animate-pulse bg-emerald-400" : "bg-slate-500"}`} + /> + {t("page_eyebrow")} ·{" "} + {upcomingEvents.length > 0 ? t("registrations_open") : t("registrations_closed")} +

+

+ {t("page_title")} +

+

+ {t("page_subtitle")} +

+
+ + {t("join_community")} + + + + {t("follow_luma")} + +
-
-
- {/* Upcoming Events Section */} -
+
+ {upcomingEvents.length > 0 && ( +
-
-

- {t("upcoming_events")} +

+ {t("upcoming_events")} · {upcomingEvents.length}

- - - {upcomingEvents.length > 0 ? ( - <> - - {upcomingEvents - .slice(0, 1) - .map((event) => - event.id.startsWith("evt-") ? ( - - ) : ( - - ) - )} - - - - - - - - {upcomingEvents.slice(1).map((event) => ( - - {event.id.startsWith("evt-") ? ( - - ) : ( - - )} - - ))} - - ) : ( - - - - )} - +
+ {visibleUpcoming.map(renderEventCard)} +
+ t("pagination_page", { page: pageNumber })} + />
- - {/* Past Events Section */} - {pastEvents.length > 0 && ( -
-
-
-

- {t("past_events")} -

-
-
- - - {pastEvents.map((event) => ( - - - - ))} - -
- )} - - {upcomingEvents.length === 0 && pastEvents.length === 0 && ( - -
- -
-

No events found at the moment.

-

Stay tuned for more updates!

-
- )} -
+ )} + + {events.length === 0 && ( +
+

{t("no_events_title")}

+

{t("page_subtitle")}

+
+ )}
); diff --git a/src/app/[locale]/events/loading.tsx b/src/app/[locale]/events/loading.tsx index d306c89..23e8fea 100644 --- a/src/app/[locale]/events/loading.tsx +++ b/src/app/[locale]/events/loading.tsx @@ -1,24 +1,20 @@ function CardSkeleton({ className = "" }: { className?: string }) { return ( -
- {/* Image Placeholder */} -
+
+
- {/* Title */}
-
-
+
+
- {/* Description */}
-
-
-
+
+
+
- {/* Button */} -
+
); } @@ -26,13 +22,11 @@ function CardSkeleton({ className = "" }: { className?: string }) { export default function EventsLoading() { return (
- {/* Page Header */}
-
-
+
+
- {/* Featured Section */}
@@ -40,7 +34,6 @@ export default function EventsLoading() {
- {/* Events Grid */}
{Array.from({ length: 6 }).map((_, index) => ( diff --git a/src/app/[locale]/events/page.tsx b/src/app/[locale]/events/page.tsx index a785894..1888a02 100644 --- a/src/app/[locale]/events/page.tsx +++ b/src/app/[locale]/events/page.tsx @@ -2,6 +2,7 @@ import { Metadata } from "next"; import Script from "next/script"; import { getTranslations } from "next-intl/server"; +import { getLocalizedPath } from "@/config/i18n/navigation"; import { events } from "@/base/data/dummy"; import EventsPageClient from "./events-page-client"; @@ -21,9 +22,9 @@ export async function generateMetadata({ title: pageTitle, description: pageDescription, alternates: { - canonical: `/${locale}/events`, + canonical: getLocalizedPath("/events", locale), languages: { - en: "/en/events", + en: "/events", bn: "/bn/events", hi: "/hi/events", es: "/es/events", @@ -32,7 +33,7 @@ export async function generateMetadata({ openGraph: { title: pageTitle, description: pageDescription, - url: `/${locale}/events`, + url: getLocalizedPath("/events", locale), siteName: "React Kolkata", locale: locale, type: "website", @@ -46,7 +47,6 @@ export async function generateMetadata({ } export default function EventsPage() { - // Generate Event Schema dynamically const eventSchemas = events.map((event) => ({ "@context": "https://schema.org", "@type": "Event", diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index c5ea8bb..c1f0472 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -8,7 +8,7 @@ import { NextIntlClientProvider } from "next-intl"; import { getMessages } from "next-intl/server"; import { routing } from "@/config/i18n/navigation"; -import { GeistMono, GeistSans } from "@/lib/fonts"; +import { GeistMono, GeistSans, InstrumentSerif, InterSans } from "@/lib/fonts"; import { cn } from "@/lib/utils"; import { JumpToTop } from "@/components/custom/jump-to-top"; import AppProvider from "@/components/providers"; @@ -75,7 +75,9 @@ export default async function RootLayout({ children, params }: RootLayoutProps) className={cn( "min-h-dvh bg-[#0B1220] text-slate-100 antialiased", GeistSans.variable, - GeistMono.variable + GeistMono.variable, + InstrumentSerif.variable, + InterSans.variable )} > diff --git a/src/app/[locale]/media-kit/page.tsx b/src/app/[locale]/media-kit/page.tsx new file mode 100644 index 0000000..3e32c6e --- /dev/null +++ b/src/app/[locale]/media-kit/page.tsx @@ -0,0 +1,251 @@ +import { Metadata } from "next"; +import Image from "next/image"; +import { LuArrowDown, LuCheck, LuFileText, LuX } from "react-icons/lu"; + +import { getLocalizedPath } from "@/config/i18n/navigation"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + + const pageTitle = "Media Kit"; + const pageDescription = "Official React Kolkata logos, typography, and usage guidelines."; + + return { + title: pageTitle, + description: pageDescription, + alternates: { + canonical: getLocalizedPath("/media-kit", locale), + }, + openGraph: { + title: pageTitle, + description: pageDescription, + url: getLocalizedPath("/media-kit", locale), + siteName: "React Kolkata", + locale: locale, + type: "website", + }, + twitter: { + card: "summary_large_image", + title: pageTitle, + description: pageDescription, + }, + }; +} + +const logos = [ + { + name: "Primary Logo", + file: "/media-kit/react-kolkata-logo.svg", + downloadName: "react-kolkata-logo.svg", + format: "SVG", + note: "Default use. Slides, docs, partner pages, print.", + dark: true, + }, + { + name: "Logo on Dark", + file: "/media-kit/react-kolkata-logo-light.png", + downloadName: "react-kolkata-logo-light.png", + format: "PNG", + note: "For dark backgrounds.", + dark: true, + }, + { + name: "Logo on Light", + file: "/media-kit/react-kolkata-logo-dark.png", + downloadName: "react-kolkata-logo-dark.png", + format: "PNG", + note: "For light backgrounds.", + dark: false, + }, + { + name: "Full Lockup on Dark", + file: "/media-kit/react-kolkata-logo-full-light.png", + downloadName: "react-kolkata-logo-full-light.png", + format: "PNG", + note: "Mark plus wordmark, for dark backgrounds.", + dark: true, + }, + { + name: "Full Lockup on Light", + file: "/media-kit/react-kolkata-logo-full-dark.png", + downloadName: "react-kolkata-logo-full-dark.png", + format: "PNG", + note: "Mark plus wordmark, for light backgrounds.", + dark: false, + }, + { + name: "Social Profile", + file: "/media-kit/react-kolkata-social-profile.png", + downloadName: "react-kolkata-social-profile.png", + format: "PNG", + note: "Avatars and profile pictures only.", + dark: true, + }, +]; + +const colours = [ + { name: "Midnight", hex: "#0B1220", use: "Backgrounds" }, + { name: "Sky", hex: "#38BDF8", use: "Accents and highlights" }, + { name: "White", hex: "#FFFFFF", use: "Text on dark surfaces" }, +]; + +const dos = [ + "Give the logo clear space on all sides.", + "Use the light logo on dark backgrounds and vice versa.", + "Prefer the SVG file. It stays sharp at every size.", +]; + +const donts = [ + "Don't stretch, squash, or rotate the logo.", + "Don't recolour it or add shadows and outlines.", + "Don't place it on busy backgrounds where it is hard to read.", +]; + +export default function MediaKitPage() { + return ( +
+
+
+
+
+ +
+

+ Media Kit +

+

+ Official logos and fonts for anyone featuring React Kolkata. Grab what you need below. +

+ +
+
+ +
+

Logos

+

+ Pick the version that fits your background. Every file downloads straight from this page. +

+ +
+ {logos.map((logo) => ( +
+
+ + {`React + +
+
+
+

{logo.name}

+ + {logo.format} + +
+

{logo.note}

+ + + Download + +
+
+ ))} +
+
+ +
+

+ Brand Colours +

+
+ {colours.map((colour) => ( +
+
+
+

{colour.name}

+

{colour.hex}

+

{colour.use}

+
+
+ ))} +
+
+ +
+

Typography

+
+
+

Instrument Serif

+

+ Display and headlines. Italic for emphasis. +

+
+
+

Inter

+

Body text and user interface.

+
+
+

Geist Mono

+

Code and small technical details.

+
+
+
+ +
+

+ Using the Logo +

+
+
+

Please do

+
    + {dos.map((item) => ( +
  • + + {item} +
  • + ))} +
+
+
+

Please don't

+
    + {donts.map((item) => ( +
  • + + {item} +
  • + ))} +
+
+
+
+
+ ); +} diff --git a/src/app/[locale]/not-found.tsx b/src/app/[locale]/not-found.tsx new file mode 100644 index 0000000..37d737b --- /dev/null +++ b/src/app/[locale]/not-found.tsx @@ -0,0 +1,79 @@ +import { Metadata } from "next"; +import { getTranslations } from "next-intl/server"; +import { LuArrowLeft, LuCalendarDays } from "react-icons/lu"; + +import { Link } from "@/config/i18n/navigation"; + +export const metadata: Metadata = { + title: "Page Not Found", + description: "The page you are looking for does not exist.", +}; + +const quickLinks = [ + { label: "Upcoming Events", href: "/#events" }, + { label: "Sponsors", href: "/#sponsors" }, + { label: "Community Partners", href: "/#partners" }, + { label: "FAQ", href: "/#faq" }, +]; + +export default async function NotFound() { + const t = await getTranslations("NotFound"); + + return ( +
+
+
+
+
+ +
+

+ {t("404")} · Lost in the crowd +

+ +

+ Wrong turn. +

+ +

+ {t("dosen't_exist_text")} The meetup is elsewhere. Let's get you back to the + community. +

+ +
+ + + {t("go_to_home")} + + + + See Upcoming Events + +
+
+ +
+ +
+
+ ); +} diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index 5fccd59..da71c65 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -1,6 +1,8 @@ import { Metadata } from "next"; import LandingPage from "@/modules/home"; +import { getLocalizedPath } from "@/config/i18n/navigation"; + type Props = { params: Promise<{ locale: string }>; }; @@ -13,9 +15,9 @@ export async function generateMetadata({ params }: Props): Promise { description: "Join React Kolkata, the hub for React developers in Kolkata. Participate in meetups, workshops, and connect with the community to build, learn, and grow.", alternates: { - canonical: `/${locale}`, + canonical: getLocalizedPath("", locale), languages: { - en: "/en", + en: "/", bn: "/bn", hi: "/hi", es: "/es", @@ -25,7 +27,7 @@ export async function generateMetadata({ params }: Props): Promise { title: "React Kolkata Community | Meetups, Events & Learning", description: "Connect with React developers in Kolkata. Join events, learn, and grow together.", - url: `/${locale}`, + url: getLocalizedPath("", locale), images: [ { url: "/images/hero.jpeg", diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index a7fa924..9290c1c 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -1,25 +1,23 @@ import { MetadataRoute } from "next"; -import { routing } from "@/config/i18n/navigation"; +import { getLocalizedPath, routing } from "@/config/i18n/navigation"; const host = "https://reactkolkata.com"; export default function sitemap(): MetadataRoute.Sitemap { - const routes = ["", "/events", "/contributors", "/blog"]; + const routes = ["", "/events", "/contributors", "/blog", "/code-of-conduct", "/media-kit"]; return routes.map((route) => { const alternates: { languages: Record } = { languages: {}, }; - // Generate alternates for all supported locales routing.locales.forEach((locale) => { - alternates.languages[locale] = `${host}/${locale}${route}`; + alternates.languages[locale] = `${host}${getLocalizedPath(route, locale)}`; }); return { - // We use the default locale for the primary URL of the sitemap entry - url: `${host}/${routing.defaultLocale}${route}`, + url: `${host}${getLocalizedPath(route, routing.defaultLocale)}`, lastModified: new Date(), changeFrequency: "weekly", priority: route === "" ? 1 : 0.8, diff --git a/src/base/constants/site/index.ts b/src/base/constants/site/index.ts index 21efb78..e7d607a 100644 --- a/src/base/constants/site/index.ts +++ b/src/base/constants/site/index.ts @@ -54,14 +54,11 @@ export const highlights = [ }, ]; -// storing constant to hit hashnode api call export const HASHNODE_API_URL = "https://gql.hashnode.com/"; -// number of blogs to fetch in an api call export const FETCH_BLOGS_COUNT_MOBILE = 4; export const FETCH_BLOGS_COUNT_PAD = 6; export const FETCH_BLOGS_COUNT_DESKTOP = 8; -// number of character for author in blog card export const AUTHOR_NAME_CHAR_LIMIT_MD = 10; export const AUTHOR_NAME_CHAR_LIMIT_XL = 13; diff --git a/src/base/data/dummy/index.ts b/src/base/data/dummy/index.ts index 68150a4..f160683 100644 --- a/src/base/data/dummy/index.ts +++ b/src/base/data/dummy/index.ts @@ -8,64 +8,12 @@ export const events = [ description: "Join us at the first-ever in-person meetup of React Kolkata. Calling out all the React and AI developers from the City of Joy and beyond to gather and make this event successful with learning and sharing.", registrationUrl: "https://luma.com/event/evt-39ktH71VIdB6ynb", - image: - "https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=1,background=white,quality=75,width=400,height=400/event-covers/z2/8226021e-7f61-4c12-a7d1-e633d7b87163.jpg", + image: "/video/poster.mp4", type: EVENT_TYPES.IN_PERSON, startDateTime: "2026-05-24T10:00:00+05:30", endDateTime: "2026-05-24T14:00:00+05:30", isFeatured: true, }, - { - id: "rk-nov-2025", - title: "Building Together: The React Kolkata Chapter by ReactPlay", - venue: "Virtual", - description: - "​If you’re passionate about React or any other exciting technology — whether it’s web development, cloud computing, AI, mobile apps, or something else — we invite you to be a speaker at our upcoming tech meetups!​This event is open to everyone, whether you're just starting out or already deep into tech. Come by, learn something new, and be part of the conversation.", - registrationUrl: "https://luma.com/eq8ntf1n", - image: - "https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=1,background=white,quality=75,width=400,height=400/event-covers/ma/ddb6a2c2-1af8-4e94-9d56-fd978b95ad39.png", - type: EVENT_TYPES.ONLINE, - startDateTime: "2025-11-29T19:00:00+05:30", - endDateTime: "2025-11-29T21:30:00+05:30", - }, - { - id: "ze3qavyg", - title: "CTRL + React: Join the React Kolkata Chapter by ReactPlay", - venue: "Virtual", - description: - "In this event, our speakers will share insights on topics like React, AI, and other emerging tech. Expect practical takeaways, real-world examples, and fresh perspectives from folks working hands-on in the field. Event Schedule: Welcome Notes, Intro to Web3: Bringing Blockchain to Your React App - Aditya Singh, Context is All You Need - Akash Nath, React Router v7 Is Not as Scary as You Think - Elijah Asaolu, AI is easy only if you know how to crack it - Arindam Majumdar.", - registrationUrl: "https://luma.com/ze3qavyg", - image: "/images/tech-events-1.jpg", - type: EVENT_TYPES.ONLINE, - startDateTime: "2025-08-30T19:00:00+05:30", - endDateTime: "2025-08-30T21:00:00+05:30", - recordingUrl: "https://www.youtube.com/watch?v=Zvyc3UaTCxw", - }, - { - id: "sup8dxf3", - title: "Speak Up, Tech Up! Join the React Kolkata Chapter by ReactPlay", - venue: "Virtual", - description: - "In this event, our speakers will share insights on topics like React, AI, and other emerging tech. Expect practical takeaways, real-world examples, and fresh perspectives from folks working hands-on in the field. Event Schedule: Welcome notes, What is serverless inferencing in AI? - Haimantika Mitra, From JSX to the Cosmos: Navigating the React Ecosystem - Rohit Debnath, WebRTC for Frontend Devs Who've Suffered Enough Already - Sumanth, Break Time – Quick Quiz Inside, Bringing AI to your React Apps - Wadad Parker, Vibe Smart, Code Better: Things every react developer should know before vibe coding - Sayak Saha.", - registrationUrl: "https://luma.com/sup8dxf3", - image: "/images/tech-events-2.jpg", - type: EVENT_TYPES.ONLINE, - startDateTime: "2025-07-26T19:00:00+05:30", - endDateTime: "2025-07-26T21:00:00+05:30", - recordingUrl: "https://www.youtube.com/watch?v=mK2DNgrK21E", - }, - { - id: "9g6knhqp", - title: "Hacktoberfest Special : Join the React Kolkata Chapter by ReactPlay", - venue: "Virtual", - description: - "Join React Kolkata for an engaging panel discussion with open source maintainers and contributors as we gear up for Hacktoberfest. We'll talk about the realities of maintaining projects, the challenges of contribution, and how developers of all levels can meaningfully get involved in open source. Expect candid insights from experienced maintainers, practical tips for first-time contributors, and a chance to connect with the community that powers so much of the web. Whether you're curious about making your first PR or eager to share your journey, this is your space to learn, discuss, and be inspired.", - registrationUrl: "https://luma.com/9g6knhqp", - image: "/images/tech-events-3.jpg", - type: EVENT_TYPES.ONLINE, - startDateTime: "2025-10-11T19:00:00+05:30", - endDateTime: "2025-10-11T21:00:00+05:30", - }, ]; export const articles = [ diff --git a/src/base/data/social-posts.ts b/src/base/data/social-posts.ts new file mode 100644 index 0000000..aa7e0c0 --- /dev/null +++ b/src/base/data/social-posts.ts @@ -0,0 +1,137 @@ +export type MentionPlatform = "LinkedIn" | "X" | "Instagram"; + +export type Mention = { + quote: string; + name: string; + meta: string; + context: string; + href: string; + platform: MentionPlatform; + postedAt?: string; + bio?: string; + avatar?: string; +}; + +export const bundledMentions: Mention[] = [ + { + quote: + "A little late to the party with this post, but the energy from the August 23rd React Kolkata Community Meetup is still fresh! I had the absolute honor of being the Title Sponsor for this fantastic event. Connecting with such a vibrant and passionate community of developers, designers, and creators was truly inspiring. A major highlight for me was hosting the session, “Conquering the Blank Board: Brainstorming and Ideation in Miro.” Huge thanks to the React Kolkata team for organizing such a stellar meetup.", + name: "Milon Paul", + meta: "Title Sponsor · Aug'26 Meetup", + context: "82 reactions · #ReactKolkata", + href: "https://in.linkedin.com/in/milon-paul-92b798302", + platform: "LinkedIn", + postedAt: "2026-09-06", + bio: "Title Sponsor of the Aug'26 Meetup · hosted the “Conquering the Blank Board” Miro ideation session", + }, + { + quote: + "Great tech events aren't just attended — they're experienced. Behind every successful developer meetup is an unspoken energy: heads down debugging together, animated debates in the aisles and ideas turning into code in real time. Recently documented the vibrant energy at the React Kolkata community meetup. Their motto — “Learn together, Grow together, Build together” — came alive in every corner of the room.", + name: "Gurdil Singh", + meta: "Event photographer", + context: "64 reactions · #reactkolkata", + href: "https://in.linkedin.com/in/gurdil-singh-05124b339", + platform: "LinkedIn", + postedAt: "2026-09-04", + bio: "Tech-event photographer documenting Kolkata meetups · @photographicgurdil_68", + }, + { + quote: + "Thrilled to share that I attended the #ReactKolkata x #Hacktoberfest event! It was an incredible experience filled with insightful sessions — what an unforgettable day with amazing speakers, guests, and community.", + name: "Gunj Joshi", + meta: "Hacktoberfest attendee", + context: "#ReactKolkata", + href: "https://linkedin.com/in/gunjjoshi", + platform: "LinkedIn", + postedAt: "2026-08-30", + bio: "The D. E. Shaw Group · attended the #ReactKolkata x Hacktoberfest event", + }, + { + quote: + "React Kolkata asked me to talk about React. I talked about React, WebAssembly, distributed systems, ghosting, commitment issues and somehow rizzed an entire room of developers. Then we turned their browsers into compute nodes. Frontend by day. Distributed systems by questionable life choice. Huge love to React Kolkata, Sulagna Ghosh and the entire team for the stage, the chaos and the trust. 10/10 event. 0/10 emotional stability.", + name: "Supratim Dhara", + meta: "Speaker · Aug'26 Meetup", + context: "#ReactKolkata", + href: "https://www.linkedin.com/feed/update/urn:li:activity:7500099508253650945/", + platform: "LinkedIn", + bio: "Co-Founder & CTO, bechoHub · spoke on React-powered compute networks", + }, + { + quote: + "Had a great day at React Kolkata as a speaker for the Second Meetup of 2026 at Techno India University. Extending thanks to Sulagna Ghosh and team for organising the meetup so well and smoothly — it was really super cool. Worth mentioning really cool OG devs I met during the meetup: Sayak Saha, Supratim Dhara and Aprajita Verma — those few hours of interaction had more impact than the whole week! It was an awesome Sunday with Wiki Kolkata. Special thanks to Tapas Adhikary for such an awesome community you have built. #rtkaug26", + name: "Ayushman Bhattacharya", + meta: "Speaker · Aug'26 Meetup", + context: "#rtkaug26 · #ReactKolkata", + href: "https://www.linkedin.com/feed/update/urn:li:activity:7497530225099173888/", + platform: "LinkedIn", + bio: "Lead Organizer, GDG JIS University · spoke on the “use client” client boundary", + }, + { + quote: + "From writing code to building with the community. Excited to be part of the React Kolkata Meetup: Aug'26 Edition at Techno India University, Kolkata. The event brings together developers, students, AI enthusiasts, founders, and tech professionals around one common goal: learning, building, and connecting. For me, events like this are not just about attending talks — they are opportunities to learn from people actually building. Learn together. Build together. Grow together.", + name: "Gourab Biswas", + meta: "Attendee · Aug'26 Meetup", + context: "#rtkaug26 · #ReactKolkata", + href: "https://www.linkedin.com/posts/gourab-biswas-478374291_rtkaug26-reactkolkata-reactjs-ugcPost-7497203023916621825-y3JP/", + platform: "LinkedIn", + bio: "Developer · attended the Aug'26 Edition at Techno India University", + }, + { + quote: + "A great day of learning, connecting and building at the React Kolkata Meetup Aug'26 Edition! I'm really happy to have been a part of it, held at Techno India University, Kolkata. As a Computer Science student interested in software development and modern web technologies, attending developer meetups like this is always a great opportunity — it's not just about listening to technical talks, but also about meeting people, sharing ideas, asking questions, and understanding how others are learning and building.", + name: "Jeet Sasmal", + meta: "Attendee · Aug'26 Meetup", + context: "#ReactKolkata · #rtkaug26", + href: "https://www.linkedin.com/posts/jeet-sasmal-321958406_reactkolkata-rtkaug26-reactjs-ugcPost-7497345877045387264-TOLs/", + platform: "LinkedIn", + bio: "Computer Science student · attended at Techno India University", + }, + { + quote: + "Second React Kolkata. Even bigger inspiration. Some events you attend. Some events make you want to build more, learn more and connect more. From deep technical discussions to the Buildathon, Quiz, networking and conversations with fellow developers — a perfect reminder that the tech community grows strongest when knowledge is shared. A special shoutout to the speakers and a very special shoutout to the React Kolkata organizing team for such a welcoming, high-energy developer community.", + name: "Sneha Chakraborty", + meta: "Two-time attendee · Aug'26 Meetup", + context: "#ReactKolkata", + href: "https://www.linkedin.com/posts/sneha-chakraborty-652978324_reactkolkata-reactjs-react-activity-7500014440843763712-1PEq", + platform: "LinkedIn", + bio: "Attended twice · from talks to Buildathon and Quiz", + }, + { + quote: + "A new milestone in my tech journey! I successfully participated in the React Kolkata Buildathon, organized in collaboration with Miro Meetup and React Siliguri at Techno India University. Being part of this Buildathon was a great opportunity to learn, build, collaborate, and challenge myself in a real-world development environment. Grateful for the experience, the learning, and the connections made along the way.", + name: "Abhranil Dutta", + meta: "Buildathon participant · Aug'26", + context: "#ReactKolkata", + href: "https://www.linkedin.com/posts/abhranil-dutta_reactkolkata-buildathon-reactjs-activity-7500241051639693312-_7U2", + platform: "LinkedIn", + bio: "Built at the Miro × React Siliguri Buildathon", + }, +]; + +const normalize = (item: Mention): Mention => ({ + quote: item.quote, + name: item.name, + meta: typeof item.meta === "string" ? item.meta : "", + context: typeof item.context === "string" ? item.context : "", + href: item.href, + platform: item.platform ?? "LinkedIn", + postedAt: typeof item.postedAt === "string" ? item.postedAt : undefined, + bio: typeof item.bio === "string" ? item.bio : undefined, + avatar: typeof item.avatar === "string" ? item.avatar : undefined, +}); + +export function mergeMentions(): Mention[] { + const seen = new Set(); + const merged: Mention[] = []; + + const push = (item: Mention) => { + const key = item.href.trim(); + if (!key || seen.has(key)) return; + seen.add(key); + merged.push(normalize(item)); + }; + + for (const item of bundledMentions) push(item); + + return merged.sort((a, b) => (b.postedAt ?? "").localeCompare(a.postedAt ?? "")); +} diff --git a/src/base/styles/globals.css b/src/base/styles/globals.css index 9c6f2b7..c0b01a6 100644 --- a/src/base/styles/globals.css +++ b/src/base/styles/globals.css @@ -4,6 +4,8 @@ :root { --font-geist-sans: "Geist Sans", ui-sans-serif, system-ui, -apple-system; --font-geist-mono: "Geist Mono", ui-monospace, SFMono-Regular; + --font-display: "Instrument Serif", ui-serif, Georgia, serif; + --font-sans-body: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif; --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); --card: oklch(1 0 0); @@ -77,7 +79,7 @@ @theme inline { --font-sans: Geist, "Geist Placeholder", sans-serif; --font-mono: var(--font-geist-mono); - /* optional: --font-sans, --font-serif, --font-mono if they are applied in the layout.tsx */ + --font-display: var(--font-display); --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); @@ -123,7 +125,16 @@ transform: translateX(-50%); } } - --animate-marquee: marquee 30s linear infinite; + --animate-marquee: marquee 45s linear infinite; + @keyframes marquee-reverse { + from { + transform: translateX(-50%); + } + to { + transform: translateX(0); + } + } + --animate-marquee-reverse: marquee-reverse 55s linear infinite; } @layer base { @@ -145,11 +156,15 @@ body { @apply bg-background text-foreground; - font-family: Geist, "Geist Placeholder", sans-serif; + font-family: var(--font-sans-body), Geist, "Geist Placeholder", sans-serif; } } @layer utilities { + .font-display { + font-family: var(--font-display); + } + /* Hide scrollbar for Chrome, Safari and Opera */ .no-scrollbar::-webkit-scrollbar { display: none; @@ -167,6 +182,19 @@ outline-color: rgb(56 189 248 / 0.8); } +/* Pause marquees on hover */ +.marquee-paused:hover .animate-marquee, +.marquee-paused:hover .animate-marquee-reverse { + animation-play-state: paused; +} + +@media (prefers-reduced-motion: reduce) { + .animate-marquee, + .animate-marquee-reverse { + animation: none; + } +} + /* Ensure sections are visible on mobile */ @media (max-width: 768px) { section { diff --git a/src/components/common/blog/BlogCard.tsx b/src/components/common/blog/BlogCard.tsx index 103a396..315b0ea 100644 --- a/src/components/common/blog/BlogCard.tsx +++ b/src/components/common/blog/BlogCard.tsx @@ -32,11 +32,10 @@ const BlogCard: React.FC = ({ const [modalOpen, setModalOpen] = useState(false); const { isPad, isDesktop } = useDeviceDetail(); - // Validate required properties if (!title || !publishedAt || !author || !author.name) { console.error("BlogCard: Missing required properties", { title, publishedAt, author }); return ( -
+

Invalid blog data

); @@ -58,13 +57,13 @@ const BlogCard: React.FC = ({ return ( <>
setModalOpen(true)} > - {/* Cover Image */} +
= ({ src={coverImageError ? defaultCoverImage : coverImage?.url || defaultCoverImage} alt={title} fill - className="object-cover transition-transform duration-700 group-hover:scale-110" + className="object-cover" onError={() => setCoverImageError(true)} sizes={featured ? "(max-width: 768px) 100vw, 50vw" : "(max-width: 768px) 100vw, 33vw"} /> - {/* Image Overlay Gradient */} +
- {/* Featured Badge */} {featured && ( -
+
Featured Post
)}
- {/* Content */}
{tags && @@ -123,7 +120,7 @@ const BlogCard: React.FC = ({
-
+
{authorImageError ? (
@@ -153,7 +150,6 @@ const BlogCard: React.FC = ({
- {/* Blog Modal */} { if ( !blog || @@ -51,9 +49,7 @@ export default function BlogList({ const [selectedTags, setSelectedTags] = useState([]); const [showFilters, setShowFilters] = useState(false); const [isPending, startTransition] = useTransition(); - const locale = useLocale(); - // Ref to track if a fetch is currently in progress (prevents race conditions) const isFetchingRef = useRef(false); const t = useTranslations("Blog"); @@ -65,11 +61,9 @@ export default function BlogList({ return FETCH_BLOGS_COUNT_DESKTOP; }, [isMobile, isPad]); - // Get all unique tags from articles const allTags = useMemo(() => { const tagMap = new Map(); if (blogsResponse.error) return []; - // get blogs from data property of blogsResponse const blogs = blogsResponse.data; blogs.forEach((article) => { article.tags.forEach((tag) => { @@ -79,7 +73,6 @@ export default function BlogList({ return Array.from(tagMap.values()); }, [blogsResponse.data, blogsResponse.error]); - // Filter articles based on selected tags const filteredArticles = useMemo(() => { if (blogsResponse.error) return []; const blogs = blogsResponse.data; @@ -100,12 +93,10 @@ export default function BlogList({ const handleMoreBlogsCTA = (e: React.MouseEvent) => { e.preventDefault(); - // Prevent concurrent requests (race condition guard) if (isFetchingRef.current || isPending) { return; } - // Mark fetch as in progress isFetchingRef.current = true; loadMoreBlogs(cursor, blogsToShow()) @@ -116,7 +107,6 @@ export default function BlogList({ }); return; } - // Validate new blogs before adding const validNewBlogs = newBlogs.filter((blog) => { if ( !blog || @@ -135,7 +125,6 @@ export default function BlogList({ startTransition(() => { setBlogsResponse((prev) => { - // Additional safety: check for duplicate IDs before merging const existingIds = new Set(prev.data.map((b) => b.id)); const uniqueNewBlogs = validNewBlogs.filter((blog) => !existingIds.has(blog.id)); @@ -156,7 +145,6 @@ export default function BlogList({ }); }) .finally(() => { - // Always reset the fetch flag, even on error isFetchingRef.current = false; }); }; @@ -164,11 +152,11 @@ export default function BlogList({ return (
{blogsResponse.error ? ( -
+

{blogsResponse.error}

@@ -190,10 +178,10 @@ export default function BlogList({ {t("filter")} - {/* Show only when we are not pres */} + {!showLoadMoreButton && ( {t("view_all_posts")} @@ -202,9 +190,8 @@ export default function BlogList({
- {/* Filter Section */} {showFilters && ( -
+

Filter by tags

diff --git a/src/components/common/blog/BlogModal.tsx b/src/components/common/blog/BlogModal.tsx index 8cf8d28..872b2e7 100644 --- a/src/components/common/blog/BlogModal.tsx +++ b/src/components/common/blog/BlogModal.tsx @@ -21,10 +21,8 @@ export default function BlogModal({ blog, modalOpen, setModalOpen }: BlogModalPr const defaultCoverImage = "/images/tech-events-1.jpg"; - // Validate blog object if (!blog) return null; - // Validate required properties if ( !blog.title || !blog.author || @@ -60,9 +58,8 @@ export default function BlogModal({ blog, modalOpen, setModalOpen }: BlogModalPr
- {/* Cover Image */} {(blog.coverImage?.url || !coverImageError) && ( -
+
)} - {/* Article Meta */}
@@ -89,7 +85,6 @@ export default function BlogModal({ blog, modalOpen, setModalOpen }: BlogModalPr
{formatBlogRelativeTime(blog.publishedAt)}
- {/* Tags */}
{blog.tags.map((tag) => ( - {/* Author Section */} -
+
-
+
{authorImageError ? (
@@ -124,7 +118,7 @@ export default function BlogModal({ blog, modalOpen, setModalOpen }: BlogModalPr

{blog.author.name}

- {/* Assuming profileUrl might be added to author object later */} + {blog.author.profileUrl && (

{blog.brief}

- {/* End of hard-coded part */} +
Published by React Kolkata Community
diff --git a/src/components/common/footer/index.tsx b/src/components/common/footer/index.tsx index 316da47..b40e629 100644 --- a/src/components/common/footer/index.tsx +++ b/src/components/common/footer/index.tsx @@ -1,124 +1,49 @@ "use client"; -import * as React from "react"; -import { useEffect, useRef } from "react"; +import Image from "next/image"; import NextLink from "next/link"; -import { usePathname } from "next/navigation"; import { trackGAEvent } from "@/utils/analytics"; -import { gsap } from "gsap"; -import { ScrollTrigger } from "gsap/ScrollTrigger"; -import { useTranslations } from "next-intl"; import { FaInstagram, FaWhatsapp } from "react-icons/fa"; -import { LuCalendar, LuLinkedin, LuMail, LuUsers } from "react-icons/lu"; -import { SiDiscord, SiGithub, SiYoutube } from "react-icons/si"; +import { LuArrowUpRight, LuLinkedin, LuMail } from "react-icons/lu"; +import { SiGithub, SiYoutube } from "react-icons/si"; import { Link } from "@/config/i18n/navigation"; -import { cn } from "@/lib/utils"; import { XLogo } from "../icons/XLogo"; -import "./footer.css"; - -if (typeof window !== "undefined") { - gsap.registerPlugin(ScrollTrigger); -} - -export type MagneticButtonProps = React.ButtonHTMLAttributes & - React.AnchorHTMLAttributes & { - as?: React.ElementType; - }; - -const MagneticButton = React.forwardRef( - ({ className, children, as: Component = "button", ...props }, forwardedRef) => { - const localRef = useRef(null); - - useEffect(() => { - if (typeof window === "undefined") return; - const element = localRef.current; - if (!element) return; - - const ctx = gsap.context(() => { - const handleMouseMove = (e: MouseEvent) => { - const rect = element.getBoundingClientRect(); - const h = rect.width / 2; - const w = rect.height / 2; - const x = e.clientX - rect.left - h; - const y = e.clientY - rect.top - w; - - gsap.to(element, { - x: x * 0.4, - y: y * 0.4, - rotationX: -y * 0.15, - rotationY: x * 0.15, - scale: 1.05, - ease: "power2.out", - duration: 0.4, - }); - }; - - const handleMouseLeave = () => { - gsap.to(element, { - x: 0, - y: 0, - rotationX: 0, - rotationY: 0, - scale: 1, - ease: "elastic.out(1, 0.3)", - duration: 1.2, - }); - }; - - element.addEventListener("mousemove", handleMouseMove as any); - element.addEventListener("mouseleave", handleMouseLeave); - - return () => { - element.removeEventListener("mousemove", handleMouseMove as any); - element.removeEventListener("mouseleave", handleMouseLeave); - }; - }, element); - - return () => ctx.revert(); - }, []); - - return ( - { - (localRef as any).current = node; - if (typeof forwardedRef === "function") forwardedRef(node); - else if (forwardedRef) (forwardedRef as any).current = node; - }} - className={cn("relative cursor-pointer overflow-hidden", className)} - {...props} - > - {children} - - ); - } -); -MagneticButton.displayName = "MagneticButton"; - -const MarqueeItem = () => ( -
- Community First - Open Source - Tech Meetups - Networking - Knowledge Sharing -
-); +const communityLinks = [ + { label: "Events", href: "/#events" as const, internal: false }, + { label: "Sponsors", href: "/#sponsors" as const, internal: false }, + { label: "Partners", href: "/#partners" as const, internal: false }, +]; + +const resourceLinks = [ + { label: "FAQ", href: "/#faq" }, + { label: "Code of Conduct", href: "/code-of-conduct" }, + { + label: "Media Kit", + href: "/media-kit", + }, +]; + +const socials = [ + { icon: XLogo, href: "https://x.com/reactkolkata", label: "X (Twitter)" }, + { icon: SiGithub, href: "https://github.com/reactplay/react-kolkata", label: "GitHub" }, + { + icon: LuLinkedin, + href: "https://www.linkedin.com/showcase/react-kolkata", + label: "LinkedIn", + }, + { icon: SiYoutube, href: "https://www.youtube.com/@Reactkolkata", label: "YouTube" }, + { + icon: FaWhatsapp, + href: "https://chat.whatsapp.com/JmCp4Za9ap0DpER0Gd4hAs", + label: "WhatsApp", + }, + { icon: FaInstagram, href: "https://www.instagram.com/reactkolkata", label: "Instagram" }, +]; export default function Footer() { - const t = useTranslations("Footer"); - const pathname = usePathname(); - const wrapperRef = useRef(null); - const giantTextRef = useRef(null); - const headingRef = useRef(null); - const linksRef = useRef(null); - - useEffect(() => { - ScrollTrigger.refresh(); - }, [pathname]); - const handleSocialClick = (platform: string) => { trackGAEvent("social_icon_click", { category: "Social", @@ -126,217 +51,147 @@ export default function Footer() { }); }; - useEffect(() => { - if (typeof window === "undefined") return; - if (!wrapperRef.current) return; - - const ctx = gsap.context(() => { - gsap.fromTo( - [headingRef.current, linksRef.current], - { y: 30, opacity: 0 }, - { - y: 0, - opacity: 1, - stagger: 0.2, - duration: 1.2, - ease: "power4.out", - scrollTrigger: { - trigger: headingRef.current, - start: "top 95%", - toggleActions: "play none none none", - }, - } - ); - }, wrapperRef); - - ScrollTrigger.refresh(); - - return () => ctx.revert(); - }, []); - return ( -
diff --git a/src/components/common/navbar/index.tsx b/src/components/common/navbar/index.tsx index 768baf5..706177c 100644 --- a/src/components/common/navbar/index.tsx +++ b/src/components/common/navbar/index.tsx @@ -3,20 +3,23 @@ import { useEffect, useRef, useState } from "react"; import Image from "next/image"; import NextLink from "next/link"; // Use NextLink for external links -import { useRouter } from "next/navigation"; import { AnimatePresence, motion } from "framer-motion"; -import { useLocale, useTranslations } from "next-intl"; -import { FaInstagram, FaWhatsapp } from "react-icons/fa"; +import { useTranslations } from "next-intl"; +import { FaInstagram, FaLinkedin, FaWhatsapp } from "react-icons/fa"; import { LuLinkedin, LuMenu, LuX } from "react-icons/lu"; import { SiGithub } from "react-icons/si"; -import { Link, usePathname } from "@/config/i18n/navigation"; // Use localized navigation for internal page routes +import { Link, usePathname, useRouter } from "@/config/i18n/navigation"; // Use localized navigation for internal page routes import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { LanguageSwitcher } from "@/components/custom/language-switcher"; import { XLogo } from "../icons/XLogo"; +type NavLink = + | { href: string; label: string; isHashLink: false } + | { href: string; hash: string; label: string; isHashLink: true }; + const Navbar = () => { const [scrolled, setScrolled] = useState(false); const [open, setOpen] = useState(false); @@ -25,25 +28,23 @@ const Navbar = () => { const router = useRouter(); const pathname = usePathname(); - const locale = useLocale(); const t = useTranslations("Navbar"); - const handleCoreTeamClick = () => { + const links: NavLink[] = [ + { href: "/", hash: "events", label: t("events"), isHashLink: true }, + { href: "/", hash: "sponsors", label: "Sponsors", isHashLink: true }, + { href: "/", hash: "faq", label: "FAQ", isHashLink: true }, + ]; + + const handleHashClick = (hash: string) => { if (pathname === "/") { - const el = document.getElementById("core-team"); + const el = document.getElementById(hash); if (el) el.scrollIntoView({ behavior: "smooth" }); } else { - router.push(`/${locale}/#core-team`); + router.push(`/#${hash}`); } }; - const links = [ - { href: "/", label: t("home"), external: false, isHashLink: false }, - { href: "/#core-team", label: t("core_team"), external: false, isHashLink: true }, - { href: "/contributors", label: t("contributors"), external: false, isHashLink: false }, - { href: "/events", label: t("events"), external: false, isHashLink: false }, - ]; - useEffect(() => { const getScrollY = () => window.scrollY || document.documentElement.scrollTop || document.body.scrollTop; @@ -93,8 +94,7 @@ const Navbar = () => { role="banner" >
- {/* Logo */} - +
{ priority />
+ + React + Kolkata + - {/* Desktop Navigation Links */} - {/* Desktop Actions / Socials */}
    {[ @@ -164,7 +163,7 @@ const Navbar = () => { label: t("github"), }, { - icon: LuLinkedin, + icon: FaLinkedin, href: "https://www.linkedin.com/showcase/react-kolkata", label: t("linkedin"), }, @@ -193,23 +192,12 @@ const Navbar = () => { ))}
- {/* */}
- {/* Mobile menu button */}
- {/* Mobile / Full Menu Overlay */} {open && (