fixed bugs

This commit is contained in:
Anant-0705 2026-07-15 21:31:47 +05:30
parent a993498b5f
commit 82c313eb69
16 changed files with 2409 additions and 757 deletions

View File

@ -1,5 +1,5 @@
import { Hono, MiddlewareHandler } from 'hono'; import { Hono, MiddlewareHandler } from 'hono';
import { AppContext, UserData } from '../utils'; import { AppContext, UserData, authMiddleware } from '../utils';
// Define types based on schema for clarity // Define types based on schema for clarity
type Post = { type Post = {
@ -24,18 +24,15 @@ const EMOTION_TAGS = ['Sadness', 'Rage', 'Joy', 'Anxiety', 'Love', 'Grief', 'Exc
const publicRoutes = new Hono<AppContext>(); const publicRoutes = new Hono<AppContext>();
// Middleware to extract and validate the anonymous user ID // We no longer use anonymousUserMiddleware, we use authMiddleware from utils
const anonymousUserMiddleware: MiddlewareHandler<AppContext> = async (c, next) => {
const anonymousUserId = c.req.header('X-Anonymous-User-ID');
if (!anonymousUserId) {
return c.json({ success: false, error: 'X-Anonymous-User-ID header is required.' }, 400);
}
c.set('anonymousUserId', anonymousUserId);
await next();
};
publicRoutes.post('/user/check-in', authMiddleware, async (c) => {
// authMiddleware ensures the user is valid and inserted into the DB
const user = c.get('user');
return c.json({ success: true, user });
});
// CRITICAL: R2 File upload route (as per general instructions) // CRITICAL: R2 File upload route (as per general instructions)
publicRoutes.post('/upload', anonymousUserMiddleware, async (c) => { publicRoutes.post('/upload', authMiddleware, async (c) => {
const body = await c.req.parseBody(); const body = await c.req.parseBody();
const file = body['file']; const file = body['file'];
@ -91,8 +88,9 @@ publicRoutes.get('/media/:key', async (c) => {
// 2. Create Post // 2. Create Post
publicRoutes.post('/posts', anonymousUserMiddleware, async (c) => { publicRoutes.post('/posts', authMiddleware, async (c) => {
const anonymousUserId = c.get('anonymousUserId'); const user = c.get('user');
const userId = user.email; // Map authenticated user to anonymous_user_id
const body = await c.req.json(); const body = await c.req.json();
const { contentType, contentData, emotionTag } = body; const { contentType, contentData, emotionTag } = body;
@ -123,7 +121,7 @@ publicRoutes.post('/posts', anonymousUserMiddleware, async (c) => {
const { success } = await c.env.DB.prepare( const { success } = await c.env.DB.prepare(
`INSERT INTO posts (id, anonymous_user_id, content_type, content_data, emotion_tag, created_at, expires_at) `INSERT INTO posts (id, anonymous_user_id, content_type, content_data, emotion_tag, created_at, expires_at)
VALUES (?, ?, ?, ?, ?, ?, ?)` VALUES (?, ?, ?, ?, ?, ?, ?)`
).bind(postId, anonymousUserId, contentType, contentData, emotionTag, createdAt, expiresAtISO).run(); ).bind(postId, userId, contentType, contentData, emotionTag, createdAt, expiresAtISO).run();
if (!success) { if (!success) {
return c.json({ success: false, error: 'Failed to create post.' }, 500); return c.json({ success: false, error: 'Failed to create post.' }, 500);
@ -183,15 +181,16 @@ publicRoutes.get('/posts', async (c) => {
}); });
// 4. Get User's Own Active Posts // 4. Get User's Own Active Posts
publicRoutes.get('/posts/me', anonymousUserMiddleware, async (c) => { publicRoutes.get('/posts/me', authMiddleware, async (c) => {
const anonymousUserId = c.get('anonymousUserId'); const user = c.get('user');
const userId = user.email;
try { try {
const { results } = await c.env.DB.prepare( const { results } = await c.env.DB.prepare(
`SELECT p.*, (SELECT COUNT(*) FROM holds h WHERE h.post_id = p.id) as holdCount `SELECT p.*, (SELECT COUNT(*) FROM holds h WHERE h.post_id = p.id) as holdCount
FROM posts p FROM posts p
WHERE p.anonymous_user_id = ? AND p.expires_at > CURRENT_TIMESTAMP WHERE p.anonymous_user_id = ? AND p.expires_at > CURRENT_TIMESTAMP
ORDER BY p.created_at DESC` ORDER BY p.created_at DESC`
).bind(anonymousUserId).all<Post & { holdCount: number }>(); ).bind(userId).all<Post & { holdCount: number }>();
return c.json({ posts: results || [] }); return c.json({ posts: results || [] });
@ -202,8 +201,9 @@ publicRoutes.get('/posts/me', anonymousUserMiddleware, async (c) => {
}); });
// 5. Hold a Post // 5. Hold a Post
publicRoutes.post('/posts/:postId/hold', anonymousUserMiddleware, async (c) => { publicRoutes.post('/posts/:postId/hold', authMiddleware, async (c) => {
const anonymousUserId = c.get('anonymousUserId'); const user = c.get('user');
const userId = user.email;
const { postId } = c.req.param(); const { postId } = c.req.param();
try { try {
@ -215,7 +215,7 @@ publicRoutes.post('/posts/:postId/hold', anonymousUserMiddleware, async (c) => {
const { success } = await c.env.DB.prepare( const { success } = await c.env.DB.prepare(
'INSERT INTO holds (post_id, anonymous_user_id) VALUES (?, ?)' 'INSERT INTO holds (post_id, anonymous_user_id) VALUES (?, ?)'
).bind(postId, anonymousUserId).run(); ).bind(postId, userId).run();
if (!success) { if (!success) {
// This likely means a UNIQUE constraint violation // This likely means a UNIQUE constraint violation
@ -236,8 +236,9 @@ publicRoutes.post('/posts/:postId/hold', anonymousUserMiddleware, async (c) => {
// 6. Report a Post // 6. Report a Post
publicRoutes.post('/posts/:postId/report', anonymousUserMiddleware, async (c) => { publicRoutes.post('/posts/:postId/report', authMiddleware, async (c) => {
const anonymousUserId = c.get('anonymousUserId'); const user = c.get('user');
const userId = user.email;
const { postId } = c.req.param(); const { postId } = c.req.param();
const body = await c.req.json(); const body = await c.req.json();
const reason = body.reason || null; const reason = body.reason || null;
@ -249,7 +250,7 @@ publicRoutes.post('/posts/:postId/report', anonymousUserMiddleware, async (c) =>
} }
const existingReport = await c.env.DB.prepare("SELECT id FROM reports WHERE post_id = ? AND reporter_anonymous_user_id = ?") const existingReport = await c.env.DB.prepare("SELECT id FROM reports WHERE post_id = ? AND reporter_anonymous_user_id = ?")
.bind(postId, anonymousUserId) .bind(postId, userId)
.first(); .first();
if (existingReport) { if (existingReport) {
@ -258,7 +259,7 @@ publicRoutes.post('/posts/:postId/report', anonymousUserMiddleware, async (c) =>
await c.env.DB.prepare( await c.env.DB.prepare(
'INSERT INTO reports (post_id, reporter_anonymous_user_id, reason) VALUES (?, ?, ?)' 'INSERT INTO reports (post_id, reporter_anonymous_user_id, reason) VALUES (?, ?, ?)'
).bind(postId, anonymousUserId, reason).run(); ).bind(postId, userId, reason).run();
return c.body(null, 202); return c.body(null, 202);
} catch (e: any) { } catch (e: any) {
@ -268,15 +269,16 @@ publicRoutes.post('/posts/:postId/report', anonymousUserMiddleware, async (c) =>
}); });
// 7. Get Post Summaries // 7. Get Post Summaries
publicRoutes.get('/posts/summaries', anonymousUserMiddleware, async (c) => { publicRoutes.get('/posts/summaries', authMiddleware, async (c) => {
const anonymousUserId = c.get('anonymousUserId'); const user = c.get('user');
const userId = user.email;
try { try {
const { results } = await c.env.DB.prepare( const { results } = await c.env.DB.prepare(
`SELECT id, emotion_tag, hold_count, dissolved_at `SELECT id, emotion_tag, hold_count, dissolved_at
FROM post_summaries FROM post_summaries
WHERE anonymous_user_id = ? WHERE anonymous_user_id = ?
ORDER BY dissolved_at DESC` ORDER BY dissolved_at DESC`
).bind(anonymousUserId).all<PostSummary>(); ).bind(userId).all<PostSummary>();
return c.json({ summaries: results || [] }); return c.json({ summaries: results || [] });
} catch (e: any) { } catch (e: any) {
@ -286,8 +288,9 @@ publicRoutes.get('/posts/summaries', anonymousUserMiddleware, async (c) => {
}); });
// 8. Clear Post Summaries // 8. Clear Post Summaries
publicRoutes.post('/posts/summaries/clear', anonymousUserMiddleware, async (c) => { publicRoutes.post('/posts/summaries/clear', authMiddleware, async (c) => {
const anonymousUserId = c.get('anonymousUserId'); const user = c.get('user');
const userId = user.email;
const body = await c.req.json(); const body = await c.req.json();
const summaryIds = body.summaryIds; const summaryIds = body.summaryIds;
@ -297,7 +300,7 @@ publicRoutes.post('/posts/summaries/clear', anonymousUserMiddleware, async (c) =
try { try {
const query = `DELETE FROM post_summaries WHERE anonymous_user_id = ? AND id IN (${summaryIds.map(() => '?').join(',')})`; const query = `DELETE FROM post_summaries WHERE anonymous_user_id = ? AND id IN (${summaryIds.map(() => '?').join(',')})`;
await c.env.DB.prepare(query).bind(anonymousUserId, ...summaryIds).run(); await c.env.DB.prepare(query).bind(userId, ...summaryIds).run();
return c.body(null, 204); return c.body(null, 204);
} catch (e: any) { } catch (e: any) {

View File

@ -6,7 +6,7 @@
{ {
"binding": "DB", "binding": "DB",
"database_name": "db-xd1uom", "database_name": "db-xd1uom",
"database_id": "23b00c58-c2c5-4f98-abba-ce49c8cfaa0c" "database_id": "c998402c-77a7-4d05-b963-11fe654161b9"
} }
], ],
"r2_buckets": [ "r2_buckets": [

File diff suppressed because it is too large Load Diff

View File

@ -2,11 +2,10 @@ import { Routes, Route } from 'react-router-dom';
import { Toaster } from 'sonner'; import { Toaster } from 'sonner';
import { Layout } from '@/components/Layout'; import { Layout } from '@/components/Layout';
import { StreamPage, CreatePostPage, MyDissolvesPage, NotFoundPage } from '@/pages'; import { StreamPage, CreatePostPage, MyDissolvesPage, NotFoundPage } from '@/pages';
import { AnonymousIdProvider } from '@/context/AnonymousIdContext';
function App() { function App() {
return ( return (
<AnonymousIdProvider> <>
<Layout> <Layout>
<Routes> <Routes>
<Route path="/" element={<StreamPage />} /> <Route path="/" element={<StreamPage />} />
@ -16,7 +15,7 @@ function App() {
</Routes> </Routes>
</Layout> </Layout>
<Toaster position="bottom-center" toastOptions={{ className: 'font-sans' }} /> <Toaster position="bottom-center" toastOptions={{ className: 'font-sans' }} />
</AnonymousIdProvider> </>
); );
} }

View File

@ -1,9 +1,9 @@
import axios from 'axios'; import axios from 'axios';
export const API_URL = (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') export const API_URL = (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || window.location.hostname.startsWith('192.168.'))
? "http://127.0.0.1:8787" ? `http://${window.location.hostname}:8787`
: "https://app-xd1uom.laflabs-tech.workers.dev"; : "https://app-xd1uom.mworld-projects.workers.dev";
export const api = axios.create({ export const api = axios.create({
baseURL: API_URL baseURL: API_URL

View File

@ -12,6 +12,7 @@ export function AudioPlayer({ src }: AudioPlayerProps) {
const [duration, setDuration] = useState(0); const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0); const [currentTime, setCurrentTime] = useState(0);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
useEffect(() => { useEffect(() => {
const audio = audioRef.current; const audio = audioRef.current;
@ -24,17 +25,36 @@ export function AudioPlayer({ src }: AudioPlayerProps) {
const handleTimeUpdate = () => setCurrentTime(audio.currentTime); const handleTimeUpdate = () => setCurrentTime(audio.currentTime);
const handleEnded = () => setIsPlaying(false); const handleEnded = () => setIsPlaying(false);
const handleCanPlay = () => setIsLoading(false); const handleCanPlay = () => setIsLoading(false);
const handleError = () => {
console.error("Error loading audio file:", audio.error);
setIsLoading(false);
setHasError(true);
};
audio.addEventListener('loadedmetadata', handleLoadedMetadata); audio.addEventListener('loadedmetadata', handleLoadedMetadata);
audio.addEventListener('timeupdate', handleTimeUpdate); audio.addEventListener('timeupdate', handleTimeUpdate);
audio.addEventListener('ended', handleEnded); audio.addEventListener('ended', handleEnded);
audio.addEventListener('canplay', handleCanPlay); audio.addEventListener('canplay', handleCanPlay);
audio.addEventListener('error', handleError);
// Check if audio has already loaded enough data before listeners were attached
if (audio.readyState >= 1) {
handleLoadedMetadata();
}
if (audio.readyState >= 3) {
handleCanPlay();
}
if (audio.error) {
handleError();
}
return () => { return () => {
audio.removeEventListener('loadedmetadata', handleLoadedMetadata); audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
audio.removeEventListener('timeupdate', handleTimeUpdate); audio.removeEventListener('timeupdate', handleTimeUpdate);
audio.removeEventListener('timeupdate', handleTimeUpdate);
audio.removeEventListener('ended', handleEnded); audio.removeEventListener('ended', handleEnded);
audio.removeEventListener('canplay', handleCanPlay); audio.removeEventListener('canplay', handleCanPlay);
audio.removeEventListener('error', handleError);
}; };
}, []); }, []);
@ -54,11 +74,13 @@ export function AudioPlayer({ src }: AudioPlayerProps) {
<audio ref={audioRef} src={src} preload="metadata"></audio> <audio ref={audioRef} src={src} preload="metadata"></audio>
<button <button
onClick={togglePlayPause} onClick={togglePlayPause}
disabled={isLoading} disabled={isLoading || hasError}
className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-primary text-on-primary disabled:bg-primary-disabled" className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-primary text-on-primary disabled:bg-primary-disabled"
> >
{isLoading ? ( {isLoading ? (
<Loader2 className="h-5 w-5 animate-spin" /> <Loader2 className="h-5 w-5 animate-spin" />
) : hasError ? (
<span className="text-xs">!</span>
) : isPlaying ? ( ) : isPlaying ? (
<Pause className="h-5 w-5" /> <Pause className="h-5 w-5" />
) : ( ) : (

View File

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import { NavLink, useLocation, Link } from 'react-router-dom'; import { NavLink, useLocation, Link } from 'react-router-dom';
import { Plus } from 'lucide-react'; import { Plus, LogIn, LogOut, User } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useAuth } from '@/context/AuthContext';
interface LayoutProps { interface LayoutProps {
children: React.ReactNode; children: React.ReactNode;
@ -9,11 +10,12 @@ interface LayoutProps {
export function Layout({ children }: LayoutProps) { export function Layout({ children }: LayoutProps) {
const location = useLocation(); const location = useLocation();
const { isAuthenticated, login, logout, user } = useAuth();
const navItems = [ const navItems = [
{ path: '/', label: 'The Stream' }, { path: '/', label: 'The Stream', public: true },
{ path: '/my-dissolves', label: 'My Dissolves' }, { path: '/my-dissolves', label: 'My Dissolves', public: false },
]; ].filter(item => item.public || isAuthenticated);
return ( return (
<div className="min-h-screen bg-canvas font-sans"> <div className="min-h-screen bg-canvas font-sans">
@ -42,9 +44,28 @@ export function Layout({ children }: LayoutProps) {
})} })}
</nav> </nav>
</div> </div>
<Link to="/create" className="inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium h-10 px-6 bg-primary text-on-primary hover:bg-primary-active transition-colors"> <div className="flex items-center gap-4">
<Plus className="mr-2 h-4 w-4" /> Share a Feeling {isAuthenticated ? (
</Link> <>
<Link to="/create" className="hidden md:inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium h-10 px-6 bg-primary text-on-primary hover:bg-primary-active transition-colors">
<Plus className="mr-2 h-4 w-4" /> Share a Feeling
</Link>
<div className="flex items-center gap-3 border-l border-hairline pl-4 ml-2">
<div className="flex items-center gap-2 text-sm text-ink font-medium">
<User className="h-4 w-4 text-muted" />
<span className="hidden sm:inline">{user?.fullName || 'User'}</span>
</div>
<button onClick={logout} className="p-2 text-muted hover:text-ink transition-colors" aria-label="Log out">
<LogOut className="h-5 w-5" />
</button>
</div>
</>
) : (
<button onClick={login} className="inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium h-10 px-6 border border-hairline text-ink hover:bg-surface-soft transition-colors">
<LogIn className="mr-2 h-4 w-4" /> Log In
</button>
)}
</div>
</div> </div>
</header> </header>
<main className="container-full py-8 md:py-12"> <main className="container-full py-8 md:py-12">

View File

@ -6,6 +6,7 @@ import { toast } from 'sonner';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useApi } from '@/hooks/useApi'; import { useApi } from '@/hooks/useApi';
import { useAuth } from '@/context/AuthContext';
import type { Post } from '@/types'; import type { Post } from '@/types';
import { AudioPlayer } from './AudioPlayer'; import { AudioPlayer } from './AudioPlayer';
import { ReportModal } from './ReportModal'; import { ReportModal } from './ReportModal';
@ -21,8 +22,13 @@ export function PostCard({ post }: PostCardProps) {
const isHeld = useIsPostHeld(post.id) ?? false; const isHeld = useIsPostHeld(post.id) ?? false;
const [isReportModalOpen, setIsReportModalOpen] = useState(false); const [isReportModalOpen, setIsReportModalOpen] = useState(false);
const { isAuthenticated, login } = useAuth();
const handleHold = () => { const handleHold = () => {
if (!isAuthenticated) {
login();
return;
}
if (isHeld) return; if (isHeld) return;
holdPost(post.id, { holdPost(post.id, {
onSuccess: () => { onSuccess: () => {
@ -81,7 +87,13 @@ export function PostCard({ post }: PostCardProps) {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => setIsReportModalOpen(true)} onClick={() => {
if (!isAuthenticated) {
login();
} else {
setIsReportModalOpen(true);
}
}}
className="flex h-8 w-8 items-center justify-center rounded-full text-muted transition-colors hover:bg-hairline hover:text-ink" className="flex h-8 w-8 items-center justify-center rounded-full text-muted transition-colors hover:bg-hairline hover:text-ink"
aria-label="Report post" aria-label="Report post"
> >

View File

@ -1,40 +0,0 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { v4 as uuidv4 } from 'uuid';
interface AnonymousIdContextType {
anonymousId: string | null;
isLoading: boolean;
}
const AnonymousIdContext = createContext<AnonymousIdContextType | undefined>(undefined);
export const AnonymousIdProvider = ({ children }: { children: ReactNode }) => {
const [anonymousId, setAnonymousId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
let storedId = localStorage.getItem('anonymous_user_id');
if (!storedId) {
storedId = uuidv4();
localStorage.setItem('anonymous_user_id', storedId);
}
setAnonymousId(storedId);
setIsLoading(false);
}, []);
const value = { anonymousId, isLoading };
return (
<AnonymousIdContext.Provider value={value}>
{children}
</AnonymousIdContext.Provider>
);
};
export const useAnonymousId = () => {
const context = useContext(AnonymousIdContext);
if (context === undefined) {
throw new Error('useAnonymousId must be used within an AnonymousIdProvider');
}
return context;
};

View File

@ -1,129 +1,123 @@
import { useMutation, useQuery, useInfiniteQuery, QueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useInfiniteQuery, QueryClient } from '@tanstack/react-query';
import { api, API_URL } from '../api'; import { api, API_URL } from '../api';
import { useAnonymousId } from '@/context/AnonymousIdContext'; import { useAuth } from '@/context/AuthContext';
import type { EmotionTag, Post, PostSummary, PostContentType } from '@/types'; import type { EmotionTag, Post, PostSummary, PostContentType } from '@/types';
// A wrapper to inject the anonymous ID header const callApi = <T,>(method: 'get' | 'post' | 'delete', url: string, data?: any): Promise<{ data: T }> => {
const callApi = <T,>(method: 'get' | 'post' | 'delete', url: string, anonymousId: string, data?: any): Promise<{ data: T }> => {
const headers = { 'X-Anonymous-User-ID': anonymousId };
if (method === 'post' || method === 'delete') { if (method === 'post' || method === 'delete') {
return api[method]<T>(url, data, { headers }); return api[method]<T>(url, data);
} }
return api.get<T>(url, { headers }); return api.get<T>(url);
} }
export const useApi = () => { export const useApi = () => {
const { anonymousId } = useAnonymousId(); const { isAuthenticated } = useAuth();
// Stream // Stream
const useGetPosts = (emotion?: EmotionTag) => { const useGetPosts = (emotion?: EmotionTag) => {
return useInfiniteQuery( return useInfiniteQuery({
['posts', emotion], queryKey: ['posts', emotion],
async ({ pageParam: cursor }) => { queryFn: async ({ pageParam: cursor }) => {
if (!anonymousId) throw new Error('Anonymous ID not available');
let url = '/api/posts?'; let url = '/api/posts?';
if (emotion) url += `emotion=${emotion}&`; if (emotion) url += `emotion=${emotion}&`;
if (cursor) url += `cursor=${cursor}`; if (cursor) url += `cursor=${cursor}`;
const res = await callApi<{posts: Post[], nextCursor: string | null}>('get', url, anonymousId); const res = await callApi<{posts: Post[], nextCursor: string | null}>('get', url);
return res.data; return res.data;
}, },
{ getNextPageParam: (lastPage) => lastPage.nextCursor,
getNextPageParam: (lastPage) => lastPage.nextCursor, enabled: true, // Stream is public, no authentication required to view
enabled: !!anonymousId, });
}
);
}; };
// Create // Create
const useCreatePost = (queryClient: QueryClient) => { const useCreatePost = (queryClient: QueryClient) => {
return useMutation( return useMutation({
({ contentType, contentData, emotionTag }: { contentType: PostContentType, contentData: string, emotionTag: EmotionTag }) => { mutationFn: ({ contentType, contentData, emotionTag }: { contentType: PostContentType, contentData: string, emotionTag: EmotionTag }) => {
if (!anonymousId) throw new Error('Anonymous ID not available'); if (!isAuthenticated) throw new Error('Must be logged in to post');
return callApi('post', '/api/posts', anonymousId, { contentType, contentData, emotionTag }); return callApi('post', '/api/posts', { contentType, contentData, emotionTag });
}, },
{ onSuccess: () => {
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['my-posts'] });
queryClient.invalidateQueries(['my-posts']); queryClient.invalidateQueries({ queryKey: ['posts'] });
queryClient.invalidateQueries(['posts']); },
}, });
}
);
}; };
const useUploadFile = () => { const useUploadFile = () => {
return useMutation( return useMutation({
(file: File) => { mutationFn: (file: File) => {
if (!anonymousId) throw new Error('Anonymous ID not available'); if (!isAuthenticated) throw new Error('Must be logged in to upload');
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
return callApi<{url: string}>('post', '/api/upload', anonymousId, formData); return callApi<{url: string}>('post', '/api/upload', formData);
}, },
); });
} }
// My Dissolves // My Dissolves
const useGetMyPosts = () => { const useGetMyPosts = () => {
return useQuery( return useQuery({
['my-posts'], queryKey: ['my-posts'],
() => { queryFn: () => {
if (!anonymousId) throw new Error('Anonymous ID not available'); if (!isAuthenticated) throw new Error('Must be logged in');
return callApi<{posts: Post[]}>('get', '/api/posts/me', anonymousId).then(res => res.data); return callApi<{posts: Post[]}>('get', '/api/posts/me').then(res => res.data);
}, },
{ enabled: !!anonymousId } enabled: isAuthenticated
); });
}; };
const useGetSummaries = () => { const useGetSummaries = () => {
return useQuery( return useQuery({
['summaries'], queryKey: ['summaries'],
() => { queryFn: () => {
if (!anonymousId) throw new Error('Anonymous ID not available'); if (!isAuthenticated) throw new Error('Must be logged in');
return callApi<{summaries: PostSummary[]}>('get', '/api/posts/summaries', anonymousId).then(res => res.data); return callApi<{summaries: PostSummary[]}>('get', '/api/posts/summaries').then(res => res.data);
}, },
{ enabled: !!anonymousId } enabled: isAuthenticated
); });
}; };
const useClearSummaries = (queryClient: QueryClient) => { const useClearSummaries = (queryClient: QueryClient) => {
return useMutation( return useMutation({
(summaryIds: string[]) => { mutationFn: (summaryIds: string[]) => {
if (!anonymousId) throw new Error('Anonymous ID not available'); if (!isAuthenticated) throw new Error('Must be logged in');
return callApi('post', '/api/posts/summaries/clear', anonymousId, { summaryIds }); return callApi('post', '/api/posts/summaries/clear', { summaryIds });
}, },
{ onSuccess: () => {
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['summaries'] });
queryClient.invalidateQueries(['summaries']); },
}, });
}
);
}; };
// Interactions // Interactions
const useHoldPost = (queryClient: QueryClient) => { const useHoldPost = (queryClient: QueryClient) => {
return useMutation( return useMutation({
(postId: string) => { mutationFn: (postId: string) => {
if (!anonymousId) throw new Error('Anonymous ID not available'); if (!isAuthenticated) throw new Error('Must be logged in');
return callApi('post', `/api/posts/${postId}/hold`, anonymousId); return callApi('post', `/api/posts/${postId}/hold`);
}, },
{ onSuccess: (_data, postId) => {
onSuccess: (_data, postId) => { queryClient.setQueryData(['held-posts', postId], true);
queryClient.setQueryData(['held-posts', postId], true);
}
} }
); });
}; };
const useReportPost = () => { const useReportPost = () => {
return useMutation( return useMutation({
({ postId, reason }: { postId: string, reason: string }) => { mutationFn: ({ postId, reason }: { postId: string, reason: string }) => {
if (!anonymousId) throw new Error('Anonymous ID not available'); if (!isAuthenticated) throw new Error('Must be logged in');
return callApi('post', `/api/posts/${postId}/report`, anonymousId, { reason }); return callApi('post', `/api/posts/${postId}/report`, { reason });
}, },
); });
}; };
const useIsPostHeld = (postId: string) => { const useIsPostHeld = (postId: string) => {
const {data} = useQuery(['held-posts', postId], () => false, { staleTime: Infinity, cacheTime: Infinity }); const {data} = useQuery({
queryKey: ['held-posts', postId],
queryFn: () => false,
staleTime: Infinity,
cacheTime: Infinity
});
return data; return data;
} }
@ -136,3 +130,4 @@ export const useApi = () => {
return { useGetPosts, useCreatePost, useUploadFile, useGetMyPosts, useGetSummaries, useClearSummaries, useHoldPost, useReportPost, useIsPostHeld, getMediaUrl }; return { useGetPosts, useCreatePost, useUploadFile, useGetMyPosts, useGetSummaries, useClearSummaries, useHoldPost, useReportPost, useIsPostHeld, getMediaUrl };
}; };

View File

@ -22,7 +22,6 @@ export const useCountdown = (targetDate: string | Date) => {
format: ['hours', 'minutes', 'seconds'], format: ['hours', 'minutes', 'seconds'],
zero: true, zero: true,
delimiter: ':', delimiter: ':',
pad: { hours: 2, minutes: 2, seconds: 2 },
}); });
// Special handling for formatDuration output // Special handling for formatDuration output
const parts = formatted.split(':').map(p => p.padStart(2, '0')); const parts = formatted.split(':').map(p => p.padStart(2, '0'));

View File

@ -1,14 +1,9 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@font-face {
font-family: 'Airbnb Cereal VF';
src: url('https://d21p2nvrwwh70b.cloudfront.net/fonts/Airbnb_Cereal_W_VF.woff2') format('woff2-variations');
font-weight: 100 900;
font-style: normal;
}
@layer base { @layer base {
:root { :root {
--canvas: 0 0% 100%; /* #ffffff */ --canvas: 0 0% 100%; /* #ffffff */

View File

@ -1,5 +1,6 @@
import { useState, useRef } from 'react'; import { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/context/AuthContext';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -18,6 +19,15 @@ const contentTypes: { type: PostContentType, label: string, icon: React.ElementT
export default function CreatePostPage() { export default function CreatePostPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { isAuthenticated } = useAuth();
useEffect(() => {
if (!isAuthenticated) {
navigate('/');
toast.error('You must be logged in to share a feeling.');
}
}, [isAuthenticated, navigate]);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { useCreatePost, useUploadFile } = useApi(); const { useCreatePost, useUploadFile } = useApi();
const createPostMutation = useCreatePost(queryClient); const createPostMutation = useCreatePost(queryClient);
@ -53,8 +63,10 @@ export default function CreatePostPage() {
audioChunksRef.current = []; audioChunksRef.current = [];
recorder.ondataavailable = (event) => audioChunksRef.current.push(event.data); recorder.ondataavailable = (event) => audioChunksRef.current.push(event.data);
recorder.onstop = () => { recorder.onstop = () => {
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' }); const mimeType = recorder.mimeType || 'audio/webm';
const audioFile = new File([audioBlob], 'recording.webm', { type: 'audio/webm' }); const ext = mimeType.includes('mp4') ? 'mp4' : 'webm';
const audioBlob = new Blob(audioChunksRef.current, { type: mimeType });
const audioFile = new File([audioBlob], `recording.${ext}`, { type: mimeType });
setFileData(audioFile); setFileData(audioFile);
stream.getTracks().forEach(track => track.stop()); // Stop microphone access stream.getTracks().forEach(track => track.stop()); // Stop microphone access
}; };

View File

@ -1,4 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/context/AuthContext';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
@ -10,6 +12,16 @@ import { MyPostCard } from '@/components/MyPostCard';
import type { PostSummary } from '@/types'; import type { PostSummary } from '@/types';
export default function MyDissolvesPage() { export default function MyDissolvesPage() {
const navigate = useNavigate();
const { isAuthenticated } = useAuth();
useEffect(() => {
if (!isAuthenticated) {
navigate('/');
toast.error('You must be logged in to view your dissolves.');
}
}, [isAuthenticated, navigate]);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { useGetMyPosts, useGetSummaries, useClearSummaries } = useApi(); const { useGetMyPosts, useGetSummaries, useClearSummaries } = useApi();
const { data: myPostsData, isLoading: isLoadingPosts } = useGetMyPosts(); const { data: myPostsData, isLoading: isLoadingPosts } = useGetMyPosts();

View File

@ -32,7 +32,7 @@ module.exports = {
full: '9999px', full: '9999px',
}, },
fontFamily: { fontFamily: {
sans: ['Airbnb Cereal VF', 'Circular', 'sans-serif'], sans: ['Inter', 'sans-serif'],
}, },
boxShadow: { boxShadow: {
card: '0 0 0 1px rgba(0,0,0,0.02), 0 2px 6px rgba(0,0,0,0.04), 0 4px 8px rgba(0,0,0,0.02)', card: '0 0 0 1px rgba(0,0,0,0.02), 0 2px 6px rgba(0,0,0,0.04), 0 4px 8px rgba(0,0,0,0.02)',

View File

@ -17,7 +17,11 @@
"moduleResolution": "Node", "moduleResolution": "Node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "react-jsx" "jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}, },
"include": [ "include": [
"src" "src"