mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-21 00:26:42 +02:00
style: auto-format codebase with Prettier
Applied formatting to 1487+ files using pnpm format:write - TypeScript/JavaScript files - Svelte components - Astro pages - JSON configs - Markdown docs 13 files still need manual review (Astro JSX comments)
This commit is contained in:
parent
0241f5554c
commit
d36b321d9d
3952 changed files with 661498 additions and 739751 deletions
|
|
@ -7,6 +7,7 @@ Diese Dokumentation beschreibt alle Optimierungen, die implementiert wurden, um
|
|||
**Datum:** Oktober 2025
|
||||
**Status:** ✅ Implementiert & Erweitert
|
||||
**Impact:**
|
||||
|
||||
- -60-80% schnellere Ladezeiten
|
||||
- -95% weniger DB Queries
|
||||
- -40-98% weniger Datenverbrauch
|
||||
|
|
@ -45,6 +46,7 @@ Diese Dokumentation beschreibt alle Optimierungen, die implementiert wurden, um
|
|||
### 1. expo-image Integration ⭐ Höchste Priorität
|
||||
|
||||
**Warum expo-image?**
|
||||
|
||||
- Built-in Memory + Disk Caching
|
||||
- Native Performance
|
||||
- Progressive Loading Support
|
||||
|
|
@ -58,23 +60,25 @@ Diese Dokumentation beschreibt alle Optimierungen, die implementiert wurden, um
|
|||
import { Image } from 'expo-image';
|
||||
|
||||
<Image
|
||||
source={{ uri: thumbnailUrl }}
|
||||
style={{ width: imageSize, height: imageSize }}
|
||||
contentFit="cover"
|
||||
transition={200}
|
||||
cachePolicy="memory-disk"
|
||||
placeholder={{ blurhash: 'L5H2EC=PM+yV0g-mq.wG9c010J}I' }}
|
||||
placeholderContentFit="cover"
|
||||
/>
|
||||
source={{ uri: thumbnailUrl }}
|
||||
style={{ width: imageSize, height: imageSize }}
|
||||
contentFit="cover"
|
||||
transition={200}
|
||||
cachePolicy="memory-disk"
|
||||
placeholder={{ blurhash: 'L5H2EC=PM+yV0g-mq.wG9c010J}I' }}
|
||||
placeholderContentFit="cover"
|
||||
/>;
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
|
||||
- ✅ Automatisches Memory + Disk Caching
|
||||
- ✅ 200ms Fade-in Transition
|
||||
- ✅ BlurHash Placeholder für sofortiges visuelles Feedback
|
||||
- ✅ Bessere Performance als RN Image
|
||||
|
||||
**Dateien geändert:**
|
||||
|
||||
- `components/ImageCard.tsx` (2 Instanzen)
|
||||
- `app/image/[id].tsx` (Detail Screen)
|
||||
|
||||
|
|
@ -83,70 +87,75 @@ import { Image } from 'expo-image';
|
|||
### 2. Supabase Query Optimierung ⭐⭐ Sehr wichtig
|
||||
|
||||
**Problem:**
|
||||
|
||||
```tsx
|
||||
// VORHER: 60+ Queries für 20 Bilder
|
||||
const enhancedImages = await Promise.all(imageData.map(async (img) => {
|
||||
const [_, likesData] = await Promise.all([
|
||||
fetchImageTags(img.id), // Query 1
|
||||
supabase.from('image_likes') // Query 2
|
||||
.select('*', { count: 'exact' })
|
||||
.eq('image_id', img.id)
|
||||
]);
|
||||
const enhancedImages = await Promise.all(
|
||||
imageData.map(async (img) => {
|
||||
const [_, likesData] = await Promise.all([
|
||||
fetchImageTags(img.id), // Query 1
|
||||
supabase
|
||||
.from('image_likes') // Query 2
|
||||
.select('*', { count: 'exact' })
|
||||
.eq('image_id', img.id),
|
||||
]);
|
||||
|
||||
const { data: userLike } = await supabase // Query 3
|
||||
.from('image_likes')
|
||||
.select('id')
|
||||
.eq('image_id', img.id)
|
||||
.eq('user_id', user.id)
|
||||
.single();
|
||||
// ... 3 Queries pro Bild!
|
||||
}));
|
||||
const { data: userLike } = await supabase // Query 3
|
||||
.from('image_likes')
|
||||
.select('id')
|
||||
.eq('image_id', img.id)
|
||||
.eq('user_id', user.id)
|
||||
.single();
|
||||
// ... 3 Queries pro Bild!
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
**Lösung: Batch Queries**
|
||||
|
||||
```tsx
|
||||
// NACHHER: Nur 3 Queries total!
|
||||
// 1. Batch fetch alle Tags parallel
|
||||
await Promise.all(imageData.map(img => fetchImageTags(img.id)));
|
||||
await Promise.all(imageData.map((img) => fetchImageTags(img.id)));
|
||||
|
||||
// 2. Alle Likes in EINER Query
|
||||
const imageIds = imageData.map(img => img.id);
|
||||
const imageIds = imageData.map((img) => img.id);
|
||||
const [likesCountData, userLikesData] = await Promise.all([
|
||||
supabase
|
||||
.from('image_likes')
|
||||
.select('image_id')
|
||||
.in('image_id', imageIds), // Alle auf einmal!
|
||||
supabase.from('image_likes').select('image_id').in('image_id', imageIds), // Alle auf einmal!
|
||||
|
||||
user ? supabase
|
||||
.from('image_likes')
|
||||
.select('image_id')
|
||||
.in('image_id', imageIds)
|
||||
.eq('user_id', user.id)
|
||||
: Promise.resolve({ data: [] })
|
||||
user
|
||||
? supabase
|
||||
.from('image_likes')
|
||||
.select('image_id')
|
||||
.in('image_id', imageIds)
|
||||
.eq('user_id', user.id)
|
||||
: Promise.resolve({ data: [] }),
|
||||
]);
|
||||
|
||||
// 3. Lookup Maps für O(1) Access
|
||||
const likesCountMap = new Map<string, number>();
|
||||
likesCountData.data?.forEach(like => {
|
||||
likesCountMap.set(like.image_id, (likesCountMap.get(like.image_id) || 0) + 1);
|
||||
likesCountData.data?.forEach((like) => {
|
||||
likesCountMap.set(like.image_id, (likesCountMap.get(like.image_id) || 0) + 1);
|
||||
});
|
||||
|
||||
const userLikesSet = new Set(userLikesData.data?.map(like => like.image_id) || []);
|
||||
const userLikesSet = new Set(userLikesData.data?.map((like) => like.image_id) || []);
|
||||
|
||||
// 4. Combine in O(n)
|
||||
const enhancedImages = imageData.map(img => ({
|
||||
...img,
|
||||
likes_count: likesCountMap.get(img.id) || 0,
|
||||
user_has_liked: userLikesSet.has(img.id)
|
||||
const enhancedImages = imageData.map((img) => ({
|
||||
...img,
|
||||
likes_count: likesCountMap.get(img.id) || 0,
|
||||
user_has_liked: userLikesSet.has(img.id),
|
||||
}));
|
||||
```
|
||||
|
||||
**Resultat:**
|
||||
|
||||
- **Vorher:** 60+ Queries
|
||||
- **Nachher:** 3 Queries
|
||||
- **Reduktion:** -95% 🔥
|
||||
|
||||
**Datei geändert:**
|
||||
|
||||
- `app/(tabs)/explore/index.tsx` (Lines 185-219)
|
||||
|
||||
---
|
||||
|
|
@ -155,12 +164,12 @@ const enhancedImages = imageData.map(img => ({
|
|||
|
||||
**Strategie:**
|
||||
|
||||
| View Mode | Größe | Auflösung | Dateigröße | Ersparnis |
|
||||
|-----------|-------|-----------|------------|-----------|
|
||||
| `grid5` | tiny | 100x100px | ~10 KB | -98% |
|
||||
| `grid3` | small | 200x200px | ~30 KB | -94% |
|
||||
| `single` | medium | 400x400px | ~80 KB | -84% |
|
||||
| Detail | full | Original | ~500 KB | 0% (volle Qualität) |
|
||||
| View Mode | Größe | Auflösung | Dateigröße | Ersparnis |
|
||||
| --------- | ------ | --------- | ---------- | ------------------- |
|
||||
| `grid5` | tiny | 100x100px | ~10 KB | -98% |
|
||||
| `grid3` | small | 200x200px | ~30 KB | -94% |
|
||||
| `single` | medium | 400x400px | ~80 KB | -84% |
|
||||
| Detail | full | Original | ~500 KB | 0% (volle Qualität) |
|
||||
|
||||
**Implementierung:**
|
||||
|
||||
|
|
@ -170,38 +179,39 @@ const enhancedImages = imageData.map(img => ({
|
|||
export type ThumbnailSize = 'tiny' | 'small' | 'medium' | 'full';
|
||||
|
||||
export function getThumbnailUrl(
|
||||
publicUrl: string | null,
|
||||
size: ThumbnailSize = 'medium'
|
||||
publicUrl: string | null,
|
||||
size: ThumbnailSize = 'medium'
|
||||
): string | null {
|
||||
if (!publicUrl) return null;
|
||||
if (!publicUrl) return null;
|
||||
|
||||
const dimensions: Record<ThumbnailSize, number> = {
|
||||
tiny: 100, // grid5
|
||||
small: 200, // grid3
|
||||
medium: 400, // single
|
||||
full: 0, // Original
|
||||
};
|
||||
const dimensions: Record<ThumbnailSize, number> = {
|
||||
tiny: 100, // grid5
|
||||
small: 200, // grid3
|
||||
medium: 400, // single
|
||||
full: 0, // Original
|
||||
};
|
||||
|
||||
const targetSize = dimensions[size];
|
||||
if (targetSize === 0) return publicUrl; // Full resolution
|
||||
const targetSize = dimensions[size];
|
||||
if (targetSize === 0) return publicUrl; // Full resolution
|
||||
|
||||
const url = new URL(publicUrl);
|
||||
url.searchParams.set('width', targetSize.toString());
|
||||
url.searchParams.set('height', targetSize.toString());
|
||||
url.searchParams.set('resize', 'cover');
|
||||
url.searchParams.set('quality', '80');
|
||||
const url = new URL(publicUrl);
|
||||
url.searchParams.set('width', targetSize.toString());
|
||||
url.searchParams.set('height', targetSize.toString());
|
||||
url.searchParams.set('resize', 'cover');
|
||||
url.searchParams.set('quality', '80');
|
||||
|
||||
return url.toString();
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export function getSizeForViewMode(
|
||||
viewMode: 'single' | 'grid3' | 'grid5'
|
||||
): ThumbnailSize {
|
||||
switch (viewMode) {
|
||||
case 'grid5': return 'tiny';
|
||||
case 'grid3': return 'small';
|
||||
case 'single': return 'medium';
|
||||
}
|
||||
export function getSizeForViewMode(viewMode: 'single' | 'grid3' | 'grid5'): ThumbnailSize {
|
||||
switch (viewMode) {
|
||||
case 'grid5':
|
||||
return 'tiny';
|
||||
case 'grid3':
|
||||
return 'small';
|
||||
case 'single':
|
||||
return 'medium';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -212,19 +222,21 @@ export function getSizeForViewMode(
|
|||
const thumbnailUrl = getThumbnailUrl(publicUrl, getSizeForViewMode(viewMode));
|
||||
|
||||
<Image
|
||||
source={{ uri: thumbnailUrl }}
|
||||
// ... rest of props
|
||||
/>
|
||||
source={{ uri: thumbnailUrl }}
|
||||
// ... rest of props
|
||||
/>;
|
||||
```
|
||||
|
||||
**Wie es funktioniert:**
|
||||
|
||||
Original URL:
|
||||
|
||||
```
|
||||
https://xxx.supabase.co/storage/v1/object/public/generated-images/image.webp
|
||||
```
|
||||
|
||||
Thumbnail URL (grid5):
|
||||
|
||||
```
|
||||
https://xxx.supabase.co/storage/v1/object/public/generated-images/image.webp
|
||||
?width=100
|
||||
|
|
@ -236,6 +248,7 @@ https://xxx.supabase.co/storage/v1/object/public/generated-images/image.webp
|
|||
Supabase generiert und cached diese Transformationen automatisch!
|
||||
|
||||
**Dateien:**
|
||||
|
||||
- `utils/image.ts` (neu erstellt)
|
||||
- `components/ImageCard.tsx` (nutzt Thumbnails)
|
||||
- `app/image/[id].tsx` (nutzt 'full' für Detail View)
|
||||
|
|
@ -249,18 +262,17 @@ Supabase generiert und cached diese Transformationen automatisch!
|
|||
```tsx
|
||||
// app/(tabs)/explore/index.tsx & app/(tabs)/index/index.tsx
|
||||
<FlatList
|
||||
data={filteredImages}
|
||||
renderItem={renderImage}
|
||||
keyExtractor={(item) => item.id}
|
||||
data={filteredImages}
|
||||
renderItem={renderImage}
|
||||
keyExtractor={(item) => item.id}
|
||||
// Performance Props:
|
||||
removeClippedSubviews={Platform.OS === 'android'} // Entfernt Views außerhalb Viewport
|
||||
maxToRenderPerBatch={10} // Weniger Items pro Render-Batch
|
||||
windowSize={5} // Kleineres Render-Fenster
|
||||
initialNumToRender={6} // Schnellerer Initial Load
|
||||
updateCellsBatchingPeriod={50} // Häufigere Updates
|
||||
|
||||
// Performance Props:
|
||||
removeClippedSubviews={Platform.OS === 'android'} // Entfernt Views außerhalb Viewport
|
||||
maxToRenderPerBatch={10} // Weniger Items pro Render-Batch
|
||||
windowSize={5} // Kleineres Render-Fenster
|
||||
initialNumToRender={6} // Schnellerer Initial Load
|
||||
updateCellsBatchingPeriod={50} // Häufigere Updates
|
||||
|
||||
// ... rest of props
|
||||
// ... rest of props
|
||||
/>
|
||||
```
|
||||
|
||||
|
|
@ -273,6 +285,7 @@ Supabase generiert und cached diese Transformationen automatisch!
|
|||
- **updateCellsBatchingPeriod**: Wie oft die Render-Queue geleert wird (ms)
|
||||
|
||||
**Dateien geändert:**
|
||||
|
||||
- `app/(tabs)/explore/index.tsx`
|
||||
- `app/(tabs)/index/index.tsx`
|
||||
|
||||
|
|
@ -282,29 +295,32 @@ Supabase generiert und cached diese Transformationen automatisch!
|
|||
|
||||
### Erwarteter Gewinn
|
||||
|
||||
| Metrik | Vorher | Nachher | Verbesserung |
|
||||
|--------|--------|---------|--------------|
|
||||
| **Initiales Laden** | ~3-4s | ~1-1.5s | **-60-70%** |
|
||||
| **DB Queries (Explore)** | 60+ | 3 | **-95%** |
|
||||
| **Scrolling FPS** | ~40 FPS | ~55-60 FPS | **+40-50%** |
|
||||
| **Cache Hits (2nd Load)** | 0% | 80%+ | **+80%** |
|
||||
| **Datenverbrauch (Grid5)** | ~10 MB | ~200 KB | **-98%** |
|
||||
| **Datenverbrauch (Grid3)** | ~10 MB | ~600 KB | **-94%** |
|
||||
| **Datenverbrauch (Single)** | ~10 MB | ~1.6 MB | **-84%** |
|
||||
| Metrik | Vorher | Nachher | Verbesserung |
|
||||
| --------------------------- | ------- | ---------- | ------------ |
|
||||
| **Initiales Laden** | ~3-4s | ~1-1.5s | **-60-70%** |
|
||||
| **DB Queries (Explore)** | 60+ | 3 | **-95%** |
|
||||
| **Scrolling FPS** | ~40 FPS | ~55-60 FPS | **+40-50%** |
|
||||
| **Cache Hits (2nd Load)** | 0% | 80%+ | **+80%** |
|
||||
| **Datenverbrauch (Grid5)** | ~10 MB | ~200 KB | **-98%** |
|
||||
| **Datenverbrauch (Grid3)** | ~10 MB | ~600 KB | **-94%** |
|
||||
| **Datenverbrauch (Single)** | ~10 MB | ~1.6 MB | **-84%** |
|
||||
|
||||
### Real-World Szenario: 20 Bilder laden
|
||||
|
||||
**Grid5 View:**
|
||||
|
||||
- Vorher: 20 × 500 KB = 10 MB
|
||||
- Nachher: 20 × 10 KB = 200 KB
|
||||
- **Ersparnis: 9.8 MB (-98%)**
|
||||
|
||||
**Grid3 View:**
|
||||
|
||||
- Vorher: 20 × 500 KB = 10 MB
|
||||
- Nachher: 20 × 30 KB = 600 KB
|
||||
- **Ersparnis: 9.4 MB (-94%)**
|
||||
|
||||
**Single View:**
|
||||
|
||||
- Vorher: 20 × 500 KB = 10 MB
|
||||
- Nachher: 20 × 80 KB = 1.6 MB
|
||||
- **Ersparnis: 8.4 MB (-84%)**
|
||||
|
|
@ -314,9 +330,11 @@ Supabase generiert und cached diese Transformationen automatisch!
|
|||
## Code-Änderungen Übersicht
|
||||
|
||||
### Neue Dateien
|
||||
|
||||
- ✨ `utils/image.ts` - Thumbnail URL Generation
|
||||
|
||||
### Geänderte Dateien
|
||||
|
||||
1. `package.json` - expo-image Package hinzugefügt
|
||||
2. `components/ImageCard.tsx` - expo-image + Thumbnail Support
|
||||
3. `app/(tabs)/explore/index.tsx` - Batch Queries + FlatList Props
|
||||
|
|
@ -324,9 +342,10 @@ Supabase generiert und cached diese Transformationen automatisch!
|
|||
5. `app/image/[id].tsx` - expo-image + Full Resolution
|
||||
|
||||
### Dependencies
|
||||
|
||||
```json
|
||||
{
|
||||
"expo-image": "~3.0.9"
|
||||
"expo-image": "~3.0.9"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -335,6 +354,7 @@ Supabase generiert und cached diese Transformationen automatisch!
|
|||
## Testing Checklist
|
||||
|
||||
### Funktionalität
|
||||
|
||||
- [ ] Bilder laden korrekt in allen View-Modes (single, grid3, grid5)
|
||||
- [ ] Thumbnails werden korrekt generiert
|
||||
- [ ] Detail-Screen zeigt volle Auflösung
|
||||
|
|
@ -342,12 +362,14 @@ Supabase generiert und cached diese Transformationen automatisch!
|
|||
- [ ] BlurHash Placeholder wird angezeigt
|
||||
|
||||
### Performance
|
||||
|
||||
- [ ] Initiales Laden ist spürbar schneller
|
||||
- [ ] Scrolling ist flüssiger (60 FPS)
|
||||
- [ ] Weniger Datenverbrauch (check Developer Tools)
|
||||
- [ ] Keine Memory Leaks
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- [ ] Bilder ohne public_url zeigen Placeholder
|
||||
- [ ] Offline-Modus zeigt gecachte Bilder
|
||||
- [ ] Wechsel zwischen View-Modes funktioniert
|
||||
|
|
@ -362,6 +384,7 @@ Supabase generiert und cached diese Transformationen automatisch!
|
|||
**Problem:** Alle Bilder hatten denselben generic BlurHash
|
||||
|
||||
**Lösung:**
|
||||
|
||||
- Neue DB Column `blurhash` in `images` Tabelle
|
||||
- BlurHash wird an ImageCard übergeben
|
||||
- Individueller Placeholder pro Bild
|
||||
|
|
@ -376,14 +399,15 @@ ALTER TABLE images ADD COLUMN IF NOT EXISTS blurhash TEXT;
|
|||
```tsx
|
||||
// ImageCard.tsx
|
||||
<Image
|
||||
source={{ uri: thumbnailUrl }}
|
||||
placeholder={{
|
||||
blurhash: blurhash || 'L5H2EC=PM+yV0g-mq.wG9c010J}I' // Fallback
|
||||
}}
|
||||
source={{ uri: thumbnailUrl }}
|
||||
placeholder={{
|
||||
blurhash: blurhash || 'L5H2EC=PM+yV0g-mq.wG9c010J}I', // Fallback
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
**Dateien:**
|
||||
|
||||
- Migration: `supabase/migrations/add_blurhash_to_images.sql`
|
||||
- Utility: `utils/blurhash.ts`
|
||||
- Updated: `components/ImageCard.tsx`, beide Screens
|
||||
|
|
@ -404,16 +428,17 @@ const thumbnailUrl = getThumbnailUrl(publicUrl, getSizeForViewMode(viewMode));
|
|||
const tinyThumbnailUrl = getThumbnailUrl(publicUrl, 'tiny'); // 100x100px
|
||||
|
||||
<Image
|
||||
source={{ uri: thumbnailUrl }}
|
||||
placeholder={
|
||||
tinyThumbnailUrl
|
||||
? { uri: tinyThumbnailUrl } // Progressive!
|
||||
: { blurhash: blurhash || DEFAULT_BLURHASH }
|
||||
}
|
||||
/>
|
||||
source={{ uri: thumbnailUrl }}
|
||||
placeholder={
|
||||
tinyThumbnailUrl
|
||||
? { uri: tinyThumbnailUrl } // Progressive!
|
||||
: { blurhash: blurhash || DEFAULT_BLURHASH }
|
||||
}
|
||||
/>;
|
||||
```
|
||||
|
||||
**Ablauf:**
|
||||
|
||||
1. BlurHash erscheint sofort (0ms)
|
||||
2. Tiny Thumbnail lädt (~50-100ms, ~2 KB)
|
||||
3. Richtiges Thumbnail lädt (~200-500ms, ~10-80 KB)
|
||||
|
|
@ -434,30 +459,31 @@ const tinyThumbnailUrl = getThumbnailUrl(publicUrl, 'tiny'); // 100x100px
|
|||
```tsx
|
||||
// app/(tabs)/index/index.tsx & explore/index.tsx
|
||||
useEffect(() => {
|
||||
if (!pagination.hasMore || pagination.loading) return;
|
||||
if (!pagination.hasMore || pagination.loading) return;
|
||||
|
||||
const prefetchNextPage = async () => {
|
||||
// Fetch IDs der nächsten Page
|
||||
const { data } = await supabase
|
||||
.from('images')
|
||||
.select('id, public_url')
|
||||
.range(nextPageStart, nextPageEnd);
|
||||
const prefetchNextPage = async () => {
|
||||
// Fetch IDs der nächsten Page
|
||||
const { data } = await supabase
|
||||
.from('images')
|
||||
.select('id, public_url')
|
||||
.range(nextPageStart, nextPageEnd);
|
||||
|
||||
// Prefetch Thumbnails
|
||||
data?.forEach(img => {
|
||||
const thumbnailUrl = getThumbnailUrl(img.public_url, thumbnailSize);
|
||||
if (thumbnailUrl) {
|
||||
Image.prefetch(thumbnailUrl);
|
||||
}
|
||||
});
|
||||
};
|
||||
// Prefetch Thumbnails
|
||||
data?.forEach((img) => {
|
||||
const thumbnailUrl = getThumbnailUrl(img.public_url, thumbnailSize);
|
||||
if (thumbnailUrl) {
|
||||
Image.prefetch(thumbnailUrl);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(prefetchNextPage, 500); // Debounced
|
||||
return () => clearTimeout(timeoutId);
|
||||
const timeoutId = setTimeout(prefetchNextPage, 500); // Debounced
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [pagination.page, viewMode]);
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Prefetcht erste 6 Bilder der nächsten Page
|
||||
- 500ms Debounce um excessive Requests zu vermeiden
|
||||
- Silent fail (nicht-kritisch)
|
||||
|
|
@ -478,32 +504,32 @@ useEffect(() => {
|
|||
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
|
||||
const pinchGesture = Gesture.Pinch()
|
||||
.onEnd((event) => {
|
||||
// Debounce: min 300ms zwischen Gesten
|
||||
if (now - lastGestureTime.current < 300) return;
|
||||
const pinchGesture = Gesture.Pinch().onEnd((event) => {
|
||||
// Debounce: min 300ms zwischen Gesten
|
||||
if (now - lastGestureTime.current < 300) return;
|
||||
|
||||
// Pinch-Out (scale > 1.15): Zoom in = größere Bilder
|
||||
if (event.scale > 1.15) {
|
||||
if (galleryViewMode === 'grid5') setGalleryViewMode('grid3');
|
||||
else if (galleryViewMode === 'grid3') setGalleryViewMode('single');
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
// Pinch-In (scale < 0.85): Zoom out = kleinere Bilder
|
||||
else if (event.scale < 0.85) {
|
||||
if (galleryViewMode === 'single') setGalleryViewMode('grid3');
|
||||
else if (galleryViewMode === 'grid3') setGalleryViewMode('grid5');
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
});
|
||||
// Pinch-Out (scale > 1.15): Zoom in = größere Bilder
|
||||
if (event.scale > 1.15) {
|
||||
if (galleryViewMode === 'grid5') setGalleryViewMode('grid3');
|
||||
else if (galleryViewMode === 'grid3') setGalleryViewMode('single');
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
// Pinch-In (scale < 0.85): Zoom out = kleinere Bilder
|
||||
else if (event.scale < 0.85) {
|
||||
if (galleryViewMode === 'single') setGalleryViewMode('grid3');
|
||||
else if (galleryViewMode === 'grid3') setGalleryViewMode('grid5');
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
});
|
||||
|
||||
// Wrap FlatList
|
||||
<GestureDetector gesture={pinchGesture}>
|
||||
<FlatList {...props} />
|
||||
</GestureDetector>
|
||||
<FlatList {...props} />
|
||||
</GestureDetector>;
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Pinch-Out: grid5 → grid3 → single (größere Bilder)
|
||||
- Pinch-In: single → grid3 → grid5 (kleinere Bilder)
|
||||
- Haptisches Feedback bei jedem Wechsel
|
||||
|
|
@ -511,6 +537,7 @@ const pinchGesture = Gesture.Pinch()
|
|||
- Threshold: >1.15 für Zoom-In, <0.85 für Zoom-Out
|
||||
|
||||
**Dateien:**
|
||||
|
||||
- `app/(tabs)/index/index.tsx`
|
||||
|
||||
**Impact:** Natürliche iOS Photos-ähnliche UX, schneller View-Wechsel ohne Button-Klick
|
||||
|
|
@ -520,19 +547,23 @@ const pinchGesture = Gesture.Pinch()
|
|||
## Nächste mögliche Optimierungen
|
||||
|
||||
### 1. BlurHash Generation beim Upload (Server-Side)
|
||||
|
||||
- BlurHash automatisch bei Edge Function generieren
|
||||
- Direkt in DB speichern
|
||||
- Aktuell: Manuell/Client-Side
|
||||
|
||||
### 3. Progressive JPEG/WebP
|
||||
|
||||
- Bilder in progressivem Format hochladen
|
||||
- Besseres Ladeverhalten
|
||||
|
||||
### 4. Image CDN
|
||||
|
||||
- CloudFlare Images oder imgix für zusätzliche Optimierung
|
||||
- Automatische Format-Konvertierung (WebP, AVIF)
|
||||
|
||||
### 5. Lazy Loading für Tags/Likes
|
||||
|
||||
- Tags/Likes nur on-demand laden
|
||||
- Reduziert initiale Query-Komplexität weiter
|
||||
|
||||
|
|
@ -545,6 +576,7 @@ const pinchGesture = Gesture.Pinch()
|
|||
Supabase nutzt imgproxy unter der Haube:
|
||||
|
||||
**Unterstützte Parameter:**
|
||||
|
||||
- `width` - Zielbreite
|
||||
- `height` - Zielhöhe
|
||||
- `resize` - Resize-Mode (cover, contain, fill)
|
||||
|
|
@ -552,27 +584,32 @@ Supabase nutzt imgproxy unter der Haube:
|
|||
- `format` - Output-Format (webp, jpg, png)
|
||||
|
||||
**Caching:**
|
||||
|
||||
- Erste Transformation: ~500ms
|
||||
- Weitere Requests: ~50ms (cached)
|
||||
- Cache-Duration: 1 Jahr
|
||||
|
||||
**Limits:**
|
||||
|
||||
- Max Size: 2500x2500px
|
||||
- Max File Size: 5MB
|
||||
|
||||
### expo-image Caching
|
||||
|
||||
**Memory Cache:**
|
||||
|
||||
- LRU (Least Recently Used) Policy
|
||||
- Größe: ~50-100 Bilder
|
||||
- Lebensdauer: Bis App-Neustart
|
||||
|
||||
**Disk Cache:**
|
||||
|
||||
- Location: `FileSystem.cacheDirectory`
|
||||
- Größe: Unbegrenzt (aber OS kann löschen)
|
||||
- Lebensdauer: Persistent
|
||||
|
||||
**Cache Management:**
|
||||
|
||||
```tsx
|
||||
// Cache manuell leeren
|
||||
import { Image } from 'expo-image';
|
||||
|
|
@ -586,23 +623,27 @@ await Image.clearDiskCache();
|
|||
## Troubleshooting
|
||||
|
||||
### Bilder laden nicht
|
||||
|
||||
1. Check Supabase Storage Permissions
|
||||
2. Verify public_url ist korrekt
|
||||
3. Check Network Tab für Fehler
|
||||
4. Cache leeren und neu versuchen
|
||||
|
||||
### Thumbnails falsche Größe
|
||||
|
||||
1. Verify URL Parameter sind korrekt
|
||||
2. Check Supabase Storage Transformations Settings
|
||||
3. Test mit direkter URL im Browser
|
||||
|
||||
### Performance nicht besser
|
||||
|
||||
1. Enable React Native Performance Monitor
|
||||
2. Check FlatList Props sind gesetzt
|
||||
3. Verify expo-image ist installiert
|
||||
4. Profile mit React DevTools
|
||||
|
||||
### Cache funktioniert nicht
|
||||
|
||||
1. Check `cachePolicy="memory-disk"`
|
||||
2. Verify URLs sind stabil (keine Query-Params ändern)
|
||||
3. Clear Cache und neu testen
|
||||
|
|
|
|||
|
|
@ -7,30 +7,36 @@ Plan für ein geteiltes UI-Komponenten-System über 10+ Apps hinweg. Ziel ist es
|
|||
## Strategie: CLI-Tool (shadcn-style) mit optionalem Tailwind-Preset
|
||||
|
||||
### Phase 1: CLI-Tool für Component Copy-Paste (Start hier)
|
||||
|
||||
**Zeitaufwand:** 1-2 Tage
|
||||
|
||||
Wir starten mit einem simplen Ansatz:
|
||||
|
||||
- Zentrales Git-Repo mit UI-Components
|
||||
- CLI-Tool das Components in Apps kopiert
|
||||
- Components gehören dann der App (volle Kontrolle)
|
||||
- Keine NPM-Dependencies für Components
|
||||
|
||||
**Vorteile dieser Reihenfolge:**
|
||||
|
||||
- Schneller Start, kein Over-Engineering
|
||||
- Wir lernen welche Design-Patterns sich wirklich wiederholen
|
||||
- Kein zweites System am Anfang
|
||||
- CLI-Tool validiert ob der Ansatz überhaupt funktioniert
|
||||
|
||||
### Phase 2: Tailwind-Preset (optional, später)
|
||||
|
||||
**Zeitaufwand:** 2-3 Stunden
|
||||
**Wann:** Nach 1-3 Monaten, wenn wir sehen was sich wiederholt
|
||||
|
||||
Falls wir merken dass bestimmte Design-Tokens (Farben, Spacing, etc.) überall gleich sind:
|
||||
|
||||
- Extrahieren in ein kleines Tailwind-Config NPM package
|
||||
- Components im Library-Repo updaten zu nutzen das Preset
|
||||
- Bestehende Apps können updaten (optional)
|
||||
|
||||
**Migration ist einfach:**
|
||||
|
||||
- Preset Package erstellen
|
||||
- Components refactoren: `bg-[#3B82F6]` → `bg-brand-primary`
|
||||
- Apps installieren Preset und re-adden Components
|
||||
|
|
@ -44,6 +50,7 @@ Falls wir merken dass bestimmte Design-Tokens (Farben, Spacing, etc.) überall g
|
|||
**Repository:** `github.com/memoro/ui` (Monorepo)
|
||||
|
||||
**Repository Struktur:**
|
||||
|
||||
```
|
||||
memoro-ui/
|
||||
├── packages/
|
||||
|
|
@ -90,42 +97,43 @@ memoro-ui/
|
|||
```
|
||||
|
||||
**registry.json Beispiel:**
|
||||
|
||||
```json
|
||||
{
|
||||
"components": {
|
||||
"ui": {
|
||||
"button": {
|
||||
"name": "Button",
|
||||
"files": ["Button.tsx"],
|
||||
"category": "ui",
|
||||
"dependencies": [],
|
||||
"description": "A pressable button component with variants"
|
||||
},
|
||||
"input": {
|
||||
"name": "Input",
|
||||
"files": ["Input.tsx"],
|
||||
"category": "ui",
|
||||
"dependencies": [],
|
||||
"description": "Text input with label and error states"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"header": {
|
||||
"name": "Header",
|
||||
"files": ["Header.tsx"],
|
||||
"category": "navigation",
|
||||
"dependencies": [],
|
||||
"description": "App header with title and optional actions"
|
||||
},
|
||||
"tab-bar": {
|
||||
"name": "TabBar",
|
||||
"files": ["TabBar.tsx"],
|
||||
"category": "navigation",
|
||||
"dependencies": [],
|
||||
"description": "Bottom tab bar navigation component"
|
||||
}
|
||||
}
|
||||
}
|
||||
"components": {
|
||||
"ui": {
|
||||
"button": {
|
||||
"name": "Button",
|
||||
"files": ["Button.tsx"],
|
||||
"category": "ui",
|
||||
"dependencies": [],
|
||||
"description": "A pressable button component with variants"
|
||||
},
|
||||
"input": {
|
||||
"name": "Input",
|
||||
"files": ["Input.tsx"],
|
||||
"category": "ui",
|
||||
"dependencies": [],
|
||||
"description": "Text input with label and error states"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"header": {
|
||||
"name": "Header",
|
||||
"files": ["Header.tsx"],
|
||||
"category": "navigation",
|
||||
"dependencies": [],
|
||||
"description": "App header with title and optional actions"
|
||||
},
|
||||
"tab-bar": {
|
||||
"name": "TabBar",
|
||||
"files": ["TabBar.tsx"],
|
||||
"category": "navigation",
|
||||
"dependencies": [],
|
||||
"description": "Bottom tab bar navigation component"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -134,26 +142,31 @@ memoro-ui/
|
|||
**Commands:**
|
||||
|
||||
**`npx @memoro/ui add <component>`**
|
||||
|
||||
- Kopiert Component-Code in `app/components/ui/`
|
||||
- Prüft ob Component bereits existiert
|
||||
- Fragt bei Konflikten nach (überschreiben/skip)
|
||||
- Zeigt Success-Message mit Import-Beispiel
|
||||
|
||||
**`npx @memoro/ui list`**
|
||||
|
||||
- Zeigt alle verfügbaren Components
|
||||
- Zeigt welche bereits in der App sind
|
||||
- Zeigt Beschreibung jedes Components
|
||||
|
||||
**`npx @memoro/ui update <component>`**
|
||||
|
||||
- Updated einen existierenden Component
|
||||
- Zeigt Diff der Änderungen
|
||||
- Fragt nach Bestätigung
|
||||
|
||||
**`npx @memoro/ui diff <component>`**
|
||||
|
||||
- Zeigt Unterschiede zwischen lokaler und Library-Version
|
||||
- Hilfreich um zu sehen ob lokale Anpassungen gemacht wurden
|
||||
|
||||
**Optional später:**
|
||||
|
||||
- `init` - Erstellt `components/ui/` Ordner
|
||||
- `remove` - Entfernt Component aus App
|
||||
- `sync` - Updated alle Components auf einmal
|
||||
|
|
@ -161,6 +174,7 @@ memoro-ui/
|
|||
### 3. Component Development Workflow
|
||||
|
||||
**Neuer Component:**
|
||||
|
||||
1. Entwickle Component in `ui-components/components/`
|
||||
2. Teste in Preview-App
|
||||
3. Schreibe README mit Usage-Beispielen
|
||||
|
|
@ -168,12 +182,14 @@ memoro-ui/
|
|||
5. Commit & Push
|
||||
|
||||
**Component nutzen:**
|
||||
|
||||
1. In App: `npx @memoro/ui add button`
|
||||
2. Component liegt jetzt in `app/components/ui/Button.tsx`
|
||||
3. Importieren: `import { Button } from '@/components/ui/Button'`
|
||||
4. Bei Bedarf app-spezifisch anpassen
|
||||
|
||||
**Component updaten:**
|
||||
|
||||
1. Änderungen in Library-Repo
|
||||
2. Apps können entscheiden ob sie updaten wollen
|
||||
3. `npx @memoro/ui update button` in jeweiliger App
|
||||
|
|
@ -184,27 +200,32 @@ memoro-ui/
|
|||
**Jeder Component sollte haben:**
|
||||
|
||||
**Consistent API:**
|
||||
|
||||
- Props sind konsistent benannt über alle Components
|
||||
- `variant`, `size`, `disabled` patterns
|
||||
- `className` für custom Tailwind classes
|
||||
- `children` wo sinnvoll
|
||||
|
||||
**TypeScript:**
|
||||
|
||||
- Vollständige Type definitions
|
||||
- Exported Types für Props
|
||||
- Generic Support wo nötig
|
||||
|
||||
**Accessibility:**
|
||||
|
||||
- ARIA labels wo nötig
|
||||
- Keyboard navigation
|
||||
- Screen reader support
|
||||
|
||||
**Styling:**
|
||||
|
||||
- NativeWind/Tailwind classes
|
||||
- Responsive by default
|
||||
- Dark mode ready (später)
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- README.md mit:
|
||||
- Beschreibung
|
||||
- Props table
|
||||
|
|
@ -214,12 +235,14 @@ memoro-ui/
|
|||
### 5. Preview/Development App
|
||||
|
||||
**Expo App in ui-components/preview:**
|
||||
|
||||
- Live preview aller Components
|
||||
- Test auf echten Devices
|
||||
- QR-Code für schnelles Testen
|
||||
- Optional: Storybook integration
|
||||
|
||||
**Zweck:**
|
||||
|
||||
- Entwickle Components in Isolation
|
||||
- Visual testing
|
||||
- Dokumentation als Live-Demo
|
||||
|
|
@ -232,12 +255,14 @@ memoro-ui/
|
|||
**UI Components (`packages/components/ui/`):**
|
||||
|
||||
**Layout:**
|
||||
|
||||
- Container
|
||||
- Stack (VStack/HStack)
|
||||
- Spacer
|
||||
- Divider
|
||||
|
||||
**Input:**
|
||||
|
||||
- Button
|
||||
- Input (TextInput)
|
||||
- Checkbox
|
||||
|
|
@ -245,6 +270,7 @@ memoro-ui/
|
|||
- Slider
|
||||
|
||||
**Display:**
|
||||
|
||||
- Text (mit Typography variants)
|
||||
- Card
|
||||
- Badge
|
||||
|
|
@ -252,17 +278,20 @@ memoro-ui/
|
|||
- Image (mit Loading states)
|
||||
|
||||
**Feedback:**
|
||||
|
||||
- Alert
|
||||
- Toast
|
||||
- Spinner/Loading
|
||||
- Progress
|
||||
|
||||
**Overlay:**
|
||||
|
||||
- Modal
|
||||
- Sheet (Bottom sheet)
|
||||
- Dropdown
|
||||
|
||||
**Navigation Components (`packages/components/navigation/`):**
|
||||
|
||||
- Header (mit Title, Back Button, Actions)
|
||||
- TabBar (Bottom Tab Navigation)
|
||||
- BackButton
|
||||
|
|
@ -272,25 +301,30 @@ memoro-ui/
|
|||
### 7. Naming Conventions
|
||||
|
||||
**Component Files:**
|
||||
|
||||
- PascalCase: `Button.tsx`, `TextInput.tsx`
|
||||
- Co-located files: `Button.stories.tsx`, `Button.test.tsx`
|
||||
|
||||
**Registry IDs:**
|
||||
|
||||
- kebab-case: `button`, `text-input`
|
||||
- Matches CLI usage: `npx ui add text-input`
|
||||
|
||||
**Variants:**
|
||||
|
||||
- lowercase: `primary`, `secondary`, `outline`
|
||||
- Sizes: `sm`, `md`, `lg`, `xl`
|
||||
|
||||
### 8. Version Strategy (später relevant)
|
||||
|
||||
**Phase 1 (jetzt):**
|
||||
|
||||
- Keine Versionierung nötig
|
||||
- Components werden kopiert = keine Breaking changes
|
||||
- Apps besitzen den Code
|
||||
|
||||
**Phase 2 (wenn nötig):**
|
||||
|
||||
- Semantic versioning für CLI-Tool
|
||||
- Component changelog in README
|
||||
- Breaking changes werden dokumentiert
|
||||
|
|
@ -299,11 +333,13 @@ memoro-ui/
|
|||
### 9. Testing Strategy
|
||||
|
||||
**CLI-Tool:**
|
||||
|
||||
- Unit tests für file operations
|
||||
- Integration tests für add/update commands
|
||||
- Test mit dummy Expo app
|
||||
|
||||
**Components:**
|
||||
|
||||
- Visual testing in Preview app
|
||||
- Optional: Jest + React Native Testing Library
|
||||
- Manual testing auf iOS/Android
|
||||
|
|
@ -333,6 +369,7 @@ memoro-ui/
|
|||
- Wenn wiederverwendbar: zu Library hinzufügen
|
||||
|
||||
**Für neue Apps:**
|
||||
|
||||
- Start projekt
|
||||
- Setup `.npmrc` mit `@memoro:registry=https://npm.pkg.github.com`
|
||||
- `npx @memoro/ui init` (erstellt structure)
|
||||
|
|
@ -342,18 +379,21 @@ memoro-ui/
|
|||
### 11. Documentation
|
||||
|
||||
**README in Library Repo:**
|
||||
|
||||
- Was ist das System
|
||||
- Wie installiert man CLI
|
||||
- Quick start guide
|
||||
- Component overview mit Links
|
||||
|
||||
**Per-Component README:**
|
||||
|
||||
- Props documentation
|
||||
- Usage examples
|
||||
- Variants showcase
|
||||
- Do's and Don'ts
|
||||
|
||||
**Changelog:**
|
||||
|
||||
- Tracked in Library repo
|
||||
- Breaking changes highlighted
|
||||
- Migration guides wenn nötig
|
||||
|
|
@ -361,6 +401,7 @@ memoro-ui/
|
|||
### 12. Future Enhancements (Phase 2+)
|
||||
|
||||
**Wenn Tailwind-Preset hinzukommt:**
|
||||
|
||||
- Mini NPM package: `@memoro/tailwind-preset`
|
||||
- Ebenfalls via GitHub Packages publiziert
|
||||
- Components nutzen Design tokens
|
||||
|
|
@ -368,6 +409,7 @@ memoro-ui/
|
|||
- Migration guide für bestehende Components
|
||||
|
||||
**Weitere Features:**
|
||||
|
||||
- Theming system (Light/Dark mode)
|
||||
- Animation presets
|
||||
- Icon set integration
|
||||
|
|
@ -375,6 +417,7 @@ memoro-ui/
|
|||
- Data fetching patterns (optional)
|
||||
|
||||
**Tooling:**
|
||||
|
||||
- VSCode snippets für Components
|
||||
- GitHub Actions für automated testing
|
||||
- Automated screenshot testing
|
||||
|
|
@ -385,17 +428,20 @@ memoro-ui/
|
|||
## Success Metrics
|
||||
|
||||
**Phase 1 (CLI-Tool):**
|
||||
|
||||
- ✅ 10+ wiederverwendbare Components
|
||||
- ✅ CLI-Tool funktioniert in allen Apps
|
||||
- ✅ Mindestens 2 Apps nutzen das System
|
||||
- ✅ Zeit für neue App-Features: -30%
|
||||
|
||||
**Phase 2 (Tailwind-Preset):**
|
||||
|
||||
- ✅ Design tokens extrahiert
|
||||
- ✅ Konsistente Farben/Spacing über alle Apps
|
||||
- ✅ Design updates in <1 Tag für alle Apps
|
||||
|
||||
**Overall:**
|
||||
|
||||
- ✅ Neue App in <1 Tag bootstrap-bar
|
||||
- ✅ UI consistency über alle Apps
|
||||
- ✅ Component reuse rate >60%
|
||||
|
|
@ -406,27 +452,32 @@ memoro-ui/
|
|||
## Timeline
|
||||
|
||||
**Week 1-2: Setup**
|
||||
|
||||
- UI-Components Repo erstellen
|
||||
- CLI-Tool Grundstruktur
|
||||
- Registry system
|
||||
- Preview app setup
|
||||
|
||||
**Week 3-4: Core Components**
|
||||
|
||||
- 5 wichtigste Components entwickeln
|
||||
- Testing in Preview app
|
||||
- Documentation schreiben
|
||||
|
||||
**Week 5: First Migration**
|
||||
|
||||
- "picture" App als Test
|
||||
- 2-3 Components migrieren
|
||||
- Learnings dokumentieren
|
||||
|
||||
**Week 6+: Iteration**
|
||||
|
||||
- Mehr Components hinzufügen
|
||||
- Weitere Apps migrieren
|
||||
- CLI verbessern basierend auf Feedback
|
||||
|
||||
**Month 2-3: Optional Tailwind-Preset**
|
||||
|
||||
- Nur wenn es sich als nötig erweist
|
||||
- Design tokens extrahieren
|
||||
- Components refactoren
|
||||
|
|
@ -437,17 +488,21 @@ memoro-ui/
|
|||
## Decisions Made
|
||||
|
||||
### 1. Package Naming ✅
|
||||
|
||||
**Entscheidung:** `@memoro/ui`
|
||||
|
||||
**Reasoning:**
|
||||
|
||||
- Klarer, einprägsamer Name
|
||||
- Namespace `@memoro` für alle zukünftigen Packages
|
||||
- Konsistent für späteres `@memoro/tailwind-preset`
|
||||
|
||||
### 2. Registry ✅
|
||||
|
||||
**Entscheidung:** GitHub Packages
|
||||
|
||||
**Reasoning:**
|
||||
|
||||
- ✅ Kostenlos für private Repos
|
||||
- ✅ Bereits in GitHub - keine extra Infrastruktur
|
||||
- ✅ Einfache CI/CD Integration mit GitHub Actions
|
||||
|
|
@ -455,18 +510,20 @@ memoro-ui/
|
|||
- ✅ Kann später zu Private NPM migriert werden wenn nötig
|
||||
|
||||
**Setup Details:**
|
||||
|
||||
```json
|
||||
// package.json im CLI-Tool
|
||||
{
|
||||
"name": "@memoro/ui",
|
||||
"repository": "https://github.com/[username]/memoro-ui",
|
||||
"publishConfig": {
|
||||
"registry": "https://npm.pkg.github.com"
|
||||
}
|
||||
"name": "@memoro/ui",
|
||||
"repository": "https://github.com/[username]/memoro-ui",
|
||||
"publishConfig": {
|
||||
"registry": "https://npm.pkg.github.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in Apps:**
|
||||
|
||||
```bash
|
||||
# .npmrc in jeder App
|
||||
@memoro:registry=https://npm.pkg.github.com
|
||||
|
|
@ -480,10 +537,12 @@ npx @memoro/ui add button
|
|||
```
|
||||
|
||||
**GitHub Personal Access Token (PAT) benötigt mit:**
|
||||
|
||||
- `read:packages` - Um Packages zu installieren
|
||||
- `write:packages` - Um zu publizieren (nur für Maintainer)
|
||||
|
||||
**CI/CD Setup (GitHub Actions):**
|
||||
|
||||
```yaml
|
||||
- name: Setup NPM for GitHub Packages
|
||||
run: |
|
||||
|
|
@ -494,59 +553,72 @@ npx @memoro/ui add button
|
|||
---
|
||||
|
||||
### 3. Component Scope ✅
|
||||
|
||||
**Entscheidung:** UI-Components + Navigation Components
|
||||
|
||||
**Included:**
|
||||
|
||||
- ✅ UI-Components (Button, Input, Card, Badge, Avatar, etc.)
|
||||
- ✅ Navigation Components (Header, TabBar, BackButton, etc.)
|
||||
|
||||
**Excluded (für jetzt):**
|
||||
|
||||
- ❌ Form validation helpers
|
||||
- ❌ Data display (Lists, Tables, Pagination)
|
||||
- ❌ Complex business logic components
|
||||
|
||||
**Reasoning:**
|
||||
|
||||
- Navigation components sind essentiell für jede App
|
||||
- Wiederholen sich über alle Apps hinweg
|
||||
- Bleiben UI-fokussiert ohne Business-Logik
|
||||
- Können später erweitert werden wenn Bedarf entsteht
|
||||
|
||||
### 4. Testing Strategy ✅
|
||||
|
||||
**Entscheidung:** Manual Testing in Phase 1
|
||||
|
||||
**Phase 1:**
|
||||
|
||||
- Manual testing in Preview App
|
||||
- Visual verification auf iOS/Android
|
||||
- Component usage testing in real apps
|
||||
|
||||
**Phase 2 (später):**
|
||||
|
||||
- Automated tests wenn Library >5 Components hat
|
||||
- Jest + React Native Testing Library
|
||||
- Visual regression testing (optional)
|
||||
|
||||
**Reasoning:**
|
||||
|
||||
- Schneller Start ohne Testing-Overhead
|
||||
- Preview App bietet gute visuelle Kontrolle
|
||||
- Automated tests später wenn Library stabiler ist
|
||||
|
||||
### 5. Preview App ✅
|
||||
|
||||
**Entscheidung:** Lokale Expo App
|
||||
|
||||
**Setup:**
|
||||
|
||||
- Expo App im Monorepo unter `packages/preview/`
|
||||
- Dev Client für native Features
|
||||
- Hot reload während Component-Development
|
||||
|
||||
**Reasoning:**
|
||||
|
||||
- ✅ Mehr Kontrolle als Expo Snack
|
||||
- ✅ Native Features testbar (z.B. Haptics, Gestures)
|
||||
- ✅ Läuft im gleichen Repo - einfaches Development
|
||||
- ✅ Kann mit Components in `packages/components/` direkt arbeiten
|
||||
|
||||
### 6. Repository Structure ✅
|
||||
|
||||
**Entscheidung:** Monorepo mit pnpm workspaces
|
||||
|
||||
**Structure:**
|
||||
|
||||
```
|
||||
memoro-ui/
|
||||
├── packages/
|
||||
|
|
@ -569,12 +641,14 @@ memoro-ui/
|
|||
```
|
||||
|
||||
**pnpm-workspace.yaml:**
|
||||
|
||||
```yaml
|
||||
packages:
|
||||
- 'packages/*'
|
||||
```
|
||||
|
||||
**Reasoning:**
|
||||
|
||||
- ✅ Alles in einem Repo - einfacher zu entwickeln
|
||||
- ✅ Shared dependencies zwischen Packages
|
||||
- ✅ pnpm = schneller & effizienter als npm/yarn
|
||||
|
|
@ -582,31 +656,36 @@ packages:
|
|||
- ✅ CLI kann direkt auf Components zugreifen
|
||||
|
||||
### 7. GitHub Organization ✅
|
||||
|
||||
**Entscheidung:** GitHub Organization `@memoro`
|
||||
|
||||
**Setup:**
|
||||
|
||||
- Neue GitHub Org: `memoro` (oder `memoro-ui`)
|
||||
- Repo: `github.com/memoro/ui` (oder ähnlich)
|
||||
- Package: `@memoro/ui`
|
||||
|
||||
**Package Configuration:**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@memoro/ui",
|
||||
"repository": "https://github.com/memoro/ui",
|
||||
"publishConfig": {
|
||||
"registry": "https://npm.pkg.github.com"
|
||||
}
|
||||
"name": "@memoro/ui",
|
||||
"repository": "https://github.com/memoro/ui",
|
||||
"publishConfig": {
|
||||
"registry": "https://npm.pkg.github.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Reasoning:**
|
||||
|
||||
- ✅ Professioneller Auftritt
|
||||
- ✅ Namespace für zukünftige Packages (`@memoro/tailwind-preset`)
|
||||
- ✅ Einfacher Team-Management später
|
||||
- ✅ Klare Trennung von Personal Projects
|
||||
|
||||
**GitHub Org Setup:**
|
||||
|
||||
1. Erstelle neue Org: https://github.com/organizations/plan
|
||||
2. Invite Members (wenn Team)
|
||||
3. Setup Package Permissions
|
||||
|
|
@ -619,6 +698,7 @@ packages:
|
|||
**Alle Haupt-Entscheidungen getroffen! ✅**
|
||||
|
||||
Optionale Entscheidungen für später:
|
||||
|
||||
- **Icon System:** Eigenes Icon-Set oder bestehende Library? (@expo/vector-icons, react-native-heroicons)
|
||||
- **Animation Library:** Reanimated, Moti, oder custom?
|
||||
- **TypeScript Strictness:** Wie streng? (strict mode, exactOptionalPropertyTypes, etc.)
|
||||
|
|
@ -628,6 +708,7 @@ Optionale Entscheidungen für später:
|
|||
## Next Steps
|
||||
|
||||
### Phase 1: Repository Setup
|
||||
|
||||
1. ✅ Plan dokumentiert
|
||||
2. ✅ Alle Entscheidungen getroffen
|
||||
3. ⏳ GitHub Organization `memoro` erstellen
|
||||
|
|
@ -638,18 +719,21 @@ Optionale Entscheidungen für später:
|
|||
- registry.json erstellen
|
||||
|
||||
### Phase 2: Preview App Setup
|
||||
|
||||
6. ⏳ Expo App in `packages/preview/` aufsetzen
|
||||
- Expo Router konfigurieren
|
||||
- NativeWind/Tailwind einrichten
|
||||
- Tabs für UI & Navigation Components
|
||||
|
||||
### Phase 3: CLI-Tool Prototyp
|
||||
|
||||
7. ⏳ CLI-Tool Grundstruktur bauen
|
||||
- TypeScript setup
|
||||
- Commands: add, list, diff, update
|
||||
- GitHub Packages publish konfigurieren
|
||||
|
||||
### Phase 4: Erste Components
|
||||
|
||||
8. ⏳ Ersten UI Component entwickeln (Button)
|
||||
- Component code in `packages/components/ui/Button/`
|
||||
- README schreiben
|
||||
|
|
@ -662,6 +746,7 @@ Optionale Entscheidungen für später:
|
|||
- registry.json eintragen
|
||||
|
||||
### Phase 5: Testing in Real App
|
||||
|
||||
10. ⏳ CLI publishen zu GitHub Packages
|
||||
11. ⏳ In "picture" App testen
|
||||
- `.npmrc` setup
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
## Tech Stack Unabhängigkeit
|
||||
|
||||
### **SvelteKit** ✅ Unabhängiger
|
||||
|
||||
- **Compiler-basiert** - kompiliert zu Vanilla JS
|
||||
- Keine Runtime Framework (React, Vue, etc.)
|
||||
- Kleinere Abhängigkeiten
|
||||
|
|
@ -19,6 +20,7 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
- Zukunftssicherer durch Web Standards
|
||||
|
||||
### **Next.js** ⚠️ React-Ökosystem
|
||||
|
||||
- Fest an React gebunden
|
||||
- Braucht React Ökosystem (React Query, etc.)
|
||||
- Größere Bundle Sizes
|
||||
|
|
@ -29,6 +31,7 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
## Performance
|
||||
|
||||
### **SvelteKit** 🚀
|
||||
|
||||
- **Extrem schnell** - kein Virtual DOM
|
||||
- Kleinere Bundles (20-30% weniger)
|
||||
- Schnelleres First Paint
|
||||
|
|
@ -36,6 +39,7 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
- Beispiel: 50KB vs 150KB initial
|
||||
|
||||
### **Next.js** 👍
|
||||
|
||||
- Gut, aber schwerer
|
||||
- Virtual DOM Overhead
|
||||
- Hydration kann langsam sein
|
||||
|
|
@ -48,6 +52,7 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
### **SvelteKit**
|
||||
|
||||
**Vorteile:**
|
||||
|
||||
- **Weniger Boilerplate** - 30-40% weniger Code
|
||||
- Intuitivere Syntax
|
||||
- Eingebaute Animationen/Transitions
|
||||
|
|
@ -55,17 +60,19 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
- Server Load Functions elegant
|
||||
|
||||
**Beispiel:**
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = 0; // Kein useState!
|
||||
let count = 0; // Kein useState!
|
||||
</script>
|
||||
|
||||
<button on:click={() => count++}>
|
||||
{count}
|
||||
{count}
|
||||
</button>
|
||||
```
|
||||
|
||||
**Nachteile:**
|
||||
|
||||
- Kleinere Community
|
||||
- Weniger StackOverflow Antworten
|
||||
- Weniger UI Libraries
|
||||
|
|
@ -73,6 +80,7 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
### **Next.js**
|
||||
|
||||
**Vorteile:**
|
||||
|
||||
- **Riesige Community** - jedes Problem schon gelöst
|
||||
- Tonnen von Libraries
|
||||
- Mehr Devs verfügbar (Hiring)
|
||||
|
|
@ -80,6 +88,7 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
- Besserer Support
|
||||
|
||||
**Nachteile:**
|
||||
|
||||
- Mehr Boilerplate
|
||||
- Komplexer (App Router vs Pages Router)
|
||||
- Hooks-Lernkurve
|
||||
|
|
@ -90,6 +99,7 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
## Supabase Integration
|
||||
|
||||
### **Beide gleich gut** ✅
|
||||
|
||||
- Supabase JS Client funktioniert überall
|
||||
- SSR Auth beide gut
|
||||
- Beide haben offizielle Guides
|
||||
|
|
@ -97,10 +107,12 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
### **Unterschiede:**
|
||||
|
||||
**SvelteKit:**
|
||||
|
||||
- Hooks in `+page.server.ts` natürlicher
|
||||
- Load Functions cleaner
|
||||
|
||||
**Next.js:**
|
||||
|
||||
- Mehr Beispiele online
|
||||
- Mehr Tutorials verfügbar
|
||||
|
||||
|
|
@ -109,6 +121,7 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
## Routing & SSR
|
||||
|
||||
### **SvelteKit** 💚
|
||||
|
||||
- **File-based Routing** - `+page.svelte`
|
||||
- Einfacher als Next.js
|
||||
- Layouts intuitiver
|
||||
|
|
@ -116,6 +129,7 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
- Weniger Magic
|
||||
|
||||
### **Next.js** 💛
|
||||
|
||||
- File-based Routing - aber komplizierter
|
||||
- App Router vs Pages Router Verwirrung
|
||||
- Mehr Konzepte (RSC, Server Actions)
|
||||
|
|
@ -128,6 +142,7 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
### **Next.js** ✅ Größer
|
||||
|
||||
**UI Libraries:**
|
||||
|
||||
- Shadcn/ui (top!)
|
||||
- Material UI
|
||||
- Chakra UI
|
||||
|
|
@ -136,6 +151,7 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
- Tausende mehr
|
||||
|
||||
**Sonstiges:**
|
||||
|
||||
- Jede Library hat React Support
|
||||
- Auth: NextAuth perfekt integriert
|
||||
- Payments: Stripe Beispiele überall
|
||||
|
|
@ -143,6 +159,7 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
### **SvelteKit** ⚠️ Kleiner, wachsend
|
||||
|
||||
**UI Libraries:**
|
||||
|
||||
- Skeleton UI
|
||||
- DaisyUI (Tailwind-based)
|
||||
- Carbon Components
|
||||
|
|
@ -150,6 +167,7 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
- Weniger Auswahl
|
||||
|
||||
**Aber:**
|
||||
|
||||
- Kann CSS Frameworks nutzen (Tailwind, UnoCSS)
|
||||
- Viele Web Components nutzbar
|
||||
|
||||
|
|
@ -158,6 +176,7 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
## Image Handling (kritisch für Picture App!)
|
||||
|
||||
### **Next.js** ✅ Exzellent
|
||||
|
||||
- `next/image` Component eingebaut
|
||||
- Automatische Optimierung
|
||||
- WebP/AVIF Konvertierung
|
||||
|
|
@ -166,6 +185,7 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
- **Produktionsreif out of the box**
|
||||
|
||||
### **SvelteKit** ⚠️ Braucht Setup
|
||||
|
||||
- Kein eingebautes Image Optimization
|
||||
- Manuell mit Vite Plugins (vite-imagetools)
|
||||
- Oder externe Services (Cloudinary, imgix)
|
||||
|
|
@ -185,10 +205,12 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
### **Unterschiede:**
|
||||
|
||||
**Next.js:**
|
||||
|
||||
- Optimiert für Vercel
|
||||
- Einige Features nur auf Vercel
|
||||
|
||||
**SvelteKit:**
|
||||
|
||||
- Adapter-System flexibler
|
||||
- Läuft überall gleich gut
|
||||
|
||||
|
|
@ -197,12 +219,14 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
## Code Sharing mit React Native
|
||||
|
||||
### **Next.js** ✅ Einfacher
|
||||
|
||||
- Beide nutzen React
|
||||
- Components **teilweise** portierbar
|
||||
- Gleiche Patterns (Hooks)
|
||||
- Logic besser teilbar
|
||||
|
||||
### **SvelteKit** ⚠️ Schwieriger
|
||||
|
||||
- Komplett andere Syntax
|
||||
- Nur Business Logic teilbar
|
||||
- UI muss komplett neu
|
||||
|
|
@ -212,11 +236,13 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
## Hiring & Team
|
||||
|
||||
### **Next.js** ✅
|
||||
|
||||
- Jeder React Dev kann Next.js
|
||||
- Größerer Talent Pool
|
||||
- Einfacher zu ersetzen
|
||||
|
||||
### **SvelteKit** ⚠️
|
||||
|
||||
- Kleinere Developer Base
|
||||
- Schwieriger zu finden
|
||||
- Aber: React Devs lernen es schnell
|
||||
|
|
@ -226,12 +252,14 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
## Long-term Maintenance
|
||||
|
||||
### **SvelteKit** ✅ Stabiler
|
||||
|
||||
- Weniger Breaking Changes
|
||||
- Klare Roadmap
|
||||
- Web Standards fokussiert
|
||||
- Weniger Refactoring nötig
|
||||
|
||||
### **Next.js** ⚠️ Schnelle Evolution
|
||||
|
||||
- App Router große Änderung (2023)
|
||||
- React Server Components komplex
|
||||
- Viel Churn
|
||||
|
|
@ -241,24 +269,25 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
|
||||
## Feature-Matrix für Picture App
|
||||
|
||||
| Feature | Next.js | SvelteKit | Gewinner |
|
||||
|---------|---------|-----------|----------|
|
||||
| Image Optimization | ✅ Exzellent | ⚠️ Manuell | Next.js |
|
||||
| Performance | 👍 Gut | 🚀 Besser | SvelteKit |
|
||||
| Supabase Integration | ✅ Gut | ✅ Gut | Unentschieden |
|
||||
| Auth | ✅ NextAuth | ✅ Hooks | Unentschieden |
|
||||
| Animations | 👍 Libraries | ✅ Native | SvelteKit |
|
||||
| SEO | ✅ Gut | ✅ Gut | Unentschieden |
|
||||
| Community Support | ✅ Riesig | ⚠️ Klein | Next.js |
|
||||
| Bundle Size | ⚠️ Größer | ✅ Kleiner | SvelteKit |
|
||||
| Code Sharing RN | ✅ React | ❌ Neu | Next.js |
|
||||
| Developer Experience | 👍 Gut | ✅ Besser | SvelteKit |
|
||||
| Feature | Next.js | SvelteKit | Gewinner |
|
||||
| -------------------- | ------------ | ---------- | ------------- |
|
||||
| Image Optimization | ✅ Exzellent | ⚠️ Manuell | Next.js |
|
||||
| Performance | 👍 Gut | 🚀 Besser | SvelteKit |
|
||||
| Supabase Integration | ✅ Gut | ✅ Gut | Unentschieden |
|
||||
| Auth | ✅ NextAuth | ✅ Hooks | Unentschieden |
|
||||
| Animations | 👍 Libraries | ✅ Native | SvelteKit |
|
||||
| SEO | ✅ Gut | ✅ Gut | Unentschieden |
|
||||
| Community Support | ✅ Riesig | ⚠️ Klein | Next.js |
|
||||
| Bundle Size | ⚠️ Größer | ✅ Kleiner | SvelteKit |
|
||||
| Code Sharing RN | ✅ React | ❌ Neu | Next.js |
|
||||
| Developer Experience | 👍 Gut | ✅ Besser | SvelteKit |
|
||||
|
||||
---
|
||||
|
||||
## Entscheidungsmatrix
|
||||
|
||||
### **Wähle SvelteKit wenn:**
|
||||
|
||||
- ✅ Maximale Unabhängigkeit wichtig
|
||||
- ✅ Performance kritisch
|
||||
- ✅ Bereit für Image Optimization Setup
|
||||
|
|
@ -266,6 +295,7 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
- ✅ Kleine, fokussierte Community okay
|
||||
|
||||
### **Wähle Next.js wenn:**
|
||||
|
||||
- ✅ Schnelle Time-to-Market wichtig
|
||||
- ✅ Image Optimization out-of-the-box benötigt
|
||||
- ✅ React-Synergien mit Mobile gewünscht
|
||||
|
|
@ -302,6 +332,7 @@ Für eine Bilder-App mit gleichwertigen Mobile (React Native) und Web Anforderun
|
|||
```
|
||||
|
||||
**Regeln:**
|
||||
|
||||
1. ❌ **Keine Next.js spezifischen Features** außer Image und Routing
|
||||
2. ✅ **Business Logic in `/shared`** auslagern
|
||||
3. ✅ **Vercel-unabhängig deployen** (z.B. Cloudflare, Netlify)
|
||||
|
|
@ -329,6 +360,7 @@ Phase 3: Optional - Migration zu SvelteKit wenn Next.js nervt
|
|||
**Warum NICHT Expo Web?**
|
||||
|
||||
Die App nutzt viele native-only Features:
|
||||
|
||||
- `react-native-worklets` (JSI/Native)
|
||||
- `react-native-reanimated` (Native Animations)
|
||||
- `react-native-pager-view` (Native Views)
|
||||
|
|
@ -336,6 +368,7 @@ Die App nutzt viele native-only Features:
|
|||
- Gesten, Zoom, Blur...
|
||||
|
||||
**Probleme:**
|
||||
|
||||
- 2-5 Tage Debugging für Mocks
|
||||
- Ständige Workarounds
|
||||
- Limitierte Features
|
||||
|
|
@ -359,15 +392,18 @@ Die App nutzt viele native-only Features:
|
|||
## Ressourcen
|
||||
|
||||
### Next.js
|
||||
|
||||
- [Next.js Docs](https://nextjs.org/docs)
|
||||
- [Next.js + Supabase](https://supabase.com/docs/guides/getting-started/tutorials/with-nextjs)
|
||||
- [Next.js Image Optimization](https://nextjs.org/docs/app/building-your-application/optimizing/images)
|
||||
|
||||
### SvelteKit (für Zukunft)
|
||||
|
||||
- [SvelteKit Docs](https://kit.svelte.dev/docs)
|
||||
- [SvelteKit + Supabase](https://supabase.com/docs/guides/getting-started/tutorials/with-sveltekit)
|
||||
|
||||
### Monorepo Setup
|
||||
|
||||
- [Turborepo](https://turbo.build/repo/docs)
|
||||
- [pnpm Workspaces](https://pnpm.io/workspaces)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue