Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"@typescript-eslint/no-namespace": "off",
"import/no-deprecated": "error",
"import/order": "off",
"import/extensions": "off",
"import/no-extraneous-dependencies": [
"error",
{
Expand Down
3 changes: 2 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BrowserRouter as Router, Routes, Route, Outlet } from 'react-router-dom
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

import LandingPage from './components/LandingPage'
import LoginPage from './components/LoginPage'
import SearchInterface from './components/SearchInterface'
import SnippetDetail from './components/SnippetDetail'
Expand Down Expand Up @@ -36,6 +37,7 @@ export default function App(): ReactElement {
<LanguageProvider>
<Router>
<Routes>
<Route path={ROOT_PATH} element={<LandingPage /> as ReactElement} />
<Route path={ONBOARDING_PATH} element={<OnboardingPage />} />
<Route path={LOGIN_PATH} element={<LoginPage />} />
<Route path={FORGET_PASSWORD_PATH} element={<ForgetPassword />} />
Expand All @@ -45,7 +47,6 @@ export default function App(): ReactElement {
<Route path={SEARCH_PATH} element={<SearchInterface />} />
<Route path={SNIPPET_DETAIL_PATH} element={<SnippetDetail />} />
</Route>
<Route path='*' element={<LoginPage />} />
</Routes>
</Router>
</LanguageProvider>
Expand Down
161 changes: 161 additions & 0 deletions src/components/LandingPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { PlayCircle } from "lucide-react"
import { Link, useNavigate } from "react-router-dom"
import { useLandingPageContent } from "@/hooks/useLandingPageContent"
import { LOGIN_PATH, ONBOARDING_PATH } from "@/constants/routes"

// Example snippets - in a real app, these would likely come from the API too
const snippets = [
{
titleEn: "Warning Against Amendment 4 in Florida",
titleEs: "Advertencia Contra la Enmienda 4 en Florida",
tags: ["Abortion and Reproductive Rights"],
},
// ... other snippets
]

export default function LandingPage(): ReactElement {
const navigate = useNavigate()
const [scrollPosition, setScrollPosition] = useState(0)
const [language, setLanguage] = useState<'en' | 'es'>('en') // In real app, this would come from a language context/store

useEffect(() => {
let animationFrameId: number;
let lastTimestamp: number;

const animate = (timestamp: number) => {
if (!lastTimestamp) lastTimestamp = timestamp;
const elapsed = timestamp - lastTimestamp;

if (elapsed > 40) {
setScrollPosition((prev) => {
const maxScroll = snippets.length * 120; // Approximate height of each card
return (prev + 1) % maxScroll;
});
lastTimestamp = timestamp;
}

animationFrameId = requestAnimationFrame(animate);
};

animationFrameId = requestAnimationFrame(animate);

return () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
};
}, []);

const { data: content, isLoading, error } = useLandingPageContent(language)

// Handle loading and error states
if (isLoading) {
return <div className="min-h-screen bg-[#2563EB] flex items-center justify-center">
<div className="text-white">Loading...</div>
</div>
}

if (error) {
return <div className="min-h-screen bg-[#2563EB] flex items-center justify-center">
<div className="text-white">Error loading content. Please try again later.</div>
</div>
}

return (
<div className="min-h-screen bg-[#2563EB]">
<header className="bg-white border-b border-gray-200">
<div className="container mx-auto px-8 h-14 flex items-center justify-between">
<Link href="/" className="text-[#2563EB] font-bold text-xl">
VERDAD
</Link>
<Button
variant="ghost"
onClick={() => setLanguage(language === 'en' ? 'es' : 'en')}
className="text-[#2563EB] hover:bg-blue-50"
>
{language === 'en' ? 'Español' : 'English'}
</Button>
</div>
</header>
<main className="container mx-auto px-8 py-16 flex flex-col lg:flex-row gap-16 items-start">
<div className="max-w-2xl space-y-8">
<h1 className="text-2xl font-bold leading-tight sm:text-3xl md:text-4xl text-white">
{content.hero_title}
</h1>
<p className="text-lg leading-relaxed sm:text-xl text-white/90">
{content.hero_description}
</p>
<div className="flex flex-col sm:flex-row gap-4">
<Button
className="bg-white text-[#2563EB] hover:bg-white/90"
size="lg"
onClick={() => navigate(ONBOARDING_PATH)}
>
Create Account
</Button>
<Button
variant="outline"
size="lg"
className="text-white border-white bg-white/10 hover:bg-white/20"
onClick={() => navigate(LOGIN_PATH)}
>
Log In
</Button>
</div>
</div>
<div className="w-full max-w-md">
<Card className="p-4 bg-white/10 backdrop-blur-sm border-white/20 overflow-hidden h-[400px]">
<div
className="transition-transform duration-1000 ease-linear"
style={{ transform: `translateY(-${scrollPosition}px)` }}
>
{[...snippets, ...snippets].map((snippet) => {
// Create a unique key using the content
const uniqueKey = `${snippet.titleEn}-${snippet.titleEs}-${snippet.tags.join('-')}`;
return (
<Card
key={uniqueKey}
className="p-4 mb-4 bg-white/5 border-white/10"
>
<div className="flex justify-between items-start mb-3">
<div className="flex-1 pr-4">
<h3 className="text-sm font-medium text-white/90">
{language === 'en' ? snippet.titleEn : snippet.titleEs}
</h3>
</div>
<Button variant="ghost" size="icon" className="text-white/80 pointer-events-none">
<PlayCircle className="h-8 w-8" />
<span className="sr-only">Play audio</span>
</Button>
</div>
<div className="w-full h-8 bg-white/10 rounded mb-3" aria-hidden="true" />
<div className="flex flex-wrap gap-2">
{snippet.tags.map((tag) => (
<Badge
key={`${uniqueKey}-${tag}`}
variant="secondary"
className="bg-white/20 text-white text-xs"
>
{tag}
</Badge>
))}
</div>
</Card>
);
})}
</div>
</Card>
</div>
</main>
<footer className="container mx-auto px-8 py-8 mt-16">
<p className="text-sm text-white/70 max-w-3xl">
{content.footer_text}
</p>
</footer>
</div>
)
}
54 changes: 54 additions & 0 deletions src/hooks/useLandingPageContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useQuery } from '@tanstack/react-query';
import type { SupabaseClient } from '@supabase/supabase-js';
import { supabase } from '@/lib/supabase';

interface ContentRow {
key: string;
content_en: string;
content_es: string;
}

export interface LandingPageContent {
hero_title: string;
hero_description: string;
footer_text: string;
}

interface Database {
public: {
Tables: {
landing_page_content: {
Row: ContentRow;
};
};
};
}

async function fetchLandingPageContent(language: 'en' | 'es'): Promise<LandingPageContent> {
const client = supabase as SupabaseClient<Database>;
const { data, error } = await client
.from('landing_page_content')
.select('key, content_en, content_es');

if (error) {
throw new Error(`Error fetching landing page content: ${error.message}`);
}

// Transform the array of rows into an object keyed by content key
return data.reduce<LandingPageContent>((acc, item: ContentRow) => {
const key = item.key as keyof LandingPageContent;
acc[key] = language === 'en' ? item.content_en : item.content_es;
return acc;
}, {
hero_title: '',
hero_description: '',
footer_text: ''
});
}

export function useLandingPageContent(language: 'en' | 'es') {
return useQuery({
queryKey: ['landingPageContent', language],
queryFn: async () => fetchLandingPageContent(language),
});
}
28 changes: 28 additions & 0 deletions supabase/migrations/20240319000000_landing_page_content.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
CREATE TABLE landing_page_content (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
key TEXT NOT NULL UNIQUE,
content_en TEXT NOT NULL,
content_es TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Insert initial content
INSERT INTO landing_page_content (key, content_en, content_es) VALUES
('hero_title', 'VERDAD detects and tracks coordinated mis/disinformation on the radio', 'VERDAD detecta y rastrea la desinformación coordinada en la radio'),
('hero_description', 'VERDAD gives journalists powerful tools to investigate content targeting immigrant and minority communities through their trusted media sources. By recording radio broadcasts, then transcribing, translating, and analyzing them in real-time, we help journalists to investigate campaigns designed to spread false information.', 'VERDAD proporciona a los periodistas herramientas poderosas para investigar contenido dirigido a comunidades inmigrantes y minoritarias a través de sus fuentes de medios confiables. Al grabar transmisiones de radio, luego transcribirlas, traducirlas y analizarlas en tiempo real, ayudamos a los periodistas a investigar campañas diseñadas para difundir información falsa.'),
('footer_text', 'Created by journalist Martina Guzmán, designed and built by Public Data Works, with support from the Reynolds Journalism Institute, the Damon J. Keith Center for Civil Rights, and the MacArthur Foundation.', 'Creado por la periodista Martina Guzmán, diseñado y construido por Public Data Works, con el apoyo del Reynolds Journalism Institute, el Damon J. Keith Center for Civil Rights y la Fundación MacArthur.');

-- Create trigger to update updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';

CREATE TRIGGER update_landing_page_content_updated_at
BEFORE UPDATE ON landing_page_content
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();