add:logs
This commit is contained in:
641
finds.md
641
finds.md
@@ -1,641 +0,0 @@
|
||||
# Serengo "Finds" Feature - Implementation Specification
|
||||
|
||||
## Feature Overview
|
||||
|
||||
Add a "Finds" feature to Serengo that allows users to save, share, and discover location-based experiences. A Find represents a memorable place that users want to share with friends, complete with photos/videos, reviews, and precise location data.
|
||||
|
||||
## Core Concept
|
||||
|
||||
**What is a Find?**
|
||||
|
||||
- A user-created post about a specific location they've visited
|
||||
- Contains: location coordinates, title, description/review, media (photos/videos), timestamp
|
||||
- Shareable with friends and the Serengo community
|
||||
- Displayed as interactive markers on the map
|
||||
|
||||
## Database Schema
|
||||
|
||||
Add the following tables to `src/lib/server/db/schema.ts`:
|
||||
|
||||
```typescript
|
||||
export const find = pgTable('find', {
|
||||
id: text('id').primaryKey(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
title: text('title').notNull(),
|
||||
description: text('description'),
|
||||
latitude: numeric('latitude', { precision: 10, scale: 7 }).notNull(),
|
||||
longitude: numeric('longitude', { precision: 10, scale: 7 }).notNull(),
|
||||
locationName: text('location_name'), // e.g., "Café Belga, Brussels"
|
||||
category: text('category'), // e.g., "cafe", "restaurant", "park", "landmark"
|
||||
isPublic: boolean('is_public').default(true),
|
||||
createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'date' }).defaultNow().notNull()
|
||||
});
|
||||
|
||||
export const findMedia = pgTable('find_media', {
|
||||
id: text('id').primaryKey(),
|
||||
findId: text('find_id')
|
||||
.notNull()
|
||||
.references(() => find.id, { onDelete: 'cascade' }),
|
||||
type: text('type').notNull(), // 'photo' or 'video'
|
||||
url: text('url').notNull(),
|
||||
thumbnailUrl: text('thumbnail_url'),
|
||||
orderIndex: integer('order_index').default(0),
|
||||
createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' }).defaultNow().notNull()
|
||||
});
|
||||
|
||||
export const findLike = pgTable('find_like', {
|
||||
id: text('id').primaryKey(),
|
||||
findId: text('find_id')
|
||||
.notNull()
|
||||
.references(() => find.id, { onDelete: 'cascade' }),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' }).defaultNow().notNull()
|
||||
});
|
||||
|
||||
export const friendship = pgTable('friendship', {
|
||||
id: text('id').primaryKey(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
friendId: text('friend_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
status: text('status').notNull(), // 'pending', 'accepted', 'blocked'
|
||||
createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' }).defaultNow().notNull()
|
||||
});
|
||||
```
|
||||
|
||||
## UI Components to Create
|
||||
|
||||
### 1. FindsMap Component (`src/lib/components/FindsMap.svelte`)
|
||||
|
||||
- Extends the existing Map component
|
||||
- Displays custom markers for Finds (different from location marker)
|
||||
- Clicking a Find marker opens a FindPreview modal
|
||||
- Shows only friends' Finds by default, with toggle for public Finds
|
||||
- Clusters markers when zoomed out for better performance
|
||||
|
||||
### 2. CreateFindModal Component (`src/lib/components/CreateFindModal.svelte`)
|
||||
|
||||
- Modal that opens when user clicks "Create Find" button
|
||||
- Form fields:
|
||||
- Title (required, max 100 chars)
|
||||
- Description/Review (optional, max 500 chars)
|
||||
- Location (auto-filled from user's current location, editable)
|
||||
- Category selector (dropdown)
|
||||
- Photo/Video upload (up to 5 files)
|
||||
- Privacy toggle (Public/Friends Only)
|
||||
- Image preview grid with drag-to-reorder
|
||||
- Submit creates Find and refreshes map
|
||||
|
||||
### 3. FindPreview Component (`src/lib/components/FindPreview.svelte`)
|
||||
|
||||
- Compact card showing Find details
|
||||
- Image/video carousel
|
||||
- Title, description, username, timestamp
|
||||
- Like button with count
|
||||
- Share button
|
||||
- "Get Directions" button (opens in maps app)
|
||||
- Edit/Delete buttons (if user owns the Find)
|
||||
|
||||
### 4. FindsList Component (`src/lib/components/FindsList.svelte`)
|
||||
|
||||
- Feed view of Finds (alternative to map view)
|
||||
- Infinite scroll or pagination
|
||||
- Filter by: Friends, Public, Category, Date range
|
||||
- Sort by: Recent, Popular (most liked), Nearest
|
||||
|
||||
### 5. FindsFilter Component (`src/lib/components/FindsFilter.svelte`)
|
||||
|
||||
- Dropdown/sidebar with filters:
|
||||
- View: Friends Only / Public / My Finds
|
||||
- Category: All / Cafes / Restaurants / Parks / etc.
|
||||
- Distance: Within 1km / 5km / 10km / 50km / Any
|
||||
|
||||
## Routes to Add
|
||||
|
||||
### `/finds` - Main Finds page
|
||||
|
||||
- Server load: Fetch user's friends' Finds + public Finds within radius
|
||||
- Toggle between Map view and List view
|
||||
- "Create Find" floating action button
|
||||
- Integrated FindsFilter component
|
||||
|
||||
### `/finds/[id]` - Individual Find detail page
|
||||
|
||||
- Full Find details with all photos/videos
|
||||
- Comments section (future feature)
|
||||
- Share button with social media integration
|
||||
- Similar Finds nearby
|
||||
|
||||
### `/finds/create` - Create Find page (alternative to modal)
|
||||
|
||||
- Full-page form for creating a Find
|
||||
- Better for mobile experience
|
||||
- Can use when coming from external share (future feature)
|
||||
|
||||
### `/profile/[userId]/finds` - User's Finds profile
|
||||
|
||||
- Grid view of all user's Finds
|
||||
- Can filter by category
|
||||
- Private Finds only visible to owner
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### `POST /api/finds` - Create Find
|
||||
|
||||
- Upload images/videos to storage (suggest Cloudflare R2 or similar)
|
||||
- Create Find record in database
|
||||
- Return created Find with media URLs
|
||||
|
||||
### `GET /api/finds?lat={lat}&lng={lng}&radius={km}` - Get nearby Finds
|
||||
|
||||
- Query Finds within radius of coordinates
|
||||
- Filter by privacy settings and friendship status
|
||||
- Return with user info and media
|
||||
|
||||
### `PATCH /api/finds/[id]` - Update Find
|
||||
|
||||
- Only owner can update
|
||||
- Update title, description, category, privacy
|
||||
|
||||
### `DELETE /api/finds/[id]` - Delete Find
|
||||
|
||||
- Only owner can delete
|
||||
- Cascade delete media files
|
||||
|
||||
### `POST /api/finds/[id]/like` - Like/Unlike Find
|
||||
|
||||
- Toggle like status
|
||||
- Return updated like count
|
||||
|
||||
## File Upload Strategy with Cloudflare R2
|
||||
|
||||
**Using Cloudflare R2 (Free Tier: 10GB storage, 1M Class A ops/month, 10M Class B ops/month)**
|
||||
|
||||
### Setup Configuration
|
||||
|
||||
Create `src/lib/server/r2.ts`:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
S3Client,
|
||||
PutObjectCommand,
|
||||
DeleteObjectCommand,
|
||||
GetObjectCommand
|
||||
} from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import {
|
||||
R2_ACCOUNT_ID,
|
||||
R2_ACCESS_KEY_ID,
|
||||
R2_SECRET_ACCESS_KEY,
|
||||
R2_BUCKET_NAME
|
||||
} from '$env/static/private';
|
||||
|
||||
export const r2Client = new S3Client({
|
||||
region: 'auto',
|
||||
endpoint: `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
||||
credentials: {
|
||||
accessKeyId: R2_ACCESS_KEY_ID,
|
||||
secretAccessKey: R2_SECRET_ACCESS_KEY
|
||||
}
|
||||
});
|
||||
|
||||
export const R2_PUBLIC_URL = `https://pub-${R2_ACCOUNT_ID}.r2.dev`; // Configure custom domain for production
|
||||
|
||||
// Upload file to R2
|
||||
export async function uploadToR2(file: File, path: string, contentType: string): Promise<string> {
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
await r2Client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: R2_BUCKET_NAME,
|
||||
Key: path,
|
||||
Body: buffer,
|
||||
ContentType: contentType,
|
||||
// Cache control for PWA compatibility
|
||||
CacheControl: 'public, max-age=31536000, immutable'
|
||||
})
|
||||
);
|
||||
|
||||
return `${R2_PUBLIC_URL}/${path}`;
|
||||
}
|
||||
|
||||
// Delete file from R2
|
||||
export async function deleteFromR2(path: string): Promise<void> {
|
||||
await r2Client.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: R2_BUCKET_NAME,
|
||||
Key: path
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Generate signed URL for temporary access (optional, for private files)
|
||||
export async function getSignedR2Url(path: string, expiresIn = 3600): Promise<string> {
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: R2_BUCKET_NAME,
|
||||
Key: path
|
||||
});
|
||||
|
||||
return await getSignedUrl(r2Client, command, { expiresIn });
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Add to `.env`:
|
||||
|
||||
```
|
||||
R2_ACCOUNT_ID=your_account_id
|
||||
R2_ACCESS_KEY_ID=your_access_key
|
||||
R2_SECRET_ACCESS_KEY=your_secret_key
|
||||
R2_BUCKET_NAME=serengo-finds
|
||||
```
|
||||
|
||||
### Image Processing & Thumbnail Generation
|
||||
|
||||
Create `src/lib/server/media-processor.ts`:
|
||||
|
||||
```typescript
|
||||
import sharp from 'sharp';
|
||||
import { uploadToR2 } from './r2';
|
||||
|
||||
const THUMBNAIL_SIZE = 400;
|
||||
const MAX_IMAGE_SIZE = 1920;
|
||||
|
||||
export async function processAndUploadImage(
|
||||
file: File,
|
||||
findId: string,
|
||||
index: number
|
||||
): Promise<{ url: string; thumbnailUrl: string }> {
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
// Generate unique filename
|
||||
const timestamp = Date.now();
|
||||
const extension = file.name.split('.').pop();
|
||||
const filename = `finds/${findId}/image-${index}-${timestamp}`;
|
||||
|
||||
// Process full-size image (resize if too large, optimize)
|
||||
const processedImage = await sharp(buffer)
|
||||
.resize(MAX_IMAGE_SIZE, MAX_IMAGE_SIZE, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true
|
||||
})
|
||||
.jpeg({ quality: 85, progressive: true })
|
||||
.toBuffer();
|
||||
|
||||
// Generate thumbnail
|
||||
const thumbnail = await sharp(buffer)
|
||||
.resize(THUMBNAIL_SIZE, THUMBNAIL_SIZE, {
|
||||
fit: 'cover',
|
||||
position: 'centre'
|
||||
})
|
||||
.jpeg({ quality: 80 })
|
||||
.toBuffer();
|
||||
|
||||
// Upload both to R2
|
||||
const imageFile = new File([processedImage], `${filename}.jpg`, { type: 'image/jpeg' });
|
||||
const thumbFile = new File([thumbnail], `${filename}-thumb.jpg`, { type: 'image/jpeg' });
|
||||
|
||||
const [url, thumbnailUrl] = await Promise.all([
|
||||
uploadToR2(imageFile, `${filename}.jpg`, 'image/jpeg'),
|
||||
uploadToR2(thumbFile, `${filename}-thumb.jpg`, 'image/jpeg')
|
||||
]);
|
||||
|
||||
return { url, thumbnailUrl };
|
||||
}
|
||||
|
||||
export async function processAndUploadVideo(
|
||||
file: File,
|
||||
findId: string,
|
||||
index: number
|
||||
): Promise<{ url: string; thumbnailUrl: string }> {
|
||||
const timestamp = Date.now();
|
||||
const filename = `finds/${findId}/video-${index}-${timestamp}.mp4`;
|
||||
|
||||
// Upload video directly (no processing on server to save resources)
|
||||
const url = await uploadToR2(file, filename, 'video/mp4');
|
||||
|
||||
// For video thumbnail, generate on client-side or use placeholder
|
||||
// This keeps server-side processing minimal
|
||||
const thumbnailUrl = `/video-placeholder.jpg`; // Use static placeholder
|
||||
|
||||
return { url, thumbnailUrl };
|
||||
}
|
||||
```
|
||||
|
||||
### PWA Service Worker Compatibility
|
||||
|
||||
Update `src/service-worker.ts` to handle R2 media:
|
||||
|
||||
```typescript
|
||||
// Add to the existing service worker after the IMAGE_CACHE declaration
|
||||
|
||||
const R2_DOMAINS = [
|
||||
'pub-', // R2 public URLs pattern
|
||||
'r2.dev',
|
||||
'r2.cloudflarestorage.com'
|
||||
];
|
||||
|
||||
// In the fetch event listener, update image handling:
|
||||
|
||||
// Handle R2 media with cache-first strategy
|
||||
if (url.hostname.includes('r2.dev') || R2_DOMAINS.some((domain) => url.hostname.includes(domain))) {
|
||||
const cachedResponse = await imageCache.match(event.request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(event.request);
|
||||
if (response.ok) {
|
||||
// Cache R2 media for offline access
|
||||
imageCache.put(event.request, response.clone());
|
||||
}
|
||||
return response;
|
||||
} catch {
|
||||
// Return cached version or fallback
|
||||
return cachedResponse || new Response('Media not available offline', { status: 404 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Endpoint for Upload
|
||||
|
||||
Create `src/routes/api/finds/upload/+server.ts`:
|
||||
|
||||
```typescript
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { processAndUploadImage, processAndUploadVideo } from '$lib/server/media-processor';
|
||||
import { generateUserId } from '$lib/server/auth'; // Reuse ID generation
|
||||
|
||||
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
|
||||
const MAX_FILES = 5;
|
||||
const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
const ALLOWED_VIDEO_TYPES = ['video/mp4', 'video/quicktime'];
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
if (!locals.user) {
|
||||
throw error(401, 'Unauthorized');
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const files = formData.getAll('files') as File[];
|
||||
const findId = (formData.get('findId') as string) || generateUserId(); // Generate if creating new Find
|
||||
|
||||
if (files.length === 0 || files.length > MAX_FILES) {
|
||||
throw error(400, `Must upload between 1 and ${MAX_FILES} files`);
|
||||
}
|
||||
|
||||
const uploadedMedia: Array<{ type: string; url: string; thumbnailUrl: string }> = [];
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
|
||||
// Validate file size
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
throw error(400, `File ${file.name} exceeds maximum size of 100MB`);
|
||||
}
|
||||
|
||||
// Process based on type
|
||||
if (ALLOWED_IMAGE_TYPES.includes(file.type)) {
|
||||
const result = await processAndUploadImage(file, findId, i);
|
||||
uploadedMedia.push({ type: 'photo', ...result });
|
||||
} else if (ALLOWED_VIDEO_TYPES.includes(file.type)) {
|
||||
const result = await processAndUploadVideo(file, findId, i);
|
||||
uploadedMedia.push({ type: 'video', ...result });
|
||||
} else {
|
||||
throw error(400, `File type ${file.type} not allowed`);
|
||||
}
|
||||
}
|
||||
|
||||
return json({ findId, media: uploadedMedia });
|
||||
};
|
||||
```
|
||||
|
||||
### Client-Side Upload Component
|
||||
|
||||
Update `CreateFindModal.svelte` to handle uploads:
|
||||
|
||||
```typescript
|
||||
async function handleFileUpload(files: FileList) {
|
||||
const formData = new FormData();
|
||||
|
||||
Array.from(files).forEach((file) => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
|
||||
// If editing existing Find, include findId
|
||||
if (existingFindId) {
|
||||
formData.append('findId', existingFindId);
|
||||
}
|
||||
|
||||
const response = await fetch('/api/finds/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Store result for Find creation
|
||||
uploadedMedia = result.media;
|
||||
generatedFindId = result.findId;
|
||||
}
|
||||
```
|
||||
|
||||
### R2 Bucket Configuration
|
||||
|
||||
**Important R2 Settings for PWA:**
|
||||
|
||||
1. **Public Access:** Enable public bucket access for `serengo-finds` bucket
|
||||
2. **CORS Configuration:**
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"AllowedOrigins": ["https://serengo.ziasvannes.tech", "http://localhost:5173"],
|
||||
"AllowedMethods": ["GET", "HEAD"],
|
||||
"AllowedHeaders": ["*"],
|
||||
"MaxAgeSeconds": 3600
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
3. **Custom Domain (Recommended):**
|
||||
- Point `media.serengo.ziasvannes.tech` to R2 bucket
|
||||
- Enables better caching and CDN integration
|
||||
- Update `R2_PUBLIC_URL` in r2.ts
|
||||
|
||||
4. **Cache Headers:** Files uploaded with `Cache-Control: public, max-age=31536000, immutable`
|
||||
- PWA service worker can cache indefinitely
|
||||
- Perfect for user-uploaded content that won't change
|
||||
|
||||
### Offline Support Strategy
|
||||
|
||||
**How PWA handles R2 media:**
|
||||
|
||||
1. **First Visit (Online):**
|
||||
- Finds and media load from R2
|
||||
- Service worker caches all media in `IMAGE_CACHE`
|
||||
- Thumbnails cached for fast list views
|
||||
|
||||
2. **Subsequent Visits (Online):**
|
||||
- Service worker serves from cache immediately (cache-first)
|
||||
- No network delay for previously viewed media
|
||||
|
||||
3. **Offline:**
|
||||
- All cached Finds and media available
|
||||
- New Find creation queued (implement with IndexedDB)
|
||||
- Upload when connection restored
|
||||
|
||||
### Cost Optimization
|
||||
|
||||
**R2 Free Tier Limits:**
|
||||
|
||||
- 10GB storage (≈ 10,000 high-quality photos or 100 videos)
|
||||
- 1M Class A operations (write/upload)
|
||||
- 10M Class B operations (read/download)
|
||||
|
||||
**Stay Within Free Tier:**
|
||||
|
||||
- Aggressive thumbnail generation (reduces bandwidth)
|
||||
- Cache-first strategy (reduces reads)
|
||||
- Lazy load images (only fetch what's visible)
|
||||
- Compress images server-side (sharp optimization)
|
||||
- Video uploads: limit to 100MB, encourage shorter clips
|
||||
|
||||
**Monitoring:**
|
||||
|
||||
- Track R2 usage in Cloudflare dashboard
|
||||
- Alert at 80% of free tier limits
|
||||
- Implement rate limiting on uploads (max 10 Finds/day per user)
|
||||
|
||||
## Map Integration
|
||||
|
||||
Update `src/lib/components/Map.svelte`:
|
||||
|
||||
- Add prop `finds?: Find[]` to display Find markers
|
||||
- Create custom Find marker component with distinct styling
|
||||
- Different marker colors for:
|
||||
- Your own Finds (blue)
|
||||
- Friends' Finds (green)
|
||||
- Public Finds (orange)
|
||||
- Add clustering for better performance with many markers
|
||||
- Implement marker click handler to open FindPreview modal
|
||||
|
||||
## Social Features (Phase 2)
|
||||
|
||||
### Friends System
|
||||
|
||||
- Add friend request functionality
|
||||
- Accept/decline friend requests
|
||||
- Friends list page
|
||||
- Privacy settings respect friendship status
|
||||
|
||||
### Notifications
|
||||
|
||||
- Notify when friend creates a Find nearby
|
||||
- Notify when someone likes your Find
|
||||
- Notify on friend request
|
||||
|
||||
### Discovery Feed
|
||||
|
||||
- Trending Finds in your area
|
||||
- Recommended Finds based on your history
|
||||
- Explore by category
|
||||
|
||||
## Mobile Considerations
|
||||
|
||||
- FindsMap should be touch-friendly with proper zoom controls
|
||||
- Create Find button as floating action button (FAB) for easy access
|
||||
- Optimize image upload for mobile (compress before upload)
|
||||
- Consider PWA features for photo capture
|
||||
- Offline support: Queue Finds when offline, sync when online
|
||||
|
||||
## Security & Privacy
|
||||
|
||||
- Validate all user inputs (title, description lengths)
|
||||
- Sanitize text content to prevent XSS
|
||||
- Check ownership before allowing edit/delete
|
||||
- Respect privacy settings in all queries
|
||||
- Rate limit Find creation (e.g., max 20 per day)
|
||||
- Implement CSRF protection (already in place)
|
||||
- Validate file types and sizes on upload
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
- Lazy load images in FindsList and FindPreview
|
||||
- Implement virtual scrolling for long lists
|
||||
- Cache Find queries with stale-while-revalidate
|
||||
- Use CDN for media files
|
||||
- Implement marker clustering on map
|
||||
- Paginate Finds feed (20-50 per page)
|
||||
- Index database on: (latitude, longitude), userId, createdAt
|
||||
|
||||
## Styling Guidelines
|
||||
|
||||
- Use existing Serengo design system and components
|
||||
- Find markers should be visually distinct from location marker
|
||||
- Maintain newspaper-inspired aesthetic
|
||||
- Use Washington font for Find titles
|
||||
- Keep UI clean and uncluttered
|
||||
- Smooth animations for modal open/close
|
||||
- Skeleton loading states for Finds feed
|
||||
|
||||
## Success Metrics to Track
|
||||
|
||||
- Number of Finds created per user
|
||||
- Photo/video upload rate
|
||||
- Likes per Find
|
||||
- Map engagement (clicks on Find markers)
|
||||
- Share rate
|
||||
- Friend connections made
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
**Phase 1 (MVP):**
|
||||
|
||||
1. Database schema and migrations
|
||||
2. Create Find functionality (with photos only)
|
||||
3. FindsMap with markers
|
||||
4. FindPreview modal
|
||||
5. Basic Find feed
|
||||
|
||||
**Phase 2:** 6. Video support 7. Like functionality 8. Friends system 9. Enhanced filters and search 10. Share functionality
|
||||
|
||||
**Phase 3:** 11. Comments on Finds 12. Discovery feed 13. Notifications 14. Advanced analytics
|
||||
|
||||
## Example User Flow
|
||||
|
||||
1. User opens Serengo app and sees map with their friends' Finds
|
||||
2. User visits a cool café and wants to share it
|
||||
3. Clicks "Create Find" FAB button
|
||||
4. Modal opens with their current location pre-filled
|
||||
5. User adds title "Amazing Belgian Waffles at Café Belga"
|
||||
6. Writes review: "Best waffles I've had in Brussels! Great atmosphere too."
|
||||
7. Uploads 2 photos of the waffles and café interior
|
||||
8. Selects category "Cafe" and keeps it Public
|
||||
9. Clicks "Share Find"
|
||||
10. Find appears on map as new marker
|
||||
11. Friends see it in their feed and can like/save it
|
||||
12. Friends can click marker to see details and get directions
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Run database migrations to create new tables
|
||||
2. Implement basic Create Find API endpoint
|
||||
3. Build CreateFindModal component
|
||||
4. Update Map component to display Find markers
|
||||
5. Test end-to-end flow
|
||||
6. Iterate based on user feedback
|
||||
|
||||
This feature will transform Serengo from a simple location app into a social discovery platform where users share and explore unexpected places together.
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Serengo is a location-based social discovery platform where users can save, share, and discover memorable places with media, reviews, and precise location data.
|
||||
|
||||
## Current Status: Phase 2A & 2C Complete + UI Integration ✅
|
||||
## Current Status: Phase 2A, 2C & 2D Complete + UI Integration ✅
|
||||
|
||||
### What Serengo Currently Has:
|
||||
|
||||
@@ -14,6 +14,7 @@ Serengo is a location-based social discovery platform where users can save, shar
|
||||
- **NEW**: Modern WebP image processing with JPEG fallbacks
|
||||
- **NEW**: Full video support with custom VideoPlayer component
|
||||
- **NEW**: Like/unlike system with optimistic UI updates
|
||||
- **NEW**: Complete friends & privacy system with friend requests and filtered feeds
|
||||
- R2 storage integration with enhanced media processing
|
||||
- Full database schema with proper relationships and social features
|
||||
- Type-safe API endpoints and error handling
|
||||
@@ -27,6 +28,9 @@ Serengo is a location-based social discovery platform where users can save, shar
|
||||
- **NEW**: Video playback with custom controls and fullscreen support
|
||||
- **NEW**: Like/unlike finds with real-time count updates
|
||||
- **NEW**: Animated like buttons with heart animations
|
||||
- **NEW**: Friend request system with send/accept/decline functionality
|
||||
- **NEW**: Friends management page with user search and relationship status
|
||||
- **NEW**: Privacy-aware find feeds with friend-specific visibility filters
|
||||
- Share functionality with clipboard copy
|
||||
- Real-time map markers with click-to-preview
|
||||
- Grid layout for find browsing
|
||||
@@ -108,19 +112,45 @@ Serengo is a location-based social discovery platform where users can save, shar
|
||||
|
||||
**Actual Effort**: ~15 hours (including UI integration)
|
||||
|
||||
### Phase 2D: Friends & Privacy System (Priority: Medium-High)
|
||||
### Phase 2D: Friends & Privacy System ✅ COMPLETE
|
||||
|
||||
**Goal**: Social connections and privacy controls
|
||||
|
||||
**Tasks:**
|
||||
**Completed Tasks:**
|
||||
|
||||
- [ ] Build friend request system (send/accept/decline)
|
||||
- [ ] Create Friends management page using SHADCN components
|
||||
- [ ] Implement friend search with user suggestions
|
||||
- [ ] Update find privacy logic to respect friendships
|
||||
- [ ] Add friend-specific find visibility filters
|
||||
- [x] Build friend request system (send/accept/decline)
|
||||
- [x] Create Friends management page using SHADCN components
|
||||
- [x] Implement friend search with user suggestions
|
||||
- [x] Update find privacy logic to respect friendships
|
||||
- [x] Add friend-specific find visibility filters
|
||||
|
||||
**Estimated Effort**: 20-25 hours
|
||||
**Implementation Details:**
|
||||
|
||||
- Created comprehensive friends API with `/api/friends` and `/api/friends/[friendshipId]` endpoints
|
||||
- Built `/api/users` endpoint for user search with friendship status integration
|
||||
- Developed complete Friends management page (`/routes/friends/`) with tabs for:
|
||||
- Friends list with remove functionality
|
||||
- Friend requests (received/sent) with accept/decline actions
|
||||
- User search with friend request sending capabilities
|
||||
- Enhanced finds API to support friend-based privacy filtering with `includeFriends` parameter
|
||||
- Created `FindsFilter.svelte` component with filter options:
|
||||
- All Finds (public, friends, and user's finds)
|
||||
- Public Only (publicly visible finds)
|
||||
- Friends Only (finds from friends)
|
||||
- My Finds (user's own finds)
|
||||
- Updated main page with integrated filter dropdown and real-time filtering
|
||||
- Enhanced ProfilePanel with Friends navigation link
|
||||
- Maintained type safety and error handling throughout all implementations
|
||||
|
||||
**Technical Architecture:**
|
||||
|
||||
- Leveraged existing `friendship` table schema without modifications
|
||||
- Used SHADCN components (Cards, Badges, Avatars, Buttons, Dropdowns) for consistent UI
|
||||
- Implemented proper authentication and authorization on all endpoints
|
||||
- Added comprehensive error handling with descriptive user feedback
|
||||
- Followed Svelte 5 patterns with `$state`, `$derived`, and `$props` runes
|
||||
|
||||
**Actual Effort**: ~22 hours
|
||||
|
||||
### Phase 2E: Advanced Filtering & Discovery (Priority: Medium)
|
||||
|
||||
@@ -176,11 +206,36 @@ Serengo is a location-based social discovery platform where users can save, shar
|
||||
- Optimize images/videos for web delivery
|
||||
|
||||
**Total Phase 2 Estimated Effort**: 82-105 hours
|
||||
**Total Phase 2 Completed Effort**: ~49 hours (Phases 2A: 12h + 2C: 15h + 2D: 22h)
|
||||
**Expected Timeline**: 8-10 weeks (part-time development)
|
||||
|
||||
## Next Steps:
|
||||
|
||||
1. Begin with Phase 2A (Modern Media Support) for immediate impact
|
||||
2. Implement Phase 2B (Google Maps POI) for better UX
|
||||
3. Add Phase 2C (Social Interactions) for user engagement
|
||||
4. Continue with remaining phases based on user feedback
|
||||
1. ✅ **Completed**: Phase 2A (Modern Media Support) for immediate impact
|
||||
2. **Next Priority**: Phase 2B (Google Maps POI) for better UX
|
||||
3. ✅ **Completed**: Phase 2C (Social Interactions) for user engagement
|
||||
4. ✅ **Completed**: Phase 2D (Friends & Privacy System) for social connections
|
||||
5. **Continue with**: Phase 2E (Advanced Filtering) or 2F (Enhanced Sharing) based on user feedback
|
||||
|
||||
## Production Ready Features Summary:
|
||||
|
||||
**Core Functionality:**
|
||||
|
||||
- Create/view finds with photos, videos, descriptions, and categories
|
||||
- Location-based filtering and discovery with interactive map
|
||||
- Media carousel with navigation (WebP images and MP4 videos)
|
||||
- Real-time map markers with click-to-preview functionality
|
||||
|
||||
**Social Features:**
|
||||
|
||||
- Like/unlike finds with real-time count updates and animations
|
||||
- Friend request system (send, accept, decline, remove)
|
||||
- Friends management page with user search capabilities
|
||||
- Privacy-aware find feeds with customizable visibility filters
|
||||
|
||||
**Technical Excellence:**
|
||||
|
||||
- Modern media formats (WebP, MP4) with fallback support
|
||||
- Type-safe API endpoints with comprehensive error handling
|
||||
- Mobile-optimized responsive design
|
||||
- Performance-optimized with proper caching and lazy loading
|
||||
|
||||
@@ -2,6 +2,35 @@
|
||||
|
||||
## Oktober 2025
|
||||
|
||||
### 16 Oktober 2025 (Woensdag) - 6 uren
|
||||
|
||||
**Werk uitgevoerd:**
|
||||
|
||||
- **Phase 2D: Friends & Privacy System - Volledige implementatie**
|
||||
- Complete vriendschapssysteem geïmplementeerd (verzenden/accepteren/weigeren/verwijderen)
|
||||
- Friends management pagina ontwikkeld met gebruikerszoekfunctionaliteit
|
||||
- Privacy-bewuste find filtering met vriendenspecifieke zichtbaarheid
|
||||
- API endpoints voor vriendschapsbeheer (/api/friends, /api/friends/[friendshipId])
|
||||
- Gebruikerszoek API met vriendschapsstatus integratie (/api/users)
|
||||
- FindsFilter component met 4 filteropties (All/Public/Friends/Mine)
|
||||
- Hoofdpagina uitgebreid met geïntegreerde filteringfunctionaliteit
|
||||
- ProfilePanel uitgebreid met Friends navigatielink
|
||||
- Type-safe implementatie met volledige error handling
|
||||
|
||||
**Commits:** Nog niet gecommit (staged changes)
|
||||
|
||||
**Details:**
|
||||
|
||||
- Complete sociale verbindingssysteem voor gebruikers
|
||||
- Real-time filtering van finds op basis van privacy instellingen
|
||||
- SHADCN componenten gebruikt voor consistente UI (Cards, Badges, Avatars, Dropdowns)
|
||||
- Svelte 5 patterns toegepast met $state, $derived, en $props runes
|
||||
- Bestaande friendship table schema optimaal benut zonder wijzigingen
|
||||
- Comprehensive authentication en authorization op alle endpoints
|
||||
- Mobile-responsive design met aangepaste styling voor kleinere schermen
|
||||
|
||||
---
|
||||
|
||||
### 14 Oktober 2025 (Maandag) - 8 uren
|
||||
|
||||
**Werk uitgevoerd:**
|
||||
@@ -238,8 +267,8 @@
|
||||
|
||||
## Totaal Overzicht
|
||||
|
||||
**Totale geschatte uren:** 55 uren
|
||||
**Werkdagen:** 9 dagen
|
||||
**Totale geschatte uren:** 61 uren
|
||||
**Werkdagen:** 10 dagen
|
||||
**Gemiddelde uren per dag:** 6.1 uur
|
||||
|
||||
### Project Milestones:
|
||||
@@ -253,6 +282,7 @@
|
||||
7. **10 Okt**: Finds feature en media upload systeem
|
||||
8. **13 Okt**: API architectuur verbetering
|
||||
9. **14 Okt**: Modern media support en social interactions
|
||||
10. **16 Okt**: Friends & Privacy System implementatie
|
||||
|
||||
### Hoofdfunctionaliteiten geïmplementeerd:
|
||||
|
||||
@@ -275,3 +305,7 @@
|
||||
- [x] WebP image processing met JPEG fallbacks
|
||||
- [x] Like/unlike systeem met real-time updates
|
||||
- [x] Social interactions en animated UI components
|
||||
- [x] Friends & Privacy System met vriendschapsverzoeken
|
||||
- [x] Privacy-bewuste find filtering met vriendenspecifieke zichtbaarheid
|
||||
- [x] Friends management pagina met gebruikerszoekfunctionaliteit
|
||||
- [x] Real-time find filtering op basis van privacy instellingen
|
||||
|
||||
Reference in New Issue
Block a user