diff --git a/app/contact/page.tsx b/app/contact/page.tsx new file mode 100644 index 0000000..5c748ff --- /dev/null +++ b/app/contact/page.tsx @@ -0,0 +1,208 @@ +import { PageContainer } from "@/components/layout/PageContainer"; +import { QRCodeDisplay } from "@/components/contact/QRCodeDisplay"; +import { JsonLd } from "@/components/seo/JsonLd"; +import { Heading } from "@/components/ui/Heading"; + +import { SITE_URL } from "@/lib/constants"; +import { + createBreadcrumbList, + createSchemaGraph, +} from "@/lib/seo"; +import { paths, urls } from "@/lib/utils/urls"; + +export async function generateMetadata() { + return { + title: "Contact | Builder Vancouver", + description: + "Connect with Builder Vancouver. Follow us on X, Nostr, and Luma. Scan our QR code to share our website.", + keywords: ["contact", "builder vancouver", "bitcoin", "connect"], + }; +} + +/** + * Contact page with real-time QR code and social links + * Optimized for mobile sharing and showcasing work + */ +export default function ContactPage() { + // Use SITE_URL as fallback, QRCodeDisplay will update with actual URL on client + const fallbackUrl = `${SITE_URL}${paths.contact()}`; + + const breadcrumbSchema = createBreadcrumbList([ + { name: "Home", url: urls.home() }, + { name: "Contact" }, + ]); + + const structuredData = createSchemaGraph(breadcrumbSchema); + + // Social links - update these with actual URLs + const socialLinks = [ + { + name: "X (Twitter)", + href: "https://x.com/builder_van", // Update with actual URL + icon: ( + + ), + color: "hover:text-neutral-100", + }, + { + name: "Nostr", + href: "https://nostr.com/builder_van", // Update with actual Nostr profile URL or npub + icon: ( + + ), + color: "hover:text-orange-400", + }, + { + name: "Luma", + href: "https://lu.ma/builder-vancouver", // Update with actual Luma URL + icon: ( + + ), + color: "hover:text-blue-400", + }, + ]; + + return ( + <> + + + {/* Hero Section */} +
+ + Connect With Us + +

+ Join the Builder Vancouver community +

+

+ Scan the QR code or follow us on social media +

+
+ + {/* QR Code Section - Mobile Optimized */} +
+
+ +
+
+ + {/* Social Links Section */} +
+

+ Follow Us +

+
+ {socialLinks.map((social) => ( + +
+
+ {social.icon} +
+

+ {social.name} +

+

+ Connect on {social.name} +

+
+
+ ))} +
+
+ + {/* Contact Information */} +
+

+ Get In Touch +

+
+

+ Email us at{" "} + + bitcoinbuildervan@gmail.com + +

+

+ We typically respond within 48 hours +

+
+
+ + {/* Work Showcase Section */} +
+

+ What We Do +

+
+
+

+ Bitcoin Education +

+

+ Learn about Bitcoin, Lightning Network, and Layer 2 solutions + through our educational sessions and workshops. +

+
+
+

+ Community Events +

+

+ Join our regular meetups, presentations, and networking events + in Vancouver. +

+
+
+

+ Open Source +

+

+ Contribute to Bitcoin-related projects and collaborate with + builders in the ecosystem. +

+
+
+

+ Resources +

+

+ Access presentations, slides, recaps, and educational materials + from our community. +

+
+
+
+
+ + ); +} diff --git a/components/contact/QRCodeDisplay.tsx b/components/contact/QRCodeDisplay.tsx new file mode 100644 index 0000000..104a5b6 --- /dev/null +++ b/components/contact/QRCodeDisplay.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { QRCodeSVG } from "qrcode.react"; + +interface QRCodeDisplayProps { + url: string; +} + +/** + * Client component for QR code display with mobile detection + * Uses the current page URL dynamically for real-time QR code generation + */ +export function QRCodeDisplay({ url }: QRCodeDisplayProps) { + const [currentUrl, setCurrentUrl] = useState(url); + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + // Use the actual current page URL + setCurrentUrl(window.location.href); + + const checkMobile = () => { + setIsMobile(window.innerWidth < 768); + }; + checkMobile(); + window.addEventListener("resize", checkMobile); + return () => window.removeEventListener("resize", checkMobile); + }, []); + + const handleShare = async () => { + if (navigator.share) { + try { + await navigator.share({ + title: "Builder Vancouver", + text: "Check out Builder Vancouver - Bitcoin Meetups & Education", + url: currentUrl, + }); + } catch (err) { + // User cancelled or error occurred + console.log("Share cancelled"); + } + } + }; + + return ( +
+

+ Share This Page +

+ + {/* QR Code Container */} +
+ +
+ + {/* URL Display */} +
+

+ Current URL: +

+
+

+ {currentUrl} +

+
+
+ + {/* Share Button for Mobile */} + {isMobile && typeof navigator !== "undefined" && "share" in navigator && ( + + )} +
+ ); +} diff --git a/components/layout/Footer.tsx b/components/layout/Footer.tsx index 2d7b758..64bdd00 100644 --- a/components/layout/Footer.tsx +++ b/components/layout/Footer.tsx @@ -43,14 +43,15 @@ export function Footer() { { href: paths.cities.list(), label: "Cities" }, { href: paths.sponsors.list(), label: "Sponsors" }, { href: paths.members.list(), label: "Members" }, + { href: paths.contact(), label: "Contact" }, ], }; // Social media links - update these with actual URLs const socialLinks = [ { - name: "Twitter", - href: "#", + name: "X (Twitter)", + href: "https://x.com/builder_van", // Update with actual URL icon: ( ), }, { - name: "GitHub", - href: "#", + name: "Nostr", + href: "https://nostr.com/builder_van", // Update with actual Nostr profile URL icon: ( ), }, { - name: "Nostr", - href: "#", + name: "Luma", + href: "https://lu.ma/builder-vancouver", // Update with actual Luma URL icon: ( ), }, @@ -214,6 +211,14 @@ export function Footer() { ))} +
+ + Contact Us + +

Builder Vancouver

Bitcoin Meetups & Education

diff --git a/components/layout/Navbar.tsx b/components/layout/Navbar.tsx index 146cf3e..e727b18 100644 --- a/components/layout/Navbar.tsx +++ b/components/layout/Navbar.tsx @@ -60,6 +60,7 @@ export function Navbar() { const navItems: NavItem[] = [ { href: "/", label: "Home" }, + { href: "/contact", label: "Contact" }, { label: "About", children: [ @@ -116,6 +117,7 @@ export function Navbar() { { href: "/sponsors", label: "Sponsors" }, { href: "/members", label: "Members" }, { href: "/get-involved", label: "Get Involved" }, + { href: "/contact", label: "Contact" }, ], }, ]; diff --git a/lib/utils/urls.ts b/lib/utils/urls.ts index 2475273..e1ed557 100644 --- a/lib/utils/urls.ts +++ b/lib/utils/urls.ts @@ -94,6 +94,7 @@ export const urls = { }, faq: () => buildUrl("/faq"), + contact: () => buildUrl("/contact"), page: (slug: string) => buildUrl(`/${slug}`), } as const; @@ -169,6 +170,7 @@ export const paths = { }, faq: () => "/faq", + contact: () => "/contact", page: (slug: string) => `/${slug}`, } as const; diff --git a/package-lock.json b/package-lock.json index f620fef..2b4af1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "leaflet": "^1.9.4", "next": "16.0.3", + "qrcode.react": "^4.2.0", "react": "19.2.0", "react-dom": "19.2.0", "react-leaflet": "^5.0.0", @@ -6831,6 +6832,15 @@ "node": ">=6" } }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/package.json b/package.json index 333eb0f..c765023 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dependencies": { "leaflet": "^1.9.4", "next": "16.0.3", + "qrcode.react": "^4.2.0", "react": "19.2.0", "react-dom": "19.2.0", "react-leaflet": "^5.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9318211..3d2452e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: next: specifier: 16.0.3 version: 16.0.3(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + qrcode.react: + specifier: ^4.2.0 + version: 4.2.0(react@19.2.0) react: specifier: 19.2.0 version: 19.2.0 @@ -2017,6 +2020,11 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qrcode.react@4.2.0: + resolution: {integrity: sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -4375,6 +4383,10 @@ snapshots: punycode@2.3.1: {} + qrcode.react@4.2.0(react@19.2.0): + dependencies: + react: 19.2.0 + queue-microtask@1.2.3: {} react-dom@19.2.0(react@19.2.0):