fixed bugs
This commit is contained in:
parent
a993498b5f
commit
82c313eb69
@ -1,5 +1,5 @@
|
||||
import { Hono, MiddlewareHandler } from 'hono';
|
||||
import { AppContext, UserData } from '../utils';
|
||||
import { AppContext, UserData, authMiddleware } from '../utils';
|
||||
|
||||
// Define types based on schema for clarity
|
||||
type Post = {
|
||||
@ -24,18 +24,15 @@ const EMOTION_TAGS = ['Sadness', 'Rage', 'Joy', 'Anxiety', 'Love', 'Grief', 'Exc
|
||||
|
||||
const publicRoutes = new Hono<AppContext>();
|
||||
|
||||
// Middleware to extract and validate the anonymous user ID
|
||||
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();
|
||||
};
|
||||
// We no longer use anonymousUserMiddleware, we use authMiddleware from utils
|
||||
|
||||
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)
|
||||
publicRoutes.post('/upload', anonymousUserMiddleware, async (c) => {
|
||||
publicRoutes.post('/upload', authMiddleware, async (c) => {
|
||||
const body = await c.req.parseBody();
|
||||
const file = body['file'];
|
||||
|
||||
@ -91,8 +88,9 @@ publicRoutes.get('/media/:key', async (c) => {
|
||||
|
||||
|
||||
// 2. Create Post
|
||||
publicRoutes.post('/posts', anonymousUserMiddleware, async (c) => {
|
||||
const anonymousUserId = c.get('anonymousUserId');
|
||||
publicRoutes.post('/posts', authMiddleware, async (c) => {
|
||||
const user = c.get('user');
|
||||
const userId = user.email; // Map authenticated user to anonymous_user_id
|
||||
const body = await c.req.json();
|
||||
|
||||
const { contentType, contentData, emotionTag } = body;
|
||||
@ -123,7 +121,7 @@ publicRoutes.post('/posts', anonymousUserMiddleware, async (c) => {
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO posts (id, anonymous_user_id, content_type, content_data, emotion_tag, created_at, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||
).bind(postId, anonymousUserId, contentType, contentData, emotionTag, createdAt, expiresAtISO).run();
|
||||
).bind(postId, userId, contentType, contentData, emotionTag, createdAt, expiresAtISO).run();
|
||||
|
||||
if (!success) {
|
||||
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
|
||||
publicRoutes.get('/posts/me', anonymousUserMiddleware, async (c) => {
|
||||
const anonymousUserId = c.get('anonymousUserId');
|
||||
publicRoutes.get('/posts/me', authMiddleware, async (c) => {
|
||||
const user = c.get('user');
|
||||
const userId = user.email;
|
||||
try {
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT p.*, (SELECT COUNT(*) FROM holds h WHERE h.post_id = p.id) as holdCount
|
||||
FROM posts p
|
||||
WHERE p.anonymous_user_id = ? AND p.expires_at > CURRENT_TIMESTAMP
|
||||
ORDER BY p.created_at DESC`
|
||||
).bind(anonymousUserId).all<Post & { holdCount: number }>();
|
||||
).bind(userId).all<Post & { holdCount: number }>();
|
||||
|
||||
return c.json({ posts: results || [] });
|
||||
|
||||
@ -202,8 +201,9 @@ publicRoutes.get('/posts/me', anonymousUserMiddleware, async (c) => {
|
||||
});
|
||||
|
||||
// 5. Hold a Post
|
||||
publicRoutes.post('/posts/:postId/hold', anonymousUserMiddleware, async (c) => {
|
||||
const anonymousUserId = c.get('anonymousUserId');
|
||||
publicRoutes.post('/posts/:postId/hold', authMiddleware, async (c) => {
|
||||
const user = c.get('user');
|
||||
const userId = user.email;
|
||||
const { postId } = c.req.param();
|
||||
|
||||
try {
|
||||
@ -215,7 +215,7 @@ publicRoutes.post('/posts/:postId/hold', anonymousUserMiddleware, async (c) => {
|
||||
|
||||
const { success } = await c.env.DB.prepare(
|
||||
'INSERT INTO holds (post_id, anonymous_user_id) VALUES (?, ?)'
|
||||
).bind(postId, anonymousUserId).run();
|
||||
).bind(postId, userId).run();
|
||||
|
||||
if (!success) {
|
||||
// This likely means a UNIQUE constraint violation
|
||||
@ -236,8 +236,9 @@ publicRoutes.post('/posts/:postId/hold', anonymousUserMiddleware, async (c) => {
|
||||
|
||||
|
||||
// 6. Report a Post
|
||||
publicRoutes.post('/posts/:postId/report', anonymousUserMiddleware, async (c) => {
|
||||
const anonymousUserId = c.get('anonymousUserId');
|
||||
publicRoutes.post('/posts/:postId/report', authMiddleware, async (c) => {
|
||||
const user = c.get('user');
|
||||
const userId = user.email;
|
||||
const { postId } = c.req.param();
|
||||
const body = await c.req.json();
|
||||
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 = ?")
|
||||
.bind(postId, anonymousUserId)
|
||||
.bind(postId, userId)
|
||||
.first();
|
||||
|
||||
if (existingReport) {
|
||||
@ -258,7 +259,7 @@ publicRoutes.post('/posts/:postId/report', anonymousUserMiddleware, async (c) =>
|
||||
|
||||
await c.env.DB.prepare(
|
||||
'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);
|
||||
} catch (e: any) {
|
||||
@ -268,15 +269,16 @@ publicRoutes.post('/posts/:postId/report', anonymousUserMiddleware, async (c) =>
|
||||
});
|
||||
|
||||
// 7. Get Post Summaries
|
||||
publicRoutes.get('/posts/summaries', anonymousUserMiddleware, async (c) => {
|
||||
const anonymousUserId = c.get('anonymousUserId');
|
||||
publicRoutes.get('/posts/summaries', authMiddleware, async (c) => {
|
||||
const user = c.get('user');
|
||||
const userId = user.email;
|
||||
try {
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT id, emotion_tag, hold_count, dissolved_at
|
||||
FROM post_summaries
|
||||
WHERE anonymous_user_id = ?
|
||||
ORDER BY dissolved_at DESC`
|
||||
).bind(anonymousUserId).all<PostSummary>();
|
||||
).bind(userId).all<PostSummary>();
|
||||
|
||||
return c.json({ summaries: results || [] });
|
||||
} catch (e: any) {
|
||||
@ -286,8 +288,9 @@ publicRoutes.get('/posts/summaries', anonymousUserMiddleware, async (c) => {
|
||||
});
|
||||
|
||||
// 8. Clear Post Summaries
|
||||
publicRoutes.post('/posts/summaries/clear', anonymousUserMiddleware, async (c) => {
|
||||
const anonymousUserId = c.get('anonymousUserId');
|
||||
publicRoutes.post('/posts/summaries/clear', authMiddleware, async (c) => {
|
||||
const user = c.get('user');
|
||||
const userId = user.email;
|
||||
const body = await c.req.json();
|
||||
const summaryIds = body.summaryIds;
|
||||
|
||||
@ -297,7 +300,7 @@ publicRoutes.post('/posts/summaries/clear', anonymousUserMiddleware, async (c) =
|
||||
|
||||
try {
|
||||
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);
|
||||
} catch (e: any) {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
{
|
||||
"binding": "DB",
|
||||
"database_name": "db-xd1uom",
|
||||
"database_id": "23b00c58-c2c5-4f98-abba-ce49c8cfaa0c"
|
||||
"database_id": "c998402c-77a7-4d05-b963-11fe654161b9"
|
||||
}
|
||||
],
|
||||
"r2_buckets": [
|
||||
|
||||
2790
frontend/package-lock.json
generated
2790
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -2,11 +2,10 @@ import { Routes, Route } from 'react-router-dom';
|
||||
import { Toaster } from 'sonner';
|
||||
import { Layout } from '@/components/Layout';
|
||||
import { StreamPage, CreatePostPage, MyDissolvesPage, NotFoundPage } from '@/pages';
|
||||
import { AnonymousIdProvider } from '@/context/AnonymousIdContext';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AnonymousIdProvider>
|
||||
<>
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/" element={<StreamPage />} />
|
||||
@ -16,7 +15,7 @@ function App() {
|
||||
</Routes>
|
||||
</Layout>
|
||||
<Toaster position="bottom-center" toastOptions={{ className: 'font-sans' }} />
|
||||
</AnonymousIdProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
export const API_URL = (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')
|
||||
? "http://127.0.0.1:8787"
|
||||
: "https://app-xd1uom.laflabs-tech.workers.dev";
|
||||
export const API_URL = (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || window.location.hostname.startsWith('192.168.'))
|
||||
? `http://${window.location.hostname}:8787`
|
||||
: "https://app-xd1uom.mworld-projects.workers.dev";
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: API_URL
|
||||
|
||||
@ -12,6 +12,7 @@ export function AudioPlayer({ src }: AudioPlayerProps) {
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current;
|
||||
@ -24,17 +25,36 @@ export function AudioPlayer({ src }: AudioPlayerProps) {
|
||||
const handleTimeUpdate = () => setCurrentTime(audio.currentTime);
|
||||
const handleEnded = () => setIsPlaying(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('timeupdate', handleTimeUpdate);
|
||||
audio.addEventListener('ended', handleEnded);
|
||||
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 () => {
|
||||
audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||
audio.removeEventListener('timeupdate', handleTimeUpdate);
|
||||
audio.removeEventListener('timeupdate', handleTimeUpdate);
|
||||
audio.removeEventListener('ended', handleEnded);
|
||||
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>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
) : hasError ? (
|
||||
<span className="text-xs">!</span>
|
||||
) : isPlaying ? (
|
||||
<Pause className="h-5 w-5" />
|
||||
) : (
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
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 { useAuth } from '@/context/AuthContext';
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
@ -9,11 +10,12 @@ interface LayoutProps {
|
||||
|
||||
export function Layout({ children }: LayoutProps) {
|
||||
const location = useLocation();
|
||||
const { isAuthenticated, login, logout, user } = useAuth();
|
||||
|
||||
const navItems = [
|
||||
{ path: '/', label: 'The Stream' },
|
||||
{ path: '/my-dissolves', label: 'My Dissolves' },
|
||||
];
|
||||
{ path: '/', label: 'The Stream', public: true },
|
||||
{ path: '/my-dissolves', label: 'My Dissolves', public: false },
|
||||
].filter(item => item.public || isAuthenticated);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-canvas font-sans">
|
||||
@ -42,9 +44,28 @@ export function Layout({ children }: LayoutProps) {
|
||||
})}
|
||||
</nav>
|
||||
</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">
|
||||
<Plus className="mr-2 h-4 w-4" /> Share a Feeling
|
||||
</Link>
|
||||
<div className="flex items-center gap-4">
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<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>
|
||||
</header>
|
||||
<main className="container-full py-8 md:py-12">
|
||||
|
||||
@ -6,6 +6,7 @@ import { toast } from 'sonner';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useApi } from '@/hooks/useApi';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import type { Post } from '@/types';
|
||||
import { AudioPlayer } from './AudioPlayer';
|
||||
import { ReportModal } from './ReportModal';
|
||||
@ -21,8 +22,13 @@ export function PostCard({ post }: PostCardProps) {
|
||||
const isHeld = useIsPostHeld(post.id) ?? false;
|
||||
|
||||
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
||||
const { isAuthenticated, login } = useAuth();
|
||||
|
||||
const handleHold = () => {
|
||||
if (!isAuthenticated) {
|
||||
login();
|
||||
return;
|
||||
}
|
||||
if (isHeld) return;
|
||||
holdPost(post.id, {
|
||||
onSuccess: () => {
|
||||
@ -81,7 +87,13 @@ export function PostCard({ post }: PostCardProps) {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<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"
|
||||
aria-label="Report post"
|
||||
>
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -1,129 +1,123 @@
|
||||
import { useMutation, useQuery, useInfiniteQuery, QueryClient } from '@tanstack/react-query';
|
||||
import { api, API_URL } from '../api';
|
||||
import { useAnonymousId } from '@/context/AnonymousIdContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
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, anonymousId: string, data?: any): Promise<{ data: T }> => {
|
||||
const headers = { 'X-Anonymous-User-ID': anonymousId };
|
||||
const callApi = <T,>(method: 'get' | 'post' | 'delete', url: string, data?: any): Promise<{ data: T }> => {
|
||||
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 = () => {
|
||||
const { anonymousId } = useAnonymousId();
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
// Stream
|
||||
const useGetPosts = (emotion?: EmotionTag) => {
|
||||
return useInfiniteQuery(
|
||||
['posts', emotion],
|
||||
async ({ pageParam: cursor }) => {
|
||||
if (!anonymousId) throw new Error('Anonymous ID not available');
|
||||
return useInfiniteQuery({
|
||||
queryKey: ['posts', emotion],
|
||||
queryFn: async ({ pageParam: cursor }) => {
|
||||
let url = '/api/posts?';
|
||||
if (emotion) url += `emotion=${emotion}&`;
|
||||
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;
|
||||
},
|
||||
{
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
enabled: !!anonymousId,
|
||||
}
|
||||
);
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
enabled: true, // Stream is public, no authentication required to view
|
||||
});
|
||||
};
|
||||
|
||||
// Create
|
||||
const useCreatePost = (queryClient: QueryClient) => {
|
||||
return useMutation(
|
||||
({ contentType, contentData, emotionTag }: { contentType: PostContentType, contentData: string, emotionTag: EmotionTag }) => {
|
||||
if (!anonymousId) throw new Error('Anonymous ID not available');
|
||||
return callApi('post', '/api/posts', anonymousId, { contentType, contentData, emotionTag });
|
||||
return useMutation({
|
||||
mutationFn: ({ contentType, contentData, emotionTag }: { contentType: PostContentType, contentData: string, emotionTag: EmotionTag }) => {
|
||||
if (!isAuthenticated) throw new Error('Must be logged in to post');
|
||||
return callApi('post', '/api/posts', { contentType, contentData, emotionTag });
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['my-posts']);
|
||||
queryClient.invalidateQueries(['posts']);
|
||||
},
|
||||
}
|
||||
);
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['my-posts'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['posts'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const useUploadFile = () => {
|
||||
return useMutation(
|
||||
(file: File) => {
|
||||
if (!anonymousId) throw new Error('Anonymous ID not available');
|
||||
return useMutation({
|
||||
mutationFn: (file: File) => {
|
||||
if (!isAuthenticated) throw new Error('Must be logged in to upload');
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return callApi<{url: string}>('post', '/api/upload', anonymousId, formData);
|
||||
return callApi<{url: string}>('post', '/api/upload', formData);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// My Dissolves
|
||||
const useGetMyPosts = () => {
|
||||
return useQuery(
|
||||
['my-posts'],
|
||||
() => {
|
||||
if (!anonymousId) throw new Error('Anonymous ID not available');
|
||||
return callApi<{posts: Post[]}>('get', '/api/posts/me', anonymousId).then(res => res.data);
|
||||
return useQuery({
|
||||
queryKey: ['my-posts'],
|
||||
queryFn: () => {
|
||||
if (!isAuthenticated) throw new Error('Must be logged in');
|
||||
return callApi<{posts: Post[]}>('get', '/api/posts/me').then(res => res.data);
|
||||
},
|
||||
{ enabled: !!anonymousId }
|
||||
);
|
||||
enabled: isAuthenticated
|
||||
});
|
||||
};
|
||||
|
||||
const useGetSummaries = () => {
|
||||
return useQuery(
|
||||
['summaries'],
|
||||
() => {
|
||||
if (!anonymousId) throw new Error('Anonymous ID not available');
|
||||
return callApi<{summaries: PostSummary[]}>('get', '/api/posts/summaries', anonymousId).then(res => res.data);
|
||||
return useQuery({
|
||||
queryKey: ['summaries'],
|
||||
queryFn: () => {
|
||||
if (!isAuthenticated) throw new Error('Must be logged in');
|
||||
return callApi<{summaries: PostSummary[]}>('get', '/api/posts/summaries').then(res => res.data);
|
||||
},
|
||||
{ enabled: !!anonymousId }
|
||||
);
|
||||
enabled: isAuthenticated
|
||||
});
|
||||
};
|
||||
|
||||
const useClearSummaries = (queryClient: QueryClient) => {
|
||||
return useMutation(
|
||||
(summaryIds: string[]) => {
|
||||
if (!anonymousId) throw new Error('Anonymous ID not available');
|
||||
return callApi('post', '/api/posts/summaries/clear', anonymousId, { summaryIds });
|
||||
return useMutation({
|
||||
mutationFn: (summaryIds: string[]) => {
|
||||
if (!isAuthenticated) throw new Error('Must be logged in');
|
||||
return callApi('post', '/api/posts/summaries/clear', { summaryIds });
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['summaries']);
|
||||
},
|
||||
}
|
||||
);
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['summaries'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Interactions
|
||||
const useHoldPost = (queryClient: QueryClient) => {
|
||||
return useMutation(
|
||||
(postId: string) => {
|
||||
if (!anonymousId) throw new Error('Anonymous ID not available');
|
||||
return callApi('post', `/api/posts/${postId}/hold`, anonymousId);
|
||||
return useMutation({
|
||||
mutationFn: (postId: string) => {
|
||||
if (!isAuthenticated) throw new Error('Must be logged in');
|
||||
return callApi('post', `/api/posts/${postId}/hold`);
|
||||
},
|
||||
{
|
||||
onSuccess: (_data, postId) => {
|
||||
queryClient.setQueryData(['held-posts', postId], true);
|
||||
}
|
||||
onSuccess: (_data, postId) => {
|
||||
queryClient.setQueryData(['held-posts', postId], true);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const useReportPost = () => {
|
||||
return useMutation(
|
||||
({ postId, reason }: { postId: string, reason: string }) => {
|
||||
if (!anonymousId) throw new Error('Anonymous ID not available');
|
||||
return callApi('post', `/api/posts/${postId}/report`, anonymousId, { reason });
|
||||
return useMutation({
|
||||
mutationFn: ({ postId, reason }: { postId: string, reason: string }) => {
|
||||
if (!isAuthenticated) throw new Error('Must be logged in');
|
||||
return callApi('post', `/api/posts/${postId}/report`, { reason });
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -136,3 +130,4 @@ export const useApi = () => {
|
||||
|
||||
return { useGetPosts, useCreatePost, useUploadFile, useGetMyPosts, useGetSummaries, useClearSummaries, useHoldPost, useReportPost, useIsPostHeld, getMediaUrl };
|
||||
};
|
||||
|
||||
|
||||
@ -22,7 +22,6 @@ export const useCountdown = (targetDate: string | Date) => {
|
||||
format: ['hours', 'minutes', 'seconds'],
|
||||
zero: true,
|
||||
delimiter: ':',
|
||||
pad: { hours: 2, minutes: 2, seconds: 2 },
|
||||
});
|
||||
// Special handling for formatDuration output
|
||||
const parts = formatted.split(':').map(p => p.padStart(2, '0'));
|
||||
|
||||
@ -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 components;
|
||||
@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 {
|
||||
:root {
|
||||
--canvas: 0 0% 100%; /* #ffffff */
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { motion } from 'framer-motion';
|
||||
import { toast } from 'sonner';
|
||||
@ -18,6 +19,15 @@ const contentTypes: { type: PostContentType, label: string, icon: React.ElementT
|
||||
|
||||
export default function CreatePostPage() {
|
||||
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 { useCreatePost, useUploadFile } = useApi();
|
||||
const createPostMutation = useCreatePost(queryClient);
|
||||
@ -53,8 +63,10 @@ export default function CreatePostPage() {
|
||||
audioChunksRef.current = [];
|
||||
recorder.ondataavailable = (event) => audioChunksRef.current.push(event.data);
|
||||
recorder.onstop = () => {
|
||||
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' });
|
||||
const audioFile = new File([audioBlob], 'recording.webm', { type: 'audio/webm' });
|
||||
const mimeType = recorder.mimeType || '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);
|
||||
stream.getTracks().forEach(track => track.stop()); // Stop microphone access
|
||||
};
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
@ -10,6 +12,16 @@ import { MyPostCard } from '@/components/MyPostCard';
|
||||
import type { PostSummary } from '@/types';
|
||||
|
||||
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 { useGetMyPosts, useGetSummaries, useClearSummaries } = useApi();
|
||||
const { data: myPostsData, isLoading: isLoadingPosts } = useGetMyPosts();
|
||||
|
||||
@ -32,7 +32,7 @@ module.exports = {
|
||||
full: '9999px',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Airbnb Cereal VF', 'Circular', 'sans-serif'],
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
},
|
||||
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)',
|
||||
|
||||
@ -17,7 +17,11 @@
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx"
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user