This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

fix issues

+168 -56
+11
app/next.config.js
··· 7 7 // We need to specify which pages need to be dynamic 8 8 // to prevent build errors with pages that use client-side features 9 9 output: 'standalone', 10 + 11 + // Configure static generation behavior 12 + experimental: { 13 + // This ensures packages with browser APIs are treated properly 14 + serverComponentsExternalPackages: ['@supabase/supabase-js', '@atproto/api'], 15 + }, 16 + 17 + // Make sure all API routes are generated with server-side functionality 18 + serverActions: { 19 + bodySizeLimit: '2mb', 20 + } 10 21 }; 11 22 12 23 module.exports = nextConfig;
+3
app/src/app/api/auth/nonce/route.ts
··· 1 1 import { NextRequest, NextResponse } from 'next/server'; 2 2 3 + // Configure this route as dynamic to fix static generation issues 4 + export const dynamic = 'force-dynamic'; 5 + 3 6 const DEFAULT_AUTH_SERVER = 'https://bsky.social'; 4 7 5 8 export async function POST(request: NextRequest) {
+3
app/src/app/api/auth/token/route.ts
··· 1 1 import { NextRequest, NextResponse } from 'next/server'; 2 2 3 + // Configure this route as dynamic to fix static generation issues 4 + export const dynamic = 'force-dynamic'; 5 + 3 6 const DEFAULT_AUTH_SERVER = 'https://bsky.social'; 4 7 const REDIRECT_URI = 'https://flushing.im/auth/callback'; 5 8 const CLIENT_ID = 'https://flushing.im/client-metadata.json';
+3
app/src/app/api/bluesky/feed/route.ts
··· 3 3 import { BskyAgent } from '@atproto/api'; 4 4 import { containsBannedWords, sanitizeText } from '@/lib/content-filter'; 5 5 6 + // Configure this route as dynamic to fix static generation issues 7 + export const dynamic = 'force-dynamic'; 8 + 6 9 // Define type for our database entry 7 10 interface FlushingRecord { 8 11 id: string | number;
+3
app/src/app/api/bluesky/flushing/route.ts
··· 1 1 import { NextRequest, NextResponse } from 'next/server'; 2 2 import { containsBannedWords, sanitizeText } from '@/lib/content-filter'; 3 3 4 + // Configure this route as dynamic to fix static generation issues 5 + export const dynamic = 'force-dynamic'; 6 + 4 7 // This is the default API URL, but we'll use the user's PDS endpoint instead if available 5 8 const DEFAULT_API_URL = 'https://bsky.social/xrpc'; 6 9 const FLUSHING_STATUS_NSID = 'im.flushing.right.now';
+3
app/src/app/api/bluesky/profile/route.ts
··· 1 1 import { NextRequest, NextResponse } from 'next/server'; 2 2 import { createClient } from '@supabase/supabase-js'; 3 + 4 + // Configure this route as dynamic to fix static generation issues 5 + export const dynamic = 'force-dynamic'; 3 6 import { containsBannedWords, sanitizeText } from '@/lib/content-filter'; 4 7 5 8 // Define interfaces for type safety
+3
app/src/app/api/bluesky/stats/route.ts
··· 1 1 import { NextRequest, NextResponse } from 'next/server'; 2 2 import { createClient } from '@supabase/supabase-js'; 3 3 4 + // Configure this route as dynamic to fix static generation issues 5 + export const dynamic = 'force-dynamic'; 6 + 4 7 // Supabase client - using environment variables 5 8 const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || ''; 6 9 const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || '';
+7 -1
app/src/app/layout.tsx
··· 3 3 import { AuthProvider } from '@/lib/auth-context'; 4 4 import { ThemeProvider } from '@/lib/theme-context'; 5 5 import ThemeToggle from '@/components/ThemeToggle'; 6 + import ClientOnly from '@/components/ClientOnly'; 7 + 8 + // Configure this layout as having dynamic runtime to fix SSR issues with theme 9 + export const dynamic = 'force-dynamic'; 6 10 7 11 export const metadata: Metadata = { 8 12 title: "im.flushing", ··· 48 52 backgroundColor: 'var(--card-background)', 49 53 borderBottom: '1px solid var(--tile-border)' 50 54 }}> 51 - <ThemeToggle /> 55 + <ClientOnly> 56 + <ThemeToggle /> 57 + </ClientOnly> 52 58 </header> 53 59 <main>{children}</main> 54 60 </ThemeProvider>
+26
app/src/components/ClientOnly.tsx
··· 1 + 'use client'; 2 + 3 + import React, { useState, useEffect, ReactNode } from 'react'; 4 + 5 + interface ClientOnlyProps { 6 + children: ReactNode; 7 + fallback?: ReactNode; 8 + } 9 + 10 + /** 11 + * A wrapper component that only renders its children on the client side. 12 + * This helps prevent hydration errors when components use browser-only APIs. 13 + */ 14 + export default function ClientOnly({ children, fallback = null }: ClientOnlyProps) { 15 + const [mounted, setMounted] = useState(false); 16 + 17 + useEffect(() => { 18 + setMounted(true); 19 + }, []); 20 + 21 + if (!mounted) { 22 + return <>{fallback}</>; 23 + } 24 + 25 + return <>{children}</>; 26 + }
+82 -37
app/src/components/ThemeToggle.tsx
··· 1 1 'use client'; 2 2 3 3 import { useTheme } from '@/lib/theme-context'; 4 - import React from 'react'; 4 + import React, { useState, useEffect } from 'react'; 5 5 import styles from './ThemeToggle.module.css'; 6 6 7 + // Default light theme icon as fallback for static rendering 8 + const LightIcon = () => ( 9 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 10 + <circle cx="12" cy="12" r="5"></circle> 11 + <line x1="12" y1="1" x2="12" y2="3"></line> 12 + <line x1="12" y1="21" x2="12" y2="23"></line> 13 + <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line> 14 + <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line> 15 + <line x1="1" y1="12" x2="3" y2="12"></line> 16 + <line x1="21" y1="12" x2="23" y2="12"></line> 17 + <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line> 18 + <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line> 19 + </svg> 20 + ); 21 + 22 + // Dark theme icon 23 + const DarkIcon = () => ( 24 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 25 + <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path> 26 + </svg> 27 + ); 28 + 29 + // System theme icon 30 + const SystemIcon = () => ( 31 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 32 + <circle cx="12" cy="12" r="10"></circle> 33 + <line x1="2" y1="12" x2="22" y2="12"></line> 34 + <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path> 35 + </svg> 36 + ); 37 + 7 38 export default function ThemeToggle() { 8 - const { theme, setTheme } = useTheme(); 39 + // Prevent hydration errors by using conditional hooks 40 + const [mounted, setMounted] = useState(false); 41 + const [themeState, setThemeState] = useState('system'); 42 + 43 + // Safe way to access theme context that won't break SSR 44 + let themeContext; 45 + try { 46 + themeContext = useTheme(); 47 + } catch (e) { 48 + // During SSR, the context won't be available, and that's ok 49 + } 50 + 51 + useEffect(() => { 52 + setMounted(true); 53 + if (themeContext) { 54 + setThemeState(themeContext.theme); 55 + } 56 + }, [themeContext]); 9 57 10 58 const toggleTheme = () => { 11 - if (theme === 'light') { 12 - setTheme('dark'); 13 - } else if (theme === 'dark') { 14 - setTheme('system'); 59 + if (!themeContext) return; 60 + 61 + if (themeState === 'light') { 62 + themeContext.setTheme('dark'); 63 + setThemeState('dark'); 64 + } else if (themeState === 'dark') { 65 + themeContext.setTheme('system'); 66 + setThemeState('system'); 15 67 } else { 16 - setTheme('light'); 68 + themeContext.setTheme('light'); 69 + setThemeState('light'); 17 70 } 18 71 }; 19 72 20 73 const getIcon = () => { 21 - if (theme === 'light') { 22 - return ( 23 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 24 - <circle cx="12" cy="12" r="5"></circle> 25 - <line x1="12" y1="1" x2="12" y2="3"></line> 26 - <line x1="12" y1="21" x2="12" y2="23"></line> 27 - <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line> 28 - <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line> 29 - <line x1="1" y1="12" x2="3" y2="12"></line> 30 - <line x1="21" y1="12" x2="23" y2="12"></line> 31 - <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line> 32 - <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line> 33 - </svg> 34 - ); 35 - } else if (theme === 'dark') { 36 - return ( 37 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 38 - <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path> 39 - </svg> 40 - ); 74 + if (themeState === 'light') { 75 + return <LightIcon />; 76 + } else if (themeState === 'dark') { 77 + return <DarkIcon />; 41 78 } else { 42 - return ( 43 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 44 - <circle cx="12" cy="12" r="10"></circle> 45 - <line x1="2" y1="12" x2="22" y2="12"></line> 46 - <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path> 47 - </svg> 48 - ); 79 + return <SystemIcon />; 49 80 } 50 81 }; 51 82 52 83 const getLabel = () => { 53 - if (theme === 'light') return 'Light'; 54 - if (theme === 'dark') return 'Dark'; 84 + if (themeState === 'light') return 'Light'; 85 + if (themeState === 'dark') return 'Dark'; 55 86 return 'System'; 56 87 }; 57 88 89 + // During SSR or before mounting, render a placeholder that won't try to use the context 90 + if (!mounted) { 91 + return ( 92 + <button className={styles.themeToggle} aria-label="Theme toggle"> 93 + <LightIcon /> 94 + <span className={styles.themeLabel}>Theme</span> 95 + </button> 96 + ); 97 + } 98 + 58 99 return ( 59 - <button className={styles.themeToggle} onClick={toggleTheme} aria-label={`Switch to ${theme === 'light' ? 'dark' : theme === 'dark' ? 'system' : 'light'} theme`}> 100 + <button 101 + className={styles.themeToggle} 102 + onClick={toggleTheme} 103 + aria-label={`Switch to ${themeState === 'light' ? 'dark' : themeState === 'dark' ? 'system' : 'light'} theme`} 104 + > 60 105 {getIcon()} 61 106 <span className={styles.themeLabel}>{getLabel()}</span> 62 107 </button>
+24 -18
app/src/lib/theme-context.tsx
··· 9 9 setTheme: (theme: Theme) => void; 10 10 } 11 11 12 - const ThemeContext = createContext<ThemeContextType | undefined>(undefined); 12 + // Create default context values to avoid SSR errors 13 + const defaultContextValue: ThemeContextType = { 14 + theme: 'system', 15 + setTheme: () => {}, 16 + }; 17 + 18 + const ThemeContext = createContext<ThemeContextType>(defaultContextValue); 13 19 14 20 export function ThemeProvider({ children }: { children: ReactNode }) { 15 21 const [theme, setTheme] = useState<Theme>('system'); ··· 17 23 18 24 // Get stored theme preference or use system default 19 25 useEffect(() => { 20 - const storedTheme = localStorage.getItem('theme') as Theme | null; 21 - if (storedTheme) { 22 - setTheme(storedTheme); 26 + // Only run in browser context 27 + if (typeof window !== 'undefined') { 28 + const storedTheme = localStorage.getItem('theme') as Theme | null; 29 + if (storedTheme) { 30 + setTheme(storedTheme); 31 + } 32 + setMounted(true); 23 33 } 24 - setMounted(true); 25 34 }, []); 26 35 27 36 // Apply theme class to document 28 37 useEffect(() => { 29 - if (!mounted) return; 38 + if (!mounted || typeof window === 'undefined') return; 30 39 31 40 // Save to local storage 32 41 localStorage.setItem('theme', theme); ··· 58 67 59 68 // Handle system preference changes 60 69 useEffect(() => { 61 - if (!mounted) return; 70 + if (!mounted || typeof window === 'undefined') return; 62 71 63 72 const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 64 73 ··· 83 92 setTheme, 84 93 }; 85 94 86 - // Don't render until mounted to prevent hydration mismatch 87 - if (!mounted) { 88 - return <>{children}</>; 89 - } 90 - 91 - return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>; 95 + // Always render the provider to avoid SSR issues, but use default values until mounted 96 + return ( 97 + <ThemeContext.Provider value={mounted ? value : defaultContextValue}> 98 + {children} 99 + </ThemeContext.Provider> 100 + ); 92 101 } 93 102 103 + // Safe version of useTheme that won't throw during SSR 94 104 export function useTheme() { 95 - const context = useContext(ThemeContext); 96 - if (context === undefined) { 97 - throw new Error('useTheme must be used within a ThemeProvider'); 98 - } 99 - return context; 105 + return useContext(ThemeContext); 100 106 }