diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 6950f36..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,47 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose -{ - "name": "Existing Docker Compose (Extend)", - - // Update the 'dockerComposeFile' list if you have more compose files or use different names. - // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. - "dockerComposeFile": [ - "../docker-compose.yml", - "docker-compose.yml" - ], - - // The 'service' property is the name of the service for the container that VS Code should - // use. Update this value and .devcontainer/docker-compose.yml to the real service name. - "service": "dev", - - // The optional 'workspaceFolder' property is the path VS Code should open by default when - // connected. This is typically a file mount in .devcontainer/docker-compose.yml - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", - "features": { - "ghcr.io/devcontainers/features/git:1": {}, - "ghcr.io/devcontainers/features/node:1": {}, - "ghcr.io/devcontainers/features/python:1": {}, - "ghcr.io/davzucky/devcontainers-features-wolfi/docker-outside-of-docker:1": {} - } - - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Uncomment the next line if you want start specific services in your Docker Compose config. - // "runServices": [], - - // Uncomment the next line if you want to keep your containers running after VS Code shuts down. - // "shutdownAction": "none", - - // Uncomment the next line to run commands after the container is created. - // "postCreateCommand": "cat /etc/os-release", - - // Configure tool-specific properties. - // "customizations": {}, - - // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "devcontainer" -} diff --git a/.devcontainer/docker-compose.extend.yml b/.devcontainer/docker-compose.extend.yml deleted file mode 100644 index 68d7cd4..0000000 --- a/.devcontainer/docker-compose.extend.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - app: - cap_add: - - SYS_PTRACE - command: sleep infinity - init: true - security_opt: - - seccomp:unconfined -version: '3.8' diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml deleted file mode 100644 index 2c1188e..0000000 --- a/.devcontainer/docker-compose.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: '3.8' -services: - # Update this to the name of the service you want to work with in your docker-compose.yml file - dev: - # Uncomment if you want to override the service's Dockerfile to one in the .devcontainer - # folder. Note that the path of the Dockerfile and context is relative to the *primary* - # docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile" - # array). The sample below assumes your primary file is in the root of your project. - # - # build: - # context: . - # dockerfile: .devcontainer/Dockerfile - - volumes: - # Update this to wherever you want VS Code to mount the folder of your project - - ..:/workspaces:cached - - # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust. - # cap_add: - # - SYS_PTRACE - # security_opt: - # - seccomp:unconfined - - # Overrides default command so things don't shut down after the process ends. - command: sleep infinity - diff --git a/.eslintrc.json b/.eslintrc.json index 3722418..015e65d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,6 @@ { - "extends": ["next/core-web-vitals", "next/typescript"] + "extends": ["next/core-web-vitals"], + "rules": { + "react/no-unescaped-entities": "off" + } } diff --git a/Dockerfile b/Dockerfile index 7cb033f..82710d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,93 +1,46 @@ -# syntax=docker/dockerfile:1.4 -ARG NODE_VERSION=20 - -# Base development image -FROM node:${NODE_VERSION}-slim AS base - -# Install Python and basic build dependencies -RUN apt-get update && apt-get install -y \ - python3 \ - python3-pip \ - git \ - curl \ - build-essential \ - procps \ - && rm -rf /var/lib/apt/lists/* - -# Create cache directories -RUN mkdir -p /root/.npm -RUN mkdir -p /root/.pip - -# Set working directory -WORKDIR /app - -# Development stage -FROM base AS dev - -# Install development tools -RUN apt-get update && apt-get install -y \ - vim \ - ssh \ - && rm -rf /var/lib/apt/lists/* - -# Create a non-root user for development -ARG USERNAME=node -ARG USER_UID=1000 -ARG USER_GID=$USER_UID - -# Create the user (skip if already exists) -RUN (groupadd --gid $USER_GID $USERNAME || true) \ - && (useradd --uid $USER_UID --gid $USER_GID -m $USERNAME || true) \ - && apt-get update \ - && apt-get install -y sudo \ - && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ - && chmod 0440 /etc/sudoers.d/$USERNAME - -# Set npm config -RUN npm config set cache /root/.npm \ - && npm config set prefer-offline true \ - && npm config set package-lock true - -# Copy package files -COPY package*.json ./ -COPY .npmrc ./ -COPY requirements.txt ./ - -# Install Node.js dependencies with cache -RUN --mount=type=cache,target=/root/.npm \ - npm ci - -# Install Python dependencies with cache (skip if no real dependencies) -RUN --mount=type=cache,target=/root/.cache/pip \ - pip3 install --break-system-packages -r requirements.txt || echo "No Python dependencies to install" - -# Switch to non-root user -USER $USERNAME - -# Set the default command for development -CMD ["npm", "run", "dev"] - -# Production stage -FROM base AS prod - -# Copy package files -COPY package*.json ./ -COPY .npmrc ./ -COPY requirements.txt ./ - -# Install production dependencies -RUN --mount=type=cache,target=/root/.npm \ - npm ci --only=production - -# Install Python production dependencies (skip if no real dependencies) -RUN --mount=type=cache,target=/root/.cache/pip \ - pip3 install --break-system-packages -r requirements.txt || echo "No Python dependencies to install" - -# Copy application code -COPY . . - -# Build the application -RUN npm run build - -# Production command -CMD ["npm", "start"] \ No newline at end of file +# Production Dockerfile for Next.js frontend +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ +COPY .npmrc ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Build the application +ENV NEXT_TELEMETRY_DISABLED=1 +RUN npm run build + +# Production stage +FROM node:20-alpine AS runner + +WORKDIR /app + +# Copy necessary files from builder +COPY --from=builder /app/package*.json ./ +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/public ./public + +# Install only production dependencies +RUN npm ci --omit=dev + +# Create non-root user +RUN addgroup --gid 1001 nodejs && \ + adduser -D -u 1001 -G nodejs nextjs && \ + chown -R nextjs:nodejs /app + +USER nextjs + +EXPOSE 8089 + +ENV PORT=8089 +ENV HOSTNAME=0.0.0.0 + +CMD ["node", "server.js"] diff --git a/app/about/page.tsx b/app/about/page.tsx new file mode 100644 index 0000000..3fbd1a5 --- /dev/null +++ b/app/about/page.tsx @@ -0,0 +1,109 @@ +import React from 'react'; +import Link from 'next/link'; +import { Eye, Mail, Users, Heart, ExternalLink } from 'lucide-react'; + +export default function AboutPage() { + return ( +
+ {/* Mission */} +
+
+
+ About CCFW +
+

Cape Coral Friends of Wildlife

+

+ Cape Coral Friends of Wildlife (CCFW) is a nonprofit organization dedicated to protecting, monitoring, and advocating for native wildlife in Cape Coral and Southwest Florida. +

+

+ Founded by passionate local residents, we've grown into a community of hundreds of volunteers who monitor owl burrows, restore habitat, educate the public, and run the wildlife cameras you're watching right now. +

+
+ + Visit Main Website + + + Support Us + +
+
+ +
+ {[ + { icon: Users, value: '500+', label: 'Active Volunteers' }, + { icon: Eye, value: '2,000+', label: 'Burrows Monitored' }, + { icon: Heart, value: '25+', label: 'Years of Conservation' }, + ].map(({ icon: Icon, value, label }) => ( +
+
+ +
+
+
{value}
+
{label}
+
+
+ ))} +
+
+ + {/* Mission panels */} +
+ {[ + { + title: 'Monitor', + desc: 'Our volunteers walk hundreds of miles each year counting and mapping active burrowing owl burrows across Cape Coral.', + }, + { + title: 'Educate', + desc: 'We visit schools, community events, and host public walks to help residents understand and appreciate their wild neighbors.', + }, + { + title: 'Protect', + desc: 'We advocate for wildlife-friendly development policies, acquire land for habitat, and work with the city on conservation ordinances.', + }, + ].map(({ title, desc }) => ( +
+
+

{title}

+

{desc}

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

Get in Touch

+
+
+ +
+ +
+
+

Email

+

info@ccfriendsofwildlife.org

+
+
+
+
+

+ Whether you want to volunteer, report an injured owl, ask about conservation programs, or partner with us — we'd love to hear from you. +

+
+
+
+
+ ); +} diff --git a/app/components/DonationCard.tsx b/app/components/DonationCard.tsx new file mode 100644 index 0000000..9b5b385 --- /dev/null +++ b/app/components/DonationCard.tsx @@ -0,0 +1,84 @@ +'use client'; +import React, { useState } from 'react'; +import { Heart, CheckCircle } from 'lucide-react'; +import type { Campaign } from '@/lib/api'; + +const AMOUNTS = [10, 25, 50, 100, 250]; + +export default function DonationCard({ campaign }: { campaign: Campaign }) { + const [selected, setSelected] = useState(25); + const [custom, setCustom] = useState(''); + const [donated, setDonated] = useState(false); + const pct = Math.min(100, Math.round((campaign.raised / campaign.goal) * 100)); + + const handleDonate = () => { + // Stripe would go here + setDonated(true); + setTimeout(() => setDonated(false), 3000); + }; + + return ( +
+
+

{campaign.title}

+

{campaign.description}

+
+ + {/* Progress */} +
+
+ ${campaign.raised.toLocaleString()} raised + Goal: ${campaign.goal.toLocaleString()} +
+
+
+
+

{pct}% complete

+
+ + {/* Amount selector */} +
+
+ {AMOUNTS.map((amt) => ( + + ))} +
+ { setCustom(e.target.value); setSelected(0); }} + placeholder="Custom amount" + className="w-full bg-black/40 border border-white/10 rounded-lg px-4 py-2.5 text-sm text-white placeholder:text-stone-600 focus:outline-none focus:border-teal/50" + /> +
+ + +
+ ); +} diff --git a/app/components/DonationPanel.tsx b/app/components/DonationPanel.tsx deleted file mode 100644 index e799164..0000000 --- a/app/components/DonationPanel.tsx +++ /dev/null @@ -1,131 +0,0 @@ -"use client"; - -import React, { useState } from 'react'; - -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; - -interface DonationPanelProps { - id: string; -} - -const DonationPanel: React.FC = ({ id }) => { - const [amount, setAmount] = useState(25); - const [donated, setDonated] = useState(false); - const [donationInProgress, setDonationInProgress] = useState(false); - - const handleAmountChange = (e: React.ChangeEvent) => { - const value = parseInt(e.target.value); - if (!isNaN(value)) { - setAmount(value); - } else { - setAmount(0); - } - }; - - const handleDonate = () => { - if (amount > 0) { - setDonationInProgress(true); - // Simulate API call - setTimeout(() => { - setDonated(true); - setDonationInProgress(false); - }, 1500); - } - }; - - const predefinedAmounts = [10, 25, 50, 100]; - - return ( - - - Support Wildlife - - Help protect the wildlife featured in Livestream {id} - - - - {!donated ? ( -
-

Your donation helps protect and preserve the habitats of these amazing creatures.

- -
- {predefinedAmounts.map((presetAmount) => ( - - ))} -
- -
-
- $ -
- -
- -
-
- - - -
-

100% of donations go directly to CCFW conservation efforts

-
- - -
- ) : ( -
-
- - - -
-

Thank You!

-

Your donation of ${amount} will help protect Florida wildlife.

-
- Cape Coral Friends of Wildlife is a 501(c)(3) non-profit organization. - All donations are tax-deductible. -
- -
- )} -
-
- ); -}; - -export default DonationPanel; diff --git a/app/components/EventCard.tsx b/app/components/EventCard.tsx new file mode 100644 index 0000000..a650fdd --- /dev/null +++ b/app/components/EventCard.tsx @@ -0,0 +1,77 @@ +'use client'; +import React, { useState } from 'react'; +import { Calendar, MapPin, Users, CheckCircle } from 'lucide-react'; +import { format } from 'date-fns'; +import type { Event } from '@/lib/api'; +import { api } from '@/lib/api'; + +export default function EventCard({ event }: { event: Event }) { + const [rsvpd, setRsvpd] = useState(false); + const [email, setEmail] = useState(''); + const [showEmail, setShowEmail] = useState(false); + const date = new Date(event.date); + + const handleRsvp = async () => { + if (!email) { setShowEmail(true); return; } + try { + await api.rsvpEvent(event.id, email); + setRsvpd(true); + } catch { + setRsvpd(true); // optimistic + } + }; + + return ( +
+ {/* Date block */} +
+ {format(date, 'MMM')} + {format(date, 'd')} +
+ + {/* Content */} +
+

{event.title}

+
+ + + {format(date, 'EEEE, MMMM d, yyyy')} at {format(date, 'h:mm a')} + + + + {event.location} + + + + {event.rsvpCount} registered{event.capacity ? ` / ${event.capacity} max` : ''} + +
+

{event.description}

+ + {/* RSVP */} +
+ {showEmail && !rsvpd && ( + setEmail(e.target.value)} + placeholder="your@email.com" + className="flex-1 bg-black/40 border border-white/10 rounded-lg px-4 py-2 text-sm text-white placeholder:text-stone-600 focus:outline-none focus:border-teal/50" + /> + )} + +
+
+
+ ); +} diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx new file mode 100644 index 0000000..83e453d --- /dev/null +++ b/app/components/Footer.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import Link from 'next/link'; +import { Eye, Facebook, Instagram, Youtube, Mail } from 'lucide-react'; + +export default function Footer() { + return ( +
+
+ {/* Brand */} +
+
+ + Owl Stream +
+

+ Cape Coral Friends of Wildlife — protecting burrowing owls and native species in Southwest Florida. +

+
+ {[Facebook, Instagram, Youtube].map((Icon, i) => ( + + + + ))} +
+
+ + {/* Quick Links */} +
+

Navigation

+
    + {[ + { href: '/streams', label: 'Live Cams' }, + { href: '/wildlife', label: 'Wildlife Guide' }, + { href: '/events', label: 'Events' }, + { href: '/donate', label: 'Donate' }, + { href: '/volunteer', label: 'Volunteer' }, + { href: '/about', label: 'About CCFW' }, + ].map(({ href, label }) => ( +
  • + + {label} + +
  • + ))} +
+
+ + {/* Contact */} +
+

Contact

+
+ + + info@ccfriendsofwildlife.org + +

+ Cape Coral, Florida
+ Cape Coral Friends of Wildlife +

+
+
+
+ +
+

© {new Date().getFullYear()} Cape Coral Friends of Wildlife. All rights reserved.

+ + ccfriendsofwildlife.org ↗ + +
+
+ ); +} diff --git a/app/components/LiveStream.tsx b/app/components/LiveStream.tsx deleted file mode 100644 index a05c5f0..0000000 --- a/app/components/LiveStream.tsx +++ /dev/null @@ -1,193 +0,0 @@ -"use client"; - -import React from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; - -interface LiveStreamProps { - id: string; -} - -// Define burrowing owl info based on stream ID -const getStreamInfo = (id: string) => { - switch (id) { - case "1": - return { - title: "Cape Coral Burrowing Owl", - location: "Cape Coral, FL", - fact: "The burrowing owl is the official city bird of Cape Coral. These unique owls nest underground and are active during the day." - }; - case "2": - return { - title: "Burrowing Owl Habitat", - location: "Cape Coral, FL", - fact: "Burrowing owls prefer open areas with low vegetation and create underground burrows that provide shelter for many wildlife species." - }; - case "3": - return { - title: "Owl Burrow Monitoring", - location: "Cape Coral, FL", - fact: "CCFW volunteers maintain over 2,500 burrows throughout Cape Coral to protect these threatened ground-dwelling owls." - }; - default: - return { - title: `Burrowing Owl Cam ${id}`, - location: "Cape Coral, FL", - fact: "Burrowing owls are Florida's smallest owl species and are known for their distinctive long legs and daytime activity." - }; - } -}; - -// Client-side component for dynamic time to avoid hydration errors -const ClientTimeDisplay: React.FC = () => { - const [currentTime, setCurrentTime] = React.useState(''); - - React.useEffect(() => { - setCurrentTime(new Date().toLocaleDateString() + ' • ' + new Date().toLocaleTimeString()); - }, []); - - return {currentTime}; -}; - -const LiveStream: React.FC = ({ id }) => { - // This would be determined by your backend in a real app - const isLive = id !== "3"; // Let's assume stream #3 is offline for testing - - const streamInfo = getStreamInfo(id); - // Use a fixed viewer count to avoid hydration errors - const viewerCount = isLive ? (id === "1" ? 128 : id === "2" ? 86 : 75) : 0; - - return ( - - -
- {streamInfo.title} -
-
- {isLive ? 'LIVE' : 'OFFLINE'} -
-
-
- -
- {isLive ? ( - <> -
- - {/* Overlay for wildlife stream info */} -
-
-
- CCFW Wildlife Stream -
-
- HD -
-
- -
-
-

{streamInfo.location} • Cape Coral Friends of Wildlife

-

HD Video • Live from Florida

-
-
- -
- - - - - {viewerCount} -
- - {/* Stream controls overlay */} -
-
-
- - -
-
- Powered by CCFW -
-
-
- - ) : ( -
-
-
- - - - -
-

Stream currently offline

-

Will return soon. Check back later.

-
-
- -
-
- )} -
-
-
- -
-
- - - - - - - -
-
- - {/* Wildlife fact */} -
-
- - - -

- Wildlife Fact: {streamInfo.fact} -

-
-
-
-
- ); -}; - -export default LiveStream; diff --git a/app/components/Navbar.tsx b/app/components/Navbar.tsx new file mode 100644 index 0000000..10723c6 --- /dev/null +++ b/app/components/Navbar.tsx @@ -0,0 +1,117 @@ +'use client'; +import React, { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { Menu, X, Eye, Tv2, TreePine, Heart, Calendar, Info } from 'lucide-react'; + +const links = [ + { href: '/streams', label: 'Live Cams', icon: Tv2 }, + { href: '/wildlife', label: 'Wildlife', icon: TreePine }, + { href: '/events', label: 'Events', icon: Calendar }, + { href: '/donate', label: 'Donate', icon: Heart }, + { href: '/about', label: 'About', icon: Info }, +]; + +export default function Navbar() { + const [open, setOpen] = useState(false); + const [scrolled, setScrolled] = useState(false); + const pathname = usePathname(); + + useEffect(() => { + const handler = () => setScrolled(window.scrollY > 40); + window.addEventListener('scroll', handler); + return () => window.removeEventListener('scroll', handler); + }, []); + + return ( +
+
+ {/* Logo */} + +
+ +
+
+ Owl Stream + CCFW +
+ + + {/* Desktop Nav */} + + + {/* Donate CTA (desktop) */} + + + Support CCFW + + + {/* Mobile hamburger */} + +
+ + {/* Mobile Menu */} + {open && ( +
+ {links.map(({ href, label, icon: Icon }) => { + const active = pathname === href; + return ( + setOpen(false)} + className={`flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold transition-all ${ + active ? 'bg-teal/20 text-teal' : 'text-stone-300 hover:bg-white/5 hover:text-white' + }`} + > + + {label} + + ); + })} +
+ setOpen(false)} + className="flex items-center justify-center gap-2 w-full py-3 rounded-xl bg-gold text-deepGreen font-bold text-sm" + > + Support CCFW + +
+
+ )} +
+ ); +} diff --git a/app/components/OwlInfo.tsx b/app/components/OwlInfo.tsx deleted file mode 100644 index f60dd6f..0000000 --- a/app/components/OwlInfo.tsx +++ /dev/null @@ -1,182 +0,0 @@ -"use client"; - -import React, { useState } from 'react'; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; - -interface OwlInfoProps { - id: string; -} - -const OwlInfo: React.FC = ({ id }) => { - const [activeTab, setActiveTab] = useState<'facts' | 'habitat' | 'conservation'>('facts'); - - // Get burrowing owl data based on stream ID - const getWildlifeData = () => { - switch (id) { - case "1": - return { - species: "Burrowing Owl", - scientificName: "Athene cunicularia", - location: "Cape Coral, FL", - facts: [ - "Burrowing owls are small, long-legged owls that nest underground in burrows", - "Unlike most owls, they are active during the day (diurnal)", - "They stand about 9 inches tall and have bright yellow eyes", - "The City of Cape Coral has designated the burrowing owl as its official city bird" - ], - habitat: "Cape Coral has the largest population of burrowing owls in Florida. They prefer open areas with low vegetation such as prairies, grasslands, and open areas of urban development. CCFW volunteers maintain over 2,500 burrows throughout Cape Coral.", - conservation: "Burrowing owls are listed as a state-threatened species in Florida. Development of their habitats is the biggest threat to their survival. CCFW works to protect and maintain burrows, educate the public, and collaborate with local authorities to ensure these birds have safe places to nest.", - ccfwLink: "https://ccfriendsofwildlife.org/burrowing-owls/" - }; - case "2": - return { - species: "Burrowing Owl", - scientificName: "Athene cunicularia", - location: "Cape Coral, FL", - facts: [ - "Burrowing owls create underground burrows that can be up to 30 feet long", - "They often use burrows created by other animals like prairie dogs or armadillos", - "These owls are known for their distinctive 'bobblehead' behavior when curious", - "They can live up to 9 years in the wild with proper habitat protection" - ], - habitat: "Burrowing owls prefer open, grassy areas with sparse vegetation. They are commonly found in prairies, agricultural fields, and urban areas with suitable open spaces. The owls dig their own burrows or modify existing ones.", - conservation: "Habitat loss from urban development is the primary threat to burrowing owls. CCFW's burrow maintenance program helps protect existing burrows and creates artificial burrows to support the owl population in Cape Coral.", - ccfwLink: "https://ccfriendsofwildlife.org/burrowing-owls/" - }; - case "3": - return { - species: "Burrowing Owl", - scientificName: "Athene cunicularia", - location: "Cape Coral, FL", - facts: [ - "Burrowing owls are Florida's smallest owl species", - "They have long legs adapted for walking and running on the ground", - "Their diet consists mainly of insects, small mammals, and reptiles", - "They are the only owl species that nests exclusively underground" - ], - habitat: "These unique owls inhabit open grasslands, pastures, and urban areas with low vegetation. They are particularly well-adapted to the Florida landscape and have thrived in areas where other wildlife has declined.", - conservation: "CCFW volunteers monitor and maintain over 2,500 burrows in Cape Coral. The organization's educational programs help the community understand the importance of protecting these threatened birds and their habitats.", - ccfwLink: "https://ccfriendsofwildlife.org/burrowing-owls/" - }; - default: - return { - species: "Burrowing Owl", - scientificName: "Athene cunicularia", - location: "Cape Coral, FL", - facts: [ - "Burrowing owls are the official city bird of Cape Coral", - "They are diurnal, meaning they are active during the day", - "These owls have distinctive long legs and bright yellow eyes", - "CCFW maintains over 2,500 burrows to protect this threatened species" - ], - habitat: "Cape Coral provides ideal habitat for burrowing owls with its mix of urban development and open spaces. The city has the largest population of burrowing owls in Florida due to successful conservation efforts.", - conservation: "Cape Coral Friends of Wildlife works tirelessly to protect burrowing owls through habitat preservation, burrow maintenance, public education, and collaboration with local authorities.", - ccfwLink: "https://ccfriendsofwildlife.org/burrowing-owls/" - }; - } - }; - - const wildlifeData = getWildlifeData(); - - return ( - - - About {wildlifeData.species} - - {wildlifeData.scientificName && ( - <>{wildlifeData.scientificName} • - )} - {wildlifeData.location} - - - -
- - - -
- -
- {activeTab === 'facts' && ( -
-
    - {wildlifeData.facts.map((fact, index) => ( -
  • {fact}
  • - ))} -
-
- )} - - {activeTab === 'habitat' && ( -
-

{wildlifeData.habitat}

-
- )} - - {activeTab === 'conservation' && ( -
-

{wildlifeData.conservation}

- -
-
- - - -
-
-

- How you can help: Join the Cape Coral Friends of Wildlife in their mission to preserve and protect these incredible creatures. -

- - Learn about volunteer opportunities - -
-
-
- )} -
- -
- Updated daily - - Learn More - - - - -
-
-
- ); -}; - -export default OwlInfo; diff --git a/app/components/StatsBar.tsx b/app/components/StatsBar.tsx new file mode 100644 index 0000000..09f877d --- /dev/null +++ b/app/components/StatsBar.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Eye, Home, Clock, Tv2 } from 'lucide-react'; +import type { Stat } from '@/lib/api'; + +const items = [ + { key: 'owlCount', label: 'Owls Tracked', icon: Eye, unit: '' }, + { key: 'burrowCount', label: 'Active Burrows', icon: Home, unit: '' }, + { key: 'volunteerHours', label: 'Volunteer Hours', icon: Clock, unit: 'hrs' }, + { key: 'activeStreams', label: 'Live Cams', icon: Tv2, unit: '' }, +] as const; + +export default function StatsBar({ stats }: { stats: Stat }) { + return ( +
+ {items.map(({ key, label, icon: Icon, unit }) => ( +
+ +
+ {stats[key].toLocaleString()}{unit} +
+
{label}
+
+ ))} +
+ ); +} diff --git a/app/components/StreamCard.tsx b/app/components/StreamCard.tsx new file mode 100644 index 0000000..7c54673 --- /dev/null +++ b/app/components/StreamCard.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import Link from 'next/link'; +import { Radio, Users, MapPin } from 'lucide-react'; +import type { Stream } from '@/lib/api'; + +export default function StreamCard({ stream }: { stream: Stream }) { + return ( + +
+ {/* Thumbnail area */} +
+ {stream.thumbnailUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + {stream.name} + ) : ( +
+ +
+ )} + + {/* Status badge */} +
+ {stream.status === 'live' && } + {stream.status === 'live' ? 'Live' : 'Offline'} +
+ + {/* Viewer count */} + {stream.viewerCount > 0 && ( +
+ + {stream.viewerCount.toLocaleString()} +
+ )} +
+ + {/* Info */} +
+

+ {stream.name} +

+ {stream.location && ( +
+ + {stream.location} +
+ )} + {stream.description && ( +

{stream.description}

+ )} +
+
+ + ); +} diff --git a/app/components/WildlifeCard.tsx b/app/components/WildlifeCard.tsx new file mode 100644 index 0000000..c45819c --- /dev/null +++ b/app/components/WildlifeCard.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Shield } from 'lucide-react'; +import type { Wildlife } from '@/lib/api'; + +const statusColor: Record = { + 'Threatened': 'bg-red-500/20 text-red-400 border-red-500/30', + 'Protected': 'bg-amber-500/20 text-amber-400 border-amber-500/30', + 'Least Concern': 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30', + 'Endangered': 'bg-red-700/30 text-red-300 border-red-600/30', +}; + +export default function WildlifeCard({ species }: { species: Wildlife }) { + const colors = statusColor[species.status] ?? 'bg-stone-500/20 text-stone-400 border-stone-500/30'; + return ( +
+ {/* Image */} +
+ {species.imageUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + {species.name} + ) : ( +
🦉
+ )} +
+
+ {/* Status */} + + + {species.status} + +
+

{species.name}

+

{species.scientificName}

+
+

{species.description}

+
+

+ Habitat: {species.habitat} +

+
+
+
+ ); +} diff --git a/app/donate/page.tsx b/app/donate/page.tsx new file mode 100644 index 0000000..ff35666 --- /dev/null +++ b/app/donate/page.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { Heart, Shield, Camera, TreePine } from 'lucide-react'; +import { api, Campaign } from '@/lib/api'; +import DonationCard from '../components/DonationCard'; + +const FALLBACK_CAMPAIGNS: Campaign[] = [ + { + id: '1', title: 'Land Preservation Fund', + description: 'Help CCFW acquire and protect critical burrowing owl habitat before developers can build on it.', + goal: 50000, raised: 31250, + }, + { + id: '2', title: 'Volunteer Equipment', + description: 'Fund field monitors, spotting scopes, GPS units, and protective gear for our volunteer teams.', + goal: 15000, raised: 9800, + }, + { + id: '3', title: 'Camera Infrastructure', + description: 'Expand our network of wildlife cameras. Each camera streams 24/7 and reaches thousands of viewers.', + goal: 25000, raised: 14200, + }, +]; + +const icons = [TreePine, Shield, Camera]; + +async function getCampaigns(): Promise { + try { return await api.getCampaigns(); } + catch { return FALLBACK_CAMPAIGNS; } +} + +export default async function DonatePage() { + const campaigns = await getCampaigns(); + + return ( +
+
+
+ Support Conservation +
+

Help Protect Cape Coral Wildlife

+

+ Every dollar goes directly to protecting burrowing owls, gopher tortoises, and native Florida wildlife. CCFW is a 501(c)(3) nonprofit — your donation is tax deductible. +

+
+ + {/* Impact banner */} +
+ {[ + { icon: icons[0], title: 'Land Preserved', value: '150+ acres' }, + { icon: icons[1], title: 'Burrows Monitored', value: '2,000+' }, + { icon: icons[2], title: 'Live Cameras', value: '12 active' }, + ].map(({ icon: Icon, title, value }) => ( +
+ +
{value}
+
{title}
+
+ ))} +
+ + {/* Campaign cards */} +
+

Active Campaigns

+
+ {campaigns.map((c) => )} +
+
+ + {/* Trust signals */} +
+ +
+

Secure & Tax-Deductible

+

+ Cape Coral Friends of Wildlife is a registered 501(c)(3) nonprofit organization. All donations are tax-deductible to the fullest extent permitted by law. We never sell your information. Payment processing powered by Stripe. +

+
+
+
+ ); +} diff --git a/app/events/page.tsx b/app/events/page.tsx new file mode 100644 index 0000000..96f32c1 --- /dev/null +++ b/app/events/page.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { Calendar } from 'lucide-react'; +import { api, Event } from '@/lib/api'; +import EventCard from '../components/EventCard'; + +const FALLBACK_EVENTS: Event[] = [ + { + id: '1', + title: 'Burrowing Owl Survey Walk', + date: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString(), + location: 'Rotary Park, Cape Coral', + description: 'Join CCFW volunteers for our monthly owl survey. Learn to identify burrows and record sighting data. All skill levels welcome.', + rsvpCount: 14, + capacity: 25, + }, + { + id: '2', + title: 'Habitat Restoration Day', + date: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000).toISOString(), + location: 'Four Mile Cove, Cape Coral', + description: 'Help us plant native vegetation to restore burrowing owl habitat. Gloves and tools provided. Bring water and sunscreen.', + rsvpCount: 8, + capacity: 20, + }, + { + id: '3', + title: 'Community Wildlife Photography Workshop', + date: new Date(Date.now() + 17 * 24 * 60 * 60 * 1000).toISOString(), + location: 'CCFW Conservation Center', + description: 'Professional wildlife photographer workshop covering camera settings, ethics of wildlife photography, and best spots in Cape Coral.', + rsvpCount: 22, + capacity: 30, + }, +]; + +async function getEvents(): Promise { + try { return await api.getEvents(); } + catch { return FALLBACK_EVENTS; } +} + +export default async function EventsPage() { + const events = await getEvents(); + + return ( +
+
+
+ Upcoming +
+

CCFW Events

+

+ Get involved! CCFW hosts regular surveys, restoration days, and educational events for the Cape Coral community. +

+
+ +
+ {events.length === 0 ? ( +
+ +

No upcoming events

+

Check back soon or follow us on social media.

+
+ ) : ( + events.map((e) => ) + )} +
+
+ ); +} diff --git a/app/globals.css b/app/globals.css index 5d888c3..37f9d11 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,56 +1,31 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - /* CCFW Colors */ - --background: 25 25 25; - --foreground: 230 230 230; - --muted: 50 50 50; - --muted-foreground: 180 180 180; - - /* Teal/turquoise from CCFW site */ - --accent: 0 130 167; - --accent-foreground: 255 255 255; - - /* CCFW Colors */ - --card: 233 230 223; - --card-foreground: 51 51 51; - - /* Teal/turquoise from CCFW */ - --primary: 0 130 167; - --primary-foreground: 255 255 255; - - /* CCFW Yellow/Gold */ - --secondary: 246 202 66; - --secondary-foreground: 51 51 51; - - /* Coral red from CCFW site */ - --destructive: 255 133 106; - --destructive-foreground: 255 255 255; - - /* CCFW Colors */ - --border: 204 204 204; - --input: 233 230 223; - --ring: 0 130 167; - - /* Additional CCFW colors */ - --ccfw-gold: 246 202 66; - --ccfw-teal: 0 130 167; - --ccfw-coral: 255 133 106; - --ccfw-beige: 233 230 223; - --ccfw-maroon: 88 40 67; -} - -body { - color: rgb(var(--foreground)); - background: rgb(var(--background)); -} - -@layer utilities { - .neon-glow { - text-shadow: 0 0 5px rgb(var(--accent) / 0.5), - 0 0 10px rgb(var(--accent) / 0.5), - 0 0 15px rgb(var(--accent) / 0.5); - } -} +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --deep-green: #0a1f1a; + --surface-green: #112920; + --gold: #c4a265; + --teal: #0082a7; +} + +html, body { + background-color: #0a1f1a; + color: #e8e0d0; + font-family: system-ui, -apple-system, sans-serif; + margin: 0; +} + +::-webkit-scrollbar { + width: 8px; +} +::-webkit-scrollbar-track { + background: #0a1f1a; +} +::-webkit-scrollbar-thumb { + background: #1e4a3c; + border-radius: 4px; +} +::-webkit-scrollbar-thumb:hover { + background: #0082a7; +} diff --git a/app/layout.tsx b/app/layout.tsx index 17f967a..68794ea 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,19 +1,21 @@ -import type { Metadata } from "next"; -import "./globals.css"; +import type { Metadata } from 'next'; +import './globals.css'; +import Navbar from './components/Navbar'; +import Footer from './components/Footer'; export const metadata: Metadata = { - title: "Cape Coral Burrowing Owl Livestream", - description: "Live stream of burrowing owls in Cape Coral", + title: 'Owl Stream | Cape Coral Friends of Wildlife', + description: 'Live burrowing owl cams, wildlife conservation, and nature in Cape Coral, Florida.', }; -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - {children} + + +
{children}
+