├── __mocks__ ├── styleMock.js ├── fileMock.js ├── next │ ├── link.js │ ├── router.js │ ├── image.js │ └── navigation.js └── hooks.js ├── .prettierignore ├── .eslintrc.json ├── src ├── app │ ├── about │ │ ├── about.module.css │ │ └── page.tsx │ ├── favicon.ico │ ├── meetups │ │ └── page.tsx │ ├── speakers │ │ └── page.tsx │ ├── react-query-provider.tsx │ ├── community │ │ └── community.module.css │ ├── api │ │ ├── speakers │ │ │ └── route.ts │ │ ├── actionLinks │ │ │ └── route.ts │ │ ├── cohort │ │ │ └── route.ts │ │ ├── person-image │ │ │ └── route.ts │ │ ├── speakerRequestForm │ │ │ └── route.ts │ │ ├── notificationForm │ │ │ └── route.ts │ │ └── people │ │ │ └── route.ts │ ├── page.tsx │ ├── hooks │ │ └── useGlobalState │ │ │ └── useGlobalState.tsx │ ├── _constants.tsx │ ├── layout.tsx │ └── page.module.css ├── components │ ├── cardsSection │ │ ├── cardsSection.tsx │ │ └── cards.test.js │ ├── spinner │ │ ├── spinner.tsx │ │ ├── spinner.module.css │ │ └── spinner.test.js │ ├── groupPhotoSection │ │ ├── groupPhotoSection.test.js │ │ ├── groupPhotoSection.tsx │ │ └── groupPhotoSection.module.css │ ├── speakerForm │ │ ├── index.tsx │ │ └── speakerForm.module.css │ ├── communityModal │ │ └── communityModal.tsx │ ├── button │ │ ├── __snapshots__ │ │ │ └── button.test.js.snap │ │ ├── button.tsx │ │ ├── button.test.js │ │ └── button.module.css │ ├── socialSection │ │ ├── socialSection.test.js │ │ ├── socialLinks.tsx │ │ └── socialSection.tsx │ ├── bannerSection │ │ └── bannerSection.test.js │ ├── heroSection │ │ └── heroSection.test.js │ ├── aboutTeam │ │ ├── aboutTeam.module.css │ │ ├── aboutTeam.tsx │ │ └── aboutTeam.test.js │ ├── toast │ │ ├── toast.tsx │ │ ├── toast.module.css │ │ └── toast.test.js │ ├── ui │ │ ├── Card.module.css │ │ ├── Card.tsx │ │ └── OptimizedImage.tsx │ ├── decorative │ │ ├── backgroundPattern.tsx │ │ └── floatingShapes.tsx │ ├── navbar │ │ └── navbar.test.js │ ├── cohortCard │ │ ├── cohortCard.tsx │ │ ├── cohortCard.test.js │ │ └── cohortCard.module.css │ ├── aboutCTA │ │ ├── aboutCTA.tsx │ │ ├── aboutCTA.test.js │ │ └── aboutCTA.module.css │ ├── modal │ │ ├── modal.tsx │ │ ├── modal.module.css │ │ └── modal.test.js │ ├── cohortsOverview │ │ ├── cohortsOverview.tsx │ │ └── cohortsOverview.module.css │ ├── communityImpact │ │ ├── communityImpact.tsx │ │ └── communityImpact.module.css │ ├── ErrorBoundary.tsx │ ├── aboutOffer │ │ ├── aboutOffer.module.css │ │ └── aboutOffer.tsx │ ├── aboutValues │ │ ├── aboutValues.tsx │ │ ├── aboutValues.module.css │ │ └── aboutValues.test.js │ ├── communityTeam │ │ ├── communityTeam.module.css │ │ └── communityTeam.tsx │ ├── communityGetInvolved │ │ ├── communityGetInvolved.tsx │ │ └── communityGetInvolved.module.css │ ├── aboutHero │ │ ├── aboutHero.tsx │ │ ├── aboutHero.test.js │ │ └── aboutHero.module.css │ ├── aboutMission │ │ ├── aboutMission.tsx │ │ ├── aboutMission.module.css │ │ └── aboutMission.test.js │ ├── teamList │ │ └── teamList.tsx │ ├── communitySpeakers │ │ ├── communitySpeakers.tsx │ │ └── communitySpeakers.module.css │ ├── sponsorshipSection │ │ └── sponsorshipSection.tsx │ ├── footer │ │ ├── footer.test.js │ │ └── footer.tsx │ ├── cohortsStructure │ │ └── cohortsStructure.tsx │ ├── cohortsProjects │ │ ├── cohortsProjects.module.css │ │ └── cohortsProjects.tsx │ ├── cohortsDetails │ │ └── cohortsDetails.module.css │ └── cohortsRequirements │ │ └── cohortsRequirements.tsx ├── hooks │ ├── useBodyScrollLock.ts │ ├── useVideoPlayer.ts │ ├── useScrollEffect.ts │ └── useForm.ts ├── types │ └── index.ts └── contexts │ └── ToastContext.tsx ├── types ├── globalTypes.d.ts └── speaker.ts ├── .prettierrc ├── .babelrc ├── lib └── appwrite_client.ts ├── .env.development ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── chore │ ├── bug_report.md │ └── chore.md └── workflows │ └── main.yml ├── tsconfig.json ├── SECURITY.md ├── next.config.mjs ├── LICENSE ├── jest.config.json ├── __test__ └── setupTests.js ├── server.js └── package.json /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | __test__/ 3 | __mocks__/ -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/about/about.module.css: -------------------------------------------------------------------------------- 1 | .aboutPage { 2 | width: 100%; 3 | overflow-x: hidden; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dallassoftwaredevelopers/DSDsite/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /types/globalTypes.d.ts: -------------------------------------------------------------------------------- 1 | // This file is deprecated - use unified types from @/types instead 2 | export * from './index'; 3 | -------------------------------------------------------------------------------- /src/app/meetups/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function MeetupsPage() { 4 | return
Meetups
; 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": true, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "jsxSingleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /src/components/cardsSection/cardsSection.tsx: -------------------------------------------------------------------------------- 1 | import JourneyTimeline from './JourneyTimeline'; 2 | 3 | export default function CardsSection() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /__mocks__/next/link.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Link = ({ children, href, ...props }) => { 4 | return {children}; 5 | }; 6 | 7 | export default Link; 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | [ 5 | "@babel/preset-react", 6 | { 7 | "runtime": "automatic" 8 | } 9 | ], 10 | "@babel/preset-typescript" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /lib/appwrite_client.ts: -------------------------------------------------------------------------------- 1 | import * as sdk from 'node-appwrite'; 2 | 3 | const client = new sdk.Client(); 4 | 5 | client 6 | .setEndpoint(process.env.APPWRITE_ENDPOINT as string) 7 | .setProject(process.env.APPWRITE_PROJECT_ID as string) 8 | .setKey(process.env.APPWRITE_SECRET as string); 9 | 10 | export default client; 11 | -------------------------------------------------------------------------------- /src/app/speakers/page.tsx: -------------------------------------------------------------------------------- 1 | import SpeakersList from '@/components/speakersList/speakersList'; 2 | 3 | export default function SpeakersPage() { 4 | return ( 5 |
6 |

7 | Our Speakers 8 |

9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/components/spinner/spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './spinner.module.css'; 3 | 4 | export default function Spinner() { 5 | return ( 6 |
7 |
8 |
9 |
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/hooks/useBodyScrollLock.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export function useBodyScrollLock(isLocked: boolean) { 4 | useEffect(() => { 5 | if (isLocked) { 6 | document.body.style.overflow = 'hidden'; 7 | } else { 8 | document.body.style.overflow = ''; 9 | } 10 | 11 | return () => { 12 | document.body.style.overflow = ''; 13 | }; 14 | }, [isLocked]); 15 | } 16 | -------------------------------------------------------------------------------- /__mocks__/hooks.js: -------------------------------------------------------------------------------- 1 | export const useScrollEffect = () => ({}); 2 | 3 | export const useVideoPlayer = () => ({ 4 | isPlaying: false, 5 | togglePlay: jest.fn(), 6 | setVideoRef: jest.fn(), 7 | }); 8 | 9 | export const useIntersectionObserver = () => ({ 10 | ref: { current: null }, 11 | isVisible: false, 12 | }); 13 | 14 | export const useLocalStorage = (key, initialValue) => [initialValue, jest.fn()]; 15 | 16 | export const useDebounce = (value, delay) => value; 17 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | APPWRITE_ENDPOINT="API_ENDPOINT" 2 | APPWRITE_PROJECT_ID="PROJECT_ID" 3 | APPWRITE_DATABASE_ID="DATABASE_ID" 4 | APPWRITE_STORAGE_BUCKET_ID="STORAGE_BUCKET_ID" 5 | APPWRITE_SECRET="SECRET" 6 | RECAPTCHA_SECRET="RECAPTCHA_SECRET" 7 | NEXT_PUBLIC_RECAPTCHA_SITEKEY="RECAPTCHA_SITEKEY" 8 | NEXT_PUBLIC_APPWRITE_HASKEY="false" 9 | CONTENTFUL_ACCESS_TOKEN="CONTENTFUL_ACCESS_TOKEN" 10 | CONTENTFUL_SPACE_ID="CONTENTFUL_SPACE_ID" 11 | BLOB_READ_WRITE_TOKEN="BLOB_READ_WRITE_TOKEN" -------------------------------------------------------------------------------- /src/components/groupPhotoSection/groupPhotoSection.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; // Import React 2 | import { render, screen } from '@testing-library/react'; 3 | import GroupPhotoSection from './groupPhotoSection'; 4 | 5 | describe('GroupPhotoSection component', () => { 6 | test('renders the component', () => { 7 | render(); 8 | const groupPhotoElement = screen.getByTestId('groupPhoto'); 9 | expect(groupPhotoElement).toBeInTheDocument(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/speakerForm/index.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic'; 2 | import { ComponentProps } from 'react'; 3 | import { LABELS } from '@/app/labels'; 4 | 5 | const SpeakerFormComponent = dynamic(() => import('./speakerForm'), { 6 | loading: () =>
{LABELS.accessibility.loadingFormText}
, 7 | ssr: false, 8 | }); 9 | 10 | export default function SpeakerForm( 11 | props: ComponentProps 12 | ) { 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/react-query-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; 5 | 6 | export default function ReactQueryProvider({ 7 | children, 8 | }: React.PropsWithChildren) { 9 | const [client] = React.useState( 10 | new QueryClient({ 11 | defaultOptions: { queries: { refetchOnWindowFocus: false } }, 12 | }) 13 | ); 14 | 15 | return {children}; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/community/community.module.css: -------------------------------------------------------------------------------- 1 | .pageContainer { 2 | display: flex; 3 | flex-direction: column; 4 | min-height: 100vh; 5 | width: 100%; 6 | overflow-x: hidden; 7 | background: hsl(var(--light)); 8 | 9 | scrollbar-width: none; 10 | -ms-overflow-style: none; 11 | } 12 | 13 | .pageContainer::-webkit-scrollbar { 14 | display: none; 15 | } 16 | 17 | .loadingContainer { 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | min-height: 100vh; 22 | background: hsl(var(--light)); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/communityModal/communityModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Modal from '@/components/modal/modal'; 3 | import SpeakerForm from '@/components/speakerForm/index'; 4 | 5 | interface CommunityModalProps { 6 | isOpen: boolean; 7 | onClose: () => void; 8 | } 9 | 10 | export default function CommunityModal({ 11 | isOpen, 12 | onClose, 13 | }: CommunityModalProps) { 14 | return ( 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/button/__snapshots__/button.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly with non-primary variant 1`] = ` 4 | 5 | 11 | 12 | `; 13 | 14 | exports[`renders correctly with primary variant 1`] = ` 15 | 16 | 22 | 23 | `; 24 | -------------------------------------------------------------------------------- /__mocks__/next/router.js: -------------------------------------------------------------------------------- 1 | export const useRouter = () => ({ 2 | push: jest.fn(), 3 | replace: jest.fn(), 4 | prefetch: jest.fn(), 5 | back: jest.fn(), 6 | forward: jest.fn(), 7 | reload: jest.fn(), 8 | query: {}, 9 | pathname: '/', 10 | asPath: '/', 11 | route: '/', 12 | events: { 13 | on: jest.fn(), 14 | off: jest.fn(), 15 | emit: jest.fn(), 16 | }, 17 | isReady: true, 18 | isPreview: false, 19 | isFallback: false, 20 | basePath: '', 21 | locale: 'en', 22 | locales: ['en'], 23 | defaultLocale: 'en', 24 | domainLocales: [], 25 | }); 26 | 27 | export default { 28 | useRouter, 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/socialSection/socialSection.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import SocialSection from './socialSection'; 3 | 4 | describe('SocialSection', () => { 5 | test('renders SocialSection component', () => { 6 | render(); 7 | 8 | const socialSection = screen.getByTestId('socialSection'); 9 | expect(socialSection).toBeInTheDocument(); 10 | }); 11 | 12 | test('renders all social icons', () => { 13 | render(); 14 | 15 | const linkList = screen.getAllByRole('listitem'); 16 | expect(linkList).toHaveLength(4); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | .env.production 39 | .env.development 40 | 41 | # blob migration 42 | blob-migration-map.json 43 | video-blob-url.json 44 | -------------------------------------------------------------------------------- /__mocks__/next/image.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Image = ({ 4 | src, 5 | alt, 6 | width, 7 | height, 8 | fill, 9 | priority, 10 | placeholder, 11 | blurDataURL, 12 | quality, 13 | sizes, 14 | ...props 15 | }) => { 16 | // Filter out Next.js specific props that shouldn't be passed to DOM img element 17 | const filteredProps = Object.keys(props).reduce((acc, key) => { 18 | if (!['onLoad', 'onError', 'onLoadingComplete'].includes(key)) { 19 | acc[key] = props[key]; 20 | } 21 | return acc; 22 | }, {}); 23 | 24 | return {alt}; 25 | }; 26 | 27 | export default Image; 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /types/speaker.ts: -------------------------------------------------------------------------------- 1 | export interface ContentfulSpeaker { 2 | name: string; 3 | role: string; 4 | company: string; 5 | photo?: { 6 | sys: { 7 | id: string; 8 | }; 9 | }; 10 | linkedin?: string; 11 | lastSpoke?: string; 12 | topics?: string; 13 | } 14 | 15 | export interface ContentfulAsset { 16 | sys?: { 17 | id: string; 18 | }; 19 | fields?: { 20 | file?: { 21 | url: string; 22 | }; 23 | }; 24 | } 25 | 26 | export interface ContentfulResponse { 27 | items: Array<{ 28 | fields: ContentfulSpeaker; 29 | }>; 30 | includes: { 31 | Asset: ContentfulAsset[]; 32 | }; 33 | } 34 | 35 | // This file is deprecated - use unified types from @/types instead 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"], 22 | "@/types/*": ["./types/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/app/about/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './about.module.css'; 3 | import AboutHero from '@/components/aboutHero/aboutHero'; 4 | import AboutMission from '@/components/aboutMission/aboutMission'; 5 | import AboutValues from '@/components/aboutValues/aboutValues'; 6 | import AboutOffer from '@/components/aboutOffer/aboutOffer'; 7 | import AboutTeam from '@/components/aboutTeam/aboutTeam'; 8 | import AboutCTA from '@/components/aboutCTA/aboutCTA'; 9 | 10 | export default function AboutPage() { 11 | return ( 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Will update this more as the project progresses. 4 | 5 | ## Supported Versions 6 | 7 | Use this section to tell people about which versions of your project are 8 | currently being supported with security updates. 9 | 10 | | Version | Supported | 11 | | ------- | ------------------ | 12 | | 5.1.x | :white_check_mark: | 13 | | 5.0.x | :x: | 14 | | 4.0.x | :white_check_mark: | 15 | | < 4.0 | :x: | 16 | 17 | ## Reporting a Vulnerability 18 | 19 | Use this section to tell people how to report a vulnerability. 20 | 21 | Tell them where to go, how often they can expect to get an update on a 22 | reported vulnerability, what to expect if the vulnerability is accepted or 23 | declined, etc. 24 | -------------------------------------------------------------------------------- /src/components/bannerSection/bannerSection.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import BannerSection from './bannerSection'; 4 | 5 | describe('BannerSection', () => { 6 | test('renders banner section component', () => { 7 | render(); 8 | const bannerSectionElement = screen.getByTestId('bannerSection'); 9 | expect(bannerSectionElement).toBeInTheDocument(); 10 | }); 11 | 12 | test('renders banner section component with label', () => { 13 | render(); 14 | const bannerSectionElement = screen.getByTestId('bannerSection'); 15 | expect(bannerSectionElement).toHaveTextContent( 16 | 'This website is made BY the community FOR the community' 17 | ); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/components/heroSection/heroSection.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; // Import React 2 | import { render, screen } from '@testing-library/react'; 3 | import HeroSection from './heroSection'; 4 | 5 | describe('HeroSection component', () => { 6 | const labelMap = { 7 | lblHero: "You don't have to code alone.", 8 | }; 9 | test('renders the component', () => { 10 | render(); 11 | const heroElement = screen.getByTestId('hero'); 12 | expect(heroElement).toBeInTheDocument(); 13 | }); 14 | 15 | test('renders the correct text', () => { 16 | render(); 17 | 18 | const heroElement = screen.getByTestId('hero'); 19 | expect(heroElement).toHaveTextContent(labelMap.lblHero); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/aboutTeam/aboutTeam.module.css: -------------------------------------------------------------------------------- 1 | .teamSection { 2 | padding: var(--spacing-12) 0; 3 | background: hsl(var(--light)); 4 | } 5 | 6 | .container { 7 | max-width: 1200px; 8 | margin: 0 auto; 9 | padding: 0 var(--spacing-4); 10 | } 11 | 12 | .sectionHeader { 13 | text-align: center; 14 | margin-bottom: var(--spacing-8); 15 | } 16 | 17 | .sectionTitle { 18 | font-size: 2.5rem; 19 | font-weight: 700; 20 | color: hsl(var(--dark)); 21 | margin-bottom: var(--spacing-4); 22 | } 23 | 24 | .sectionDescription { 25 | font-size: 1.125rem; 26 | color: hsl(var(--gray)); 27 | max-width: 600px; 28 | margin: 0 auto; 29 | line-height: 1.6; 30 | } 31 | 32 | .loadingContainer { 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | min-height: 300px; 37 | } 38 | -------------------------------------------------------------------------------- /src/components/toast/toast.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import styles from './toast.module.css'; 3 | 4 | interface ToastProps { 5 | message: string; 6 | type?: 'success' | 'error' | 'warning' | 'info'; 7 | duration?: number; 8 | onClose: () => void; 9 | } 10 | 11 | export default function Toast({ 12 | message, 13 | type = 'success', 14 | duration = 3000, 15 | onClose, 16 | }: ToastProps) { 17 | useEffect(() => { 18 | const timer = setTimeout(onClose, duration); 19 | return () => clearTimeout(timer); 20 | }, [onClose, duration]); 21 | 22 | return ( 23 |
24 | {message} 25 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/app/api/speakers/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { getSpeakers } from '../../../../lib/contentful'; 3 | 4 | export const dynamic = 'force-dynamic'; 5 | export const revalidate = 0; // Disable caching 6 | 7 | export async function GET() { 8 | try { 9 | const speakers = await getSpeakers(); 10 | return NextResponse.json(speakers, { 11 | headers: { 12 | 'Cache-Control': 'no-store, no-cache, must-revalidate', 13 | Pragma: 'no-cache', 14 | Expires: '0', 15 | }, 16 | }); 17 | } catch (error) { 18 | console.error('API Error:', error); 19 | return NextResponse.json( 20 | { 21 | error: 22 | error instanceof Error ? error.message : 'Failed to fetch speakers', 23 | }, 24 | { status: 500 } 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: 'https', 7 | hostname: 'images.ctfassets.net', 8 | port: '', 9 | pathname: '/**', 10 | }, 11 | { 12 | protocol: 'https', 13 | hostname: 'picsum.photos', 14 | port: '', 15 | pathname: '/**', 16 | }, 17 | { 18 | protocol: 'https', 19 | hostname: 'vpgsxqtnqt8tekgb.public.blob.vercel-storage.com', 20 | port: '', 21 | pathname: '/**', 22 | }, 23 | { 24 | protocol: 'https', 25 | hostname: '*.public.blob.vercel-storage.com', 26 | port: '', 27 | pathname: '/**', 28 | }, 29 | ], 30 | }, 31 | }; 32 | 33 | export default nextConfig; 34 | -------------------------------------------------------------------------------- /src/components/socialSection/socialLinks.tsx: -------------------------------------------------------------------------------- 1 | import { IconContext } from 'react-icons'; 2 | import { SocialLinkData } from './socialSection'; 3 | 4 | interface SocialLinksProps { 5 | links: SocialLinkData[]; 6 | iconContextValue: IconContext; 7 | className: string; 8 | } 9 | 10 | const SocialLinks = ({ 11 | links, 12 | iconContextValue, 13 | className, 14 | }: SocialLinksProps) => { 15 | return ( 16 | 17 | 26 | 27 | ); 28 | }; 29 | 30 | export default SocialLinks; 31 | -------------------------------------------------------------------------------- /src/components/ui/Card.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | border-radius: 12px; 3 | transition: all 0.3s ease; 4 | background: white; 5 | border: none; 6 | cursor: default; 7 | } 8 | 9 | .default { 10 | background: white; 11 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 12 | } 13 | 14 | .elevated { 15 | background: white; 16 | box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); 17 | transform: translateY(-2px); 18 | } 19 | 20 | .outlined { 21 | background: white; 22 | border: 2px solid #e5e7eb; 23 | box-shadow: none; 24 | } 25 | 26 | .card:hover { 27 | transform: translateY(-4px); 28 | box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2); 29 | } 30 | 31 | .card[type='button'] { 32 | cursor: pointer; 33 | } 34 | 35 | .paddingSM { 36 | padding: 1rem; 37 | } 38 | 39 | .paddingMD { 40 | padding: 1.5rem; 41 | } 42 | 43 | .paddingLG { 44 | padding: 2rem; 45 | } 46 | -------------------------------------------------------------------------------- /__mocks__/next/navigation.js: -------------------------------------------------------------------------------- 1 | export const useRouter = () => ({ 2 | push: jest.fn(), 3 | replace: jest.fn(), 4 | prefetch: jest.fn(), 5 | back: jest.fn(), 6 | forward: jest.fn(), 7 | refresh: jest.fn(), 8 | query: {}, 9 | pathname: '/', 10 | asPath: '/', 11 | route: '/', 12 | events: { 13 | on: jest.fn(), 14 | off: jest.fn(), 15 | emit: jest.fn(), 16 | }, 17 | }); 18 | 19 | export const usePathname = () => '/'; 20 | 21 | export const useSearchParams = () => ({ 22 | get: jest.fn(), 23 | getAll: jest.fn(), 24 | has: jest.fn(), 25 | keys: jest.fn(), 26 | values: jest.fn(), 27 | entries: jest.fn(), 28 | forEach: jest.fn(), 29 | toString: jest.fn(), 30 | }); 31 | 32 | export const useParams = () => ({}); 33 | 34 | export const redirect = jest.fn(); 35 | export const permanentRedirect = jest.fn(); 36 | export const notFound = jest.fn(); 37 | -------------------------------------------------------------------------------- /src/components/ui/Card.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import styles from './Card.module.css'; 3 | 4 | interface CardProps { 5 | children: ReactNode; 6 | variant?: 'default' | 'elevated' | 'outlined'; 7 | padding?: 'sm' | 'md' | 'lg'; 8 | className?: string; 9 | onClick?: () => void; 10 | } 11 | 12 | export default function Card({ 13 | children, 14 | variant = 'default', 15 | padding = 'md', 16 | className = '', 17 | onClick, 18 | }: CardProps) { 19 | const cardClasses = `${styles.card} ${styles[variant]} ${styles[`padding${padding.toUpperCase()}`]} ${className}`; 20 | 21 | const CardElement = onClick ? 'button' : 'div'; 22 | 23 | return ( 24 | 29 | {children} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/decorative/backgroundPattern.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import styles from './decorative.module.css'; 5 | 6 | interface BackgroundPatternProps { 7 | variant?: 'dots' | 'grid' | 'waves' | 'circles'; 8 | color?: string; 9 | opacity?: number; 10 | className?: string; 11 | } 12 | 13 | export default function BackgroundPattern({ 14 | variant = 'dots', 15 | color = 'var(--primary)', 16 | opacity = 0.05, 17 | className = '', 18 | }: BackgroundPatternProps) { 19 | return ( 20 |