diff --git a/.github/workflows/cleanup-gh-pages-preview.yml b/.github/workflows/cleanup-gh-pages-preview.yml new file mode 100644 index 0000000..b707ddb --- /dev/null +++ b/.github/workflows/cleanup-gh-pages-preview.yml @@ -0,0 +1,37 @@ +name: đŸ§č Cleanup GitHub Pages Preview + +on: + pull_request: + types: [closed] + +permissions: + contents: write + +jobs: + cleanup: + name: 🗑 Remove Preview Deployment + runs-on: ubuntu-latest + + steps: + - name: 🔍 Checkout gh-pages branch + uses: actions/checkout@v4 + with: + ref: gh-pages + fetch-depth: 1 + + - name: đŸ§č Remove preview directory + run: | + BRANCH="${{ github.head_ref }}" + SAFE_BRANCH=$(echo "$BRANCH" | sed 's/[^a-zA-Z0-9._-]/-/g') + PREVIEW_DIR="preview/${SAFE_BRANCH}" + + if [ -d "$PREVIEW_DIR" ]; then + echo "Removing preview directory: $PREVIEW_DIR" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git rm -rf "$PREVIEW_DIR" + git commit -m "đŸ§č Remove preview for closed PR #${{ github.event.pull_request.number }} ($BRANCH)" + git push + else + echo "No preview directory found at $PREVIEW_DIR, skipping cleanup." + fi diff --git a/.github/workflows/nextjs-static-gh-pages.yml b/.github/workflows/nextjs-static-gh-pages.yml index 1642d31..ce8dc10 100644 --- a/.github/workflows/nextjs-static-gh-pages.yml +++ b/.github/workflows/nextjs-static-gh-pages.yml @@ -2,57 +2,95 @@ name: 🚀 Deploy Static Next.js to GitHub Pages on: push: - branches: ['main'] # Triggers on push to main branch + branches: ['**'] # Triggers on push to any branch + pull_request: + types: [opened, synchronize, reopened] workflow_dispatch: # Allows manual triggering permissions: - contents: read - pages: write - id-token: write + contents: write + pull-requests: write concurrency: - group: 'pages' - cancel-in-progress: false + group: 'pages-${{ github.head_ref || github.ref_name }}' + cancel-in-progress: true jobs: - build: - name: 🏗 Build Static Site + build-and-deploy: + name: 🏗 Build & Deploy runs-on: ubuntu-latest - - steps: - name: 🔍 Checkout repository uses: actions/checkout@v4 + - name: 🔧 Set deployment variables + id: vars + run: | + REPO_NAME="${{ github.event.repository.name }}" + # Custom domain means no repo name prefix in the URL path. + # Set CUSTOM_DOMAIN to your domain, or leave empty to use github.io/ URLs. + CUSTOM_DOMAIN="dev.codebuilder.org" + + # Get the branch name (works for both push and PR events) + if [ "${{ github.event_name }}" = "pull_request" ]; then + BRANCH="${{ github.head_ref }}" + else + BRANCH="${{ github.ref_name }}" + fi + + # Sanitize branch name for use in URL paths + SAFE_BRANCH=$(echo "$BRANCH" | sed 's/[^a-zA-Z0-9._-]/-/g') + + if [ -n "$CUSTOM_DOMAIN" ]; then + BASE_URL="https://${CUSTOM_DOMAIN}" + else + BASE_URL="https://${{ github.repository_owner }}.github.io/${REPO_NAME}" + fi + + if [ "$BRANCH" = "main" ]; then + if [ -n "$CUSTOM_DOMAIN" ]; then + echo "base_path=" >> $GITHUB_OUTPUT + else + echo "base_path=/${REPO_NAME}" >> $GITHUB_OUTPUT + fi + echo "dest_dir=" >> $GITHUB_OUTPUT + # keep_files must be true so preview/ directories are preserved + echo "keep_files=true" >> $GITHUB_OUTPUT + echo "is_main=true" >> $GITHUB_OUTPUT + echo "preview_url=${BASE_URL}/" >> $GITHUB_OUTPUT + else + echo "base_path=/preview/${SAFE_BRANCH}" >> $GITHUB_OUTPUT + echo "dest_dir=preview/${SAFE_BRANCH}" >> $GITHUB_OUTPUT + echo "keep_files=true" >> $GITHUB_OUTPUT + echo "preview_url=${BASE_URL}/preview/${SAFE_BRANCH}/" >> $GITHUB_OUTPUT + fi + + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + echo "safe_branch=$SAFE_BRANCH" >> $GITHUB_OUTPUT + - name: 🔎 Detect package manager id: detect-pm run: | if [ -f "pnpm-lock.yaml" ]; then - echo "manager=pnpm" >> $GITHUB_ENV - echo "command=install" >> $GITHUB_ENV - echo "runner=pnpm exec" >> $GITHUB_ENV + echo "manager=pnpm" >> $GITHUB_OUTPUT + echo "command=install" >> $GITHUB_OUTPUT + echo "runner=pnpm exec" >> $GITHUB_OUTPUT else - echo "manager=npm" >> $GITHUB_ENV - echo "command=ci" >> $GITHUB_ENV - echo "runner=npx --no-install" >> $GITHUB_ENV + echo "manager=npm" >> $GITHUB_OUTPUT + echo "command=ci" >> $GITHUB_OUTPUT + echo "runner=npx --no-install" >> $GITHUB_OUTPUT fi - name: 📩 Install pnpm - if: env.manager == 'pnpm' + if: steps.detect-pm.outputs.manager == 'pnpm' run: npm install -g pnpm - name: ⚙ Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' - cache: ${{ env.manager }} - - - name: 🌐 Setup GitHub Pages - uses: actions/configure-pages@v5 - - # NOTE: We do NOT need to create .env or secrets here, as we provide them directly to the build step below. - + node-version: '24' + cache: ${{ steps.detect-pm.outputs.manager }} - name: đŸš« Ephemerally delete server/api files run: | @@ -60,30 +98,77 @@ jobs: rm -rf src/app/api src/server src/proxy.ts src/app/jobs/[id] src/app/[...not-found] prisma.config.ts - name: đŸ“„ Install dependencies - run: ${{ env.manager }} ${{ env.command }} + run: ${{ steps.detect-pm.outputs.manager }} ${{ steps.detect-pm.outputs.command }} - name: 🏗 Generate Static Build env: NEXT_OUTPUT_MODE: export GITHUB_PAGES: 1 + NEXT_BASE_PATH: ${{ steps.vars.outputs.base_path }} run: | echo "Building static files for GitHub Pages..." + echo "Base path: $NEXT_BASE_PATH" + echo "Preview URL: ${{ steps.vars.outputs.preview_url }}" pnpm build touch out/.nojekyll - - name: đŸ“€ Upload static site - uses: actions/upload-pages-artifact@v3 + - name: đŸ§č Clean stale root files from gh-pages (main only) + if: steps.vars.outputs.is_main == 'true' + run: | + # Checkout the gh-pages branch into a temp directory + git fetch origin gh-pages || true + mkdir -p /tmp/gh-pages-current + cd /tmp/gh-pages-current + git init + git remote add origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git + git fetch origin gh-pages --depth=1 || exit 0 + git checkout gh-pages || exit 0 + + # Delete everything EXCEPT the preview/ directory and CNAME + find . -maxdepth 1 ! -name '.' ! -name '.git' ! -name 'preview' ! -name 'CNAME' -exec rm -rf {} + + + git add -A + git diff --cached --quiet || git -c user.name="github-actions" -c user.email="github-actions@github.com" commit -m "clean stale root files before main deploy" + git push origin gh-pages || true + + - name: 🚀 Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 with: - path: ./out # With 'output: export', Next.js automatically puts files here. - - deploy: - name: 🚀 Deploy to GitHub Pages - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - - name: 🌍 Deploy - id: deployment - uses: actions/deploy-pages@v4 + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./out + destination_dir: ${{ steps.vars.outputs.dest_dir }} + keep_files: ${{ steps.vars.outputs.keep_files }} + cname: dev.codebuilder.org + + - name: 💬 Comment preview URL on PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const url = '${{ steps.vars.outputs.preview_url }}'; + const sha = context.sha.substring(0, 7); + const body = `🚀 **Preview deployment ready!**\n\n📎 **Preview URL:** ${url}\n\n_Deployed from commit \`${sha}\`_`; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.find(c => c.body.includes('Preview deployment ready!')); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } diff --git a/next.config.ts b/next.config.ts index 680880b..0786852 100644 --- a/next.config.ts +++ b/next.config.ts @@ -6,8 +6,11 @@ const isStaticExport = process.env.NEXT_OUTPUT_MODE === 'export' console.log(`\n Next.js static export mode: ${isStaticExport}`) -const repoName = 'codebuilder-frontend'; -const isGithubPages = !!process.env.GITHUB_PAGES; +const repoName = 'codebuilder-frontend' +const isGithubPages = !!process.env.GITHUB_PAGES +// Set by the GitHub Pages workflow to support preview deployments in subdirectories. +// e.g. "/codebuilder-frontend" for main, "/codebuilder-frontend/preview/my-branch" for previews. +const ghPagesBasePath = process.env.NEXT_BASE_PATH || '' const nextConfig = { /** @@ -21,10 +24,16 @@ const nextConfig = { // Conditionally set the output mode for the build. output: isStaticExport ? 'export' : undefined, - //basePath: //isGithubPages ? `/${repoName}` : '', - //assetPrefix: //isGithubPages ? `/${repoName}/` : '', + basePath: ghPagesBasePath || undefined, + assetPrefix: ghPagesBasePath ? `${ghPagesBasePath}/` : undefined, - allowedDevOrigins: ['https://api.codebuilder.org', 'https://new.codebuilder.org', 'https://new.codebuilder.org:443', 'https://dev.codebuilder.org', 'https://dev.codebuilder.org:443'], // resolves the CORS warning + allowedDevOrigins: [ + 'https://api.codebuilder.org', + 'https://new.codebuilder.org', + 'https://new.codebuilder.org:443', + 'https://dev.codebuilder.org', + 'https://dev.codebuilder.org:443', + ], // resolves the CORS warning // Note: If you are using next/image, you may need to add an // unoptimized: true flag here if you are not using a custom loader. @@ -65,7 +74,7 @@ const nextConfig = { return config }, -} as unknown as NextConfig; +} as unknown as NextConfig // // Only include page.* files for static export, include route.* for all other builds // if (isStaticExport) { diff --git a/public/images/portfolio/acs2.png b/public/images/portfolio/acs2.png new file mode 100755 index 0000000..74e06a5 Binary files /dev/null and b/public/images/portfolio/acs2.png differ diff --git a/public/images/portfolio/cdc.png b/public/images/portfolio/cdc.png new file mode 100755 index 0000000..9500f4a Binary files /dev/null and b/public/images/portfolio/cdc.png differ diff --git a/public/images/portfolio/logo-12252.jpg b/public/images/portfolio/logo-12252.jpg new file mode 100755 index 0000000..2977fb3 Binary files /dev/null and b/public/images/portfolio/logo-12252.jpg differ diff --git a/public/images/portfolio/logo-ddna.gif b/public/images/portfolio/logo-ddna.gif new file mode 100755 index 0000000..396f65a Binary files /dev/null and b/public/images/portfolio/logo-ddna.gif differ diff --git a/public/images/portfolio/pifm.png b/public/images/portfolio/pifm.png new file mode 100755 index 0000000..665327b Binary files /dev/null and b/public/images/portfolio/pifm.png differ diff --git a/public/images/portfolio/sf_heart.jpg b/public/images/portfolio/sf_heart.jpg new file mode 100755 index 0000000..d4a7e72 Binary files /dev/null and b/public/images/portfolio/sf_heart.jpg differ diff --git a/public/images/portfolio/taxcoursecentral.png b/public/images/portfolio/taxcoursecentral.png new file mode 100755 index 0000000..804b12e Binary files /dev/null and b/public/images/portfolio/taxcoursecentral.png differ diff --git a/public/images/portfolio/taxcoursecentral_badge.png b/public/images/portfolio/taxcoursecentral_badge.png new file mode 100755 index 0000000..affb750 Binary files /dev/null and b/public/images/portfolio/taxcoursecentral_badge.png differ diff --git a/public/images/staff/corbin.jpg b/public/images/staff/corbin.jpg new file mode 100755 index 0000000..5551829 Binary files /dev/null and b/public/images/staff/corbin.jpg differ diff --git a/public/images/staff/kevin.png b/public/images/staff/kevin.png new file mode 100755 index 0000000..a354b03 Binary files /dev/null and b/public/images/staff/kevin.png differ diff --git a/public/images/staff/larrygoodrie.jpg b/public/images/staff/larrygoodrie.jpg new file mode 100755 index 0000000..fe368e8 Binary files /dev/null and b/public/images/staff/larrygoodrie.jpg differ diff --git a/public/images/staff/tom.jpg b/public/images/staff/tom.jpg new file mode 100755 index 0000000..e461859 Binary files /dev/null and b/public/images/staff/tom.jpg differ diff --git a/public/videos/background-video-portfolio.mp4 b/public/videos/background-video-portfolio.mp4 new file mode 100755 index 0000000..8ef0231 Binary files /dev/null and b/public/videos/background-video-portfolio.mp4 differ diff --git a/public/videos/background-video-portfolio.webm b/public/videos/background-video-portfolio.webm new file mode 100755 index 0000000..a6d3d53 Binary files /dev/null and b/public/videos/background-video-portfolio.webm differ diff --git a/public/videos/contact-background.mp4 b/public/videos/contact-background.mp4 new file mode 100755 index 0000000..fb24677 Binary files /dev/null and b/public/videos/contact-background.mp4 differ diff --git a/public/videos/cover-images/background-video-portfolio-poster.jpg b/public/videos/cover-images/background-video-portfolio-poster.jpg new file mode 100644 index 0000000..aa366b7 Binary files /dev/null and b/public/videos/cover-images/background-video-portfolio-poster.jpg differ diff --git a/public/videos/cover-images/contact-background-poster.jpg b/public/videos/cover-images/contact-background-poster.jpg new file mode 100644 index 0000000..be15fbe Binary files /dev/null and b/public/videos/cover-images/contact-background-poster.jpg differ diff --git a/public/videos/cover-images/macbook-typing-poster.jpg b/public/videos/cover-images/macbook-typing-poster.jpg index 8055ea4..fef3083 100644 Binary files a/public/videos/cover-images/macbook-typing-poster.jpg and b/public/videos/cover-images/macbook-typing-poster.jpg differ diff --git a/public/videos/cover-images/network-technology-services-poster.jpg b/public/videos/cover-images/network-technology-services-poster.jpg new file mode 100644 index 0000000..cd3c4d7 Binary files /dev/null and b/public/videos/cover-images/network-technology-services-poster.jpg differ diff --git a/public/videos/network-technology-services.mp4 b/public/videos/network-technology-services.mp4 new file mode 100755 index 0000000..1f60d60 Binary files /dev/null and b/public/videos/network-technology-services.mp4 differ diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 272f744..2241975 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -1,68 +1,294 @@ 'use client' import Image from 'next/image' -import VideoPlayer from '../../components/video-player' -import { useEffect } from 'react' +import Link from 'next/link' +import VideoPlayer from '@/components/video-player' +import TeamMember from '@/components/about/TeamMember' +import Counter from '@/components/about/Counter' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faEnvelope, faPhone, faDiamond, faUsers, faMoneyBill, faCode } from '@fortawesome/free-solid-svg-icons' +import { faGithub, faLinkedin, faDiscord } from '@fortawesome/free-brands-svg-icons' +import { Roboto, Raleway, Pacifico } from '../fonts' +import { useEffect, useState } from 'react' +import ContactSection from '@/components/layout/contact-banner' +import 'animate.css' + +// Team members data +const teamMembers = [ + { + name: 'Andrew Corbin', + title: 'Software Engineer (CEO)', + bio: 'Andrew, fueled by 16 years of unwavering passion and professionalism in the software industry, founded CodeBuilder Inc. in 2017. Drawing from a wealth of technical expertise and business experiences, Andrew established CodeBuilder Inc. as a testament to his commitment to delivering innovative and impactful solutions.', + imageSrc: '/images/staff/corbin.jpg', + imageAlt: 'Andrew Corbin', + skills: [ + { label: 'Software Engineering', percentage: 95 }, + { label: 'Server Administration (DevOps)', percentage: 92 }, + { label: 'Database Engineering', percentage: 88 }, + { label: 'Operational Security (OpSec)', percentage: 85 }, + { label: 'Frontend Engineering', percentage: 78 }, + { label: 'Blockchain Development / Web3 / DApps', percentage: 82 }, + { label: 'Smart Contracts (Solidity)', percentage: 72 }, + ], + contactLinks: [ + { icon: 'envelope', label: 'Email', href: 'mailto:andrew@codebuilder.us' }, + { icon: 'github', label: 'GitHub', href: 'https://github.com/digitalnomad91' }, + { icon: 'linkedin', label: 'LinkedIn', href: 'https://linkedin.com/digitalnomad91' }, + { icon: 'discord', label: 'Discord', href: 'https://discord.com/users/542088220117303316' }, + ], + }, + { + name: 'Kevin Castiglia', + title: 'Software Engineer (Co-Founder)', + bio: 'Kevin, a seasoned software engineer with a keen interest in infrastructure, brings a wealth of experience from his tenure with several tech startups. Driven by a desire to chart his own course, he has now embarked on the journey of launching his own venture.', + imageSrc: '/images/staff/kevin.png', + imageAlt: 'Kevin Castiglia', + skills: [ + { label: 'Software Engineering', percentage: 88 }, + { label: 'DevOps', percentage: 84 }, + { label: 'Python', percentage: 91 }, + { label: 'Virtualization / Docker Containers', percentage: 72 }, + { label: 'Build/Test Automation', percentage: 68 }, + { label: 'Database Administration', percentage: 65 }, + ], + contactLinks: [ + { icon: 'phone', label: 'Call', href: 'tel:+18453638331' }, + { icon: 'envelope', label: 'Email', href: 'mailto:kevin@codebuilder.us' }, + ], + }, + { + name: 'Larry Goodrie', + title: 'System Analyst Specialist', + bio: 'With 23 years of extensive experience, Larry Goodrie excels in designing and supporting both hardware infrastructure and software requirements.', + imageSrc: '/images/staff/larrygoodrie.jpg', + imageAlt: 'Larry Goodrie', + skills: [ + { label: 'C++', percentage: 85 }, + { label: 'PHP', percentage: 78 }, + { label: 'JavaScript (ES7)', percentage: 82 }, + { label: 'Linux / Bash', percentage: 74 }, + ], + contactLinks: [{ icon: 'envelope', label: 'Email', href: 'mailto:larrygoodrie@gmail.com' }], + }, + { + name: 'Tom Johnson', + title: 'Software Engineer', + bio: 'Tom is a software engineer with a passion for open source, optimisation and clean code. Having worked with many programming languages from a young age, joining CodeBuilder was a natural progression.', + imageSrc: '/images/staff/tom.jpg', + imageAlt: 'Tom Johnson', + skills: [ + { label: 'C++', percentage: 82 }, + { label: 'PHP', percentage: 76 }, + { label: 'JavaScript (ES7)', percentage: 88 }, + { label: 'Linux / Bash', percentage: 70 }, + ], + contactLinks: [{ icon: 'envelope', label: 'Email', href: 'mailto:tom@codebuilder.us' }], + }, +] + +// Stats data +const stats = [ + { icon: faDiamond, label: 'Projects', value: 487, speed: 5000 }, + { icon: faUsers, label: 'Clients', value: 134, speed: 5000 }, + { icon: faMoneyBill, label: 'Invoices', value: 275, speed: 5000 }, + { icon: faCode, label: 'Lines of Code', value: 100000, speed: 60000 }, +] + +// Client logos +const clients = [ + { + src: '/images/portfolio/singelforaldrar_logo.jpg', + alt: 'SingelFörĂ€ldrar', + title: 'SingelFörĂ€ldrar.se', + }, + { + src: '/images/portfolio/dha_smooth_logo1.png', + alt: 'Defense Health Agency', + title: 'Defense Health Agency', + }, + { + src: '/images/portfolio/logo-12252.jpg', + alt: 'Orange County Bar Association', + title: 'Orange County Bar Association', + }, + { + src: '/images/portfolio/cdc.png', + alt: 'Centers for Disease Control', + title: 'Centers for Disease Control', + }, + { + src: '/images/portfolio/taxcoursecentral_badge.png', + alt: 'Tax Course Central', + title: 'Tax Course Central', + }, + { + src: '/images/portfolio/pifm.png', + alt: 'Park it for Me', + title: 'Park It For Me', + }, + { + src: '/images/portfolio/logo-ddna.gif', + alt: 'Developmental Disabilities Nurses Association', + title: 'Developmental Disabilities Nurses Association', + }, +] export default function About() { - useEffect(() => { - const timer = setTimeout(() => { - //throw new Error('This is a simulated server error.') - }, 1000) + const [bannerVisible, setBannerVisible] = useState(false) - return () => clearTimeout(timer) + useEffect(() => { + setBannerVisible(true) }, []) + return ( -
- {/* Hero Slider */} -
-
- +
+ {/* Banner with Video Background — pt offsets the fixed header */} +
+ {/* Video Background */} +
+
+ +
+ {/* Dark overlay */} +
-
- - {/* Main Content */} -
-
-
    -
  1. - Get started by editing{' '} - src/app/page.tsx. -
  2. -
  3. Save and see your changes instantly.
  4. -
- + + {/* Team Section */} +
+
+

+ The CodeBuilder + , Inc. Team +

+
+ {/* Fallback separator in case CSS pseudo-element doesn't render */} +
+ + {/* Team Members */} +
+ {teamMembers.map((member, index) => ( + + ))} +
+ + {/* Statistics Heading */} +
+

+ CodeBuilder + , Inc. Stats +

+
+
+ + {/* Stats Grid */} +
+ {stats.map((stat) => ( +
+ + + +

{stat.label}

+ +
+ ))} +
+ + {/* Client Logos */} +
+
+ {clients.map((client) => ( + + {client.alt} + + ))} +
+
+ +
+
+
+ + {/* Reach Out / Contact Banner */} +
) } diff --git a/src/app/api/contact/route.ts b/src/app/api/contact/route.ts new file mode 100644 index 0000000..37af45b --- /dev/null +++ b/src/app/api/contact/route.ts @@ -0,0 +1,38 @@ +import { NextResponse } from 'next/server' + +export async function POST(request: Request) { + try { + const body = await request.json() + + const { name, email, subject, message } = body + + // Validate required fields + if (!name || !email || !subject || !message) { + return NextResponse.json({ error: 'All fields are required.' }, { status: 400 }) + } + + // Basic email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!emailRegex.test(email)) { + return NextResponse.json({ error: 'Please provide a valid email address.' }, { status: 400 }) + } + + // Simulate network delay for realistic loading indicator + await new Promise((resolve) => setTimeout(resolve, 1500)) + + // Mock successful response + // In production, this would send an email via a service like SendGrid, + // Resend, or forward to the Laravel backend at /contact/submit + console.log('[Contact Form] New submission:', { + name, + email, + subject, + message: message.substring(0, 100) + (message.length > 100 ? '...' : ''), + timestamp: new Date().toISOString(), + }) + + return NextResponse.json({ success: true, message: 'Your message has been sent successfully.' }, { status: 200 }) + } catch { + return NextResponse.json({ error: 'An unexpected error occurred. Please try again.' }, { status: 500 }) + } +} diff --git a/src/app/contact/page.tsx b/src/app/contact/page.tsx new file mode 100644 index 0000000..3d12ac6 --- /dev/null +++ b/src/app/contact/page.tsx @@ -0,0 +1,544 @@ +'use client' + +import VideoPlayer from '@/components/video-player' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faUser, faEnvelope, faNavicon, faPencil, faHome, faPhone } from '@fortawesome/free-solid-svg-icons' +import { faFacebook, faTwitter, faReddit, faLinkedin, faGoogle } from '@fortawesome/free-brands-svg-icons' +import { Raleway, Pacifico } from '../fonts' +import { useEffect, useState, useCallback } from 'react' +import ContactSection from '@/components/layout/contact-banner' +import 'animate.css' + +interface FormData { + name: string + email: string + subject: string + message: string +} + +type FormStatus = 'idle' | 'submitting' | 'success' | 'error' + +export default function Contact() { + const [bannerVisible, setBannerVisible] = useState(false) + const [formData, setFormData] = useState({ + name: '', + email: '', + subject: '', + message: '', + }) + const [formStatus, setFormStatus] = useState('idle') + const [loadComplete, setLoadComplete] = useState(false) + const [showCheckmark, setShowCheckmark] = useState(false) + + useEffect(() => { + setBannerVisible(true) + }, []) + + const handleChange = useCallback((e: React.ChangeEvent) => { + const { name, value } = e.target + setFormData((prev) => ({ ...prev, [name]: value })) + }, []) + + const isFormValid = + formData.name.trim() !== '' && + formData.email.trim() !== '' && + formData.subject.trim() !== '' && + formData.message.trim() !== '' + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!isFormValid) return + + setFormStatus('submitting') + + try { + const response = await fetch('/api/contact', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData), + }) + + if (!response.ok) throw new Error('Failed to send') + + // Trigger circle-loader -> checkmark animation + setLoadComplete(true) + setTimeout(() => { + setShowCheckmark(true) + setFormStatus('success') + }, 600) + } catch { + setFormStatus('error') + } + } + + const handleRetry = () => { + setFormStatus('idle') + setLoadComplete(false) + setShowCheckmark(false) + } + + return ( +
+ {/* ── Hero Section with Video Background ── */} +
+ {/* Video Background */} +
+
+ +
+ {/* Dark overlay — matches .dark-translucent-bg */} +
+
+ + {/* Hero Content — accounts for 74px fixed header */} +
+
+ {/* Heading */} +

+ Contact Us +

+ + {/* Full-width gradient separator */} +
+ + {/* Description */} +

+ Our team of developers is eager to connect with you. Feel free to reach out with any questions, concerns, + or feedback. Your input is valuable to us, and we'll respond promptly to assist you. +

+
+
+
+ + {/* ── Main Content Section ── */} +
+
+
+ {/* ── Left Column: Form ── */} +
+ {/* Lead paragraph */} +

+ Our team of developers is eager to connect with you. Feel free to reach out with any questions, + concerns, or feedback. Your input is valuable to us, and we'll respond promptly to assist you. +

+ + {/* Success alert */} +
+ We have received your message, we will contact you very soon. +
+ + {/* Error alert */} +
+ Oops! Something went wrong, please verify your information or try again. + +
+ + {/* Contact Form */} +
+
+ {/* Name */} +
+ +
+ + +
+
+ + {/* Email */} +
+ +
+ + +
+
+ + {/* Subject */} +
+ +
+ + +
+
+ + {/* Message */} +
+ +
+