commit 7d3f08cae943ea07ccd93c5d27f5b7648609f851 Author: Logan Cusano Date: Sun Jul 13 19:03:13 2025 -0400 init diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7569ae7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# Stage 1: Build the Next.js application +FROM node:22-slim AS builder +WORKDIR /app + +# Copy package files and install dependencies +COPY package*.json ./ +RUN npm install + +# Copy the rest of the application code +COPY . . + +# Build the Next.js app +RUN npm run build + +# Stage 2: Production image +FROM node:22-slim +WORKDIR /app + +# Copy built assets from the builder stage +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/public ./public +COPY --from=builder /app/package.json . +COPY --from=builder /app/next.config.js . 2>/dev/null || true + +# Expose port 3000 +EXPOSE 3000 + +# Start the Next.js production server +CMD ["npm", "start"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..37930c8 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +IMAGE_NAME = twimg-frontend +CONTAINER_NAME = twimg-frontend + +# Target to build the Docker image for sdr-node +build: + docker build -t $(IMAGE_NAME) . + +# Target to run the sdr-node container +run: build + docker run --rm -it \ + --name $(CONTAINER_NAME) \ + -p 3000:3000 \ + $(IMAGE_NAME) \ No newline at end of file diff --git a/app/auth/login/page.tsx b/app/auth/login/page.tsx new file mode 100644 index 0000000..2cdc3a8 --- /dev/null +++ b/app/auth/login/page.tsx @@ -0,0 +1,67 @@ +const LoginPage = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const auth = useAuth(); + // In a real app, you'd use Next.js's useRouter + const [isLoggedIn, setIsLoggedIn] = useState(false); + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + try { + const formData = new URLSearchParams(); + formData.append('username', email); + formData.append('password', password); + + const response = await fetch(`${API_URL}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: formData, + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || 'Login failed'); + } + + const { id_token, uid } = await response.json(); + + // A real app would fetch user details here, but we'll mock it + const userDetails = { uid, email, role: 'user' }; // Assume 'user' for now + auth.login(id_token, userDetails); + setIsLoggedIn(true); // Simulate redirect + } catch (err) { + setError(err.message); + } + }; + + if (isLoggedIn || auth.isAuthenticated) { + // In a real app, this would be a redirect to '/' + return

Redirecting...

; + } + + return ( +
+ + + Login + + +
+
+ + setEmail(e.target.value)} required /> +
+
+ + setPassword(e.target.value)} required /> +
+ {error &&

{error}

} + +
+
+
+
+ ); + }; \ No newline at end of file diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..3e67824 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,66 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem} + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%} +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..6660a8b --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,42 @@ +const RootLayout = ({ children }) => ( + + + + {children} + + + + ); + + // Main App component to simulate routing + export default function App() { + const [route, setRoute] = useState('/login'); // Default route + + // Simulate navigation for the demo + useEffect(() => { + const handleHashChange = () => { + setRoute(window.location.hash.replace('#', '') || '/'); + }; + window.addEventListener('hashchange', handleHashChange); + handleHashChange(); // Initial check + return () => window.removeEventListener('hashchange', handleHashChange); + }, []); + + const renderPage = () => { + switch (route) { + case '/login': + return ; + case '/admin': + return ; + case '/': + default: + return ; + } + }; + + return ( + + {renderPage()} + + ); + } \ No newline at end of file diff --git a/app/main/admin/page.tsx b/app/main/admin/page.tsx new file mode 100644 index 0000000..2a510a2 --- /dev/null +++ b/app/main/admin/page.tsx @@ -0,0 +1,55 @@ +const AdminPage = () => { + const [message, setMessage] = useState(''); + const [error, setError] = useState(''); + const auth = useAuth(); + + const handleScan = async () => { + setError(''); + setMessage('Scanning...'); + try { + const data = await apiRequest('/videos/scan', { method: 'POST', token: auth.token }); + setMessage(data.message); + } catch (err) { + setError(err.message); + } + }; + + // In a real app, you'd fetch users and votes here + const users = [{email: 'admin@example.com', role: 'admin'}, {email: 'user@example.com', role: 'user'}]; + const votes = [{video_id: 'xyz', decision: 'approve', reason: 'Good clip'}]; + + if (auth.user?.role !== 'admin') { + return

Access Denied. You must be an admin to view this page.

; + } + + return ( +
+ + + Admin Actions + + + + {message &&

{message}

} + {error &&

{error}

} +
+
+ + + Users + + {/* User list would be rendered here */} +

User list functionality would go here.

+
+
+ + + Votes + + {/* Vote list would be rendered here */} +

Vote list functionality would go here.

+
+
+
+ ); +}; \ No newline at end of file diff --git a/app/main/layout.tsx b/app/main/layout.tsx new file mode 100644 index 0000000..d506ae5 --- /dev/null +++ b/app/main/layout.tsx @@ -0,0 +1,27 @@ +const MainLayout = ({ children }) => { + const auth = useAuth(); + + if (!auth.isAuthenticated) { + // This would be a redirect in a real Next.js app + return ; + } + + return ( +
+
+ +
+
+ {children} +
+
+ ); +}; \ No newline at end of file diff --git a/app/main/page.tsx b/app/main/page.tsx new file mode 100644 index 0000000..4aa7095 --- /dev/null +++ b/app/main/page.tsx @@ -0,0 +1,87 @@ +const VotingPage = () => { + const [video, setVideo] = useState(null); + const [reason, setReason] = useState(''); + const [message, setMessage] = useState(''); + const [error, setError] = useState(''); + const auth = useAuth(); + + const fetchNextVideo = async () => { + setError(''); + setMessage(''); + setVideo(null); + try { + const data = await apiRequest('/videos/vote-next', { token: auth.token }); + setVideo(data); + } catch (err) { + setError(err.message); + } + }; + + const submitVote = async (decision) => { + if (!video) return; + setError(''); + setMessage(''); + try { + const body = { decision, reason, recommended_game: '' }; // Add recommended_game if needed + await apiRequest(`/videos/${video.id}/vote`, { + method: 'POST', + body, + token: auth.token + }); + setMessage(`Vote '${decision}' submitted successfully!`); + setVideo(null); + setReason(''); + } catch (err) { + setError(err.message); + } + }; + + return ( +
+ + + Vote on a Video + + + {!video && ( +
+ +
+ )} + + {error &&

{error}

} + {message &&

{message}

} + + {video && ( +
+
+ +
+
+

Person: {video.person}

+

Game: {video.game || 'N/A'}

+
+
+ +