mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 22:46:41 +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
|
|
@ -22,12 +22,12 @@ Beispiel für eine Übersetzungsdatei:
|
|||
|
||||
```json
|
||||
{
|
||||
"common": {
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen",
|
||||
// weitere Übersetzungen...
|
||||
},
|
||||
// weitere Kategorien...
|
||||
"common": {
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen"
|
||||
// weitere Übersetzungen...
|
||||
}
|
||||
// weitere Kategorien...
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -36,6 +36,7 @@ Beispiel für eine Übersetzungsdatei:
|
|||
Stelle sicher, dass die Struktur der neuen Übersetzungsdatei exakt der Struktur der vorhandenen Dateien entspricht. Alle Schlüssel müssen vorhanden sein, um Fehler zu vermeiden.
|
||||
|
||||
Die Hauptkategorien sind:
|
||||
|
||||
- `common`: Allgemeine Begriffe
|
||||
- `auth`: Authentifizierungsbezogene Texte
|
||||
- `home`: Texte für die Startseite
|
||||
|
|
@ -66,8 +67,8 @@ Füge die neue Sprache zur `LANGUAGES`-Konstante hinzu:
|
|||
|
||||
```typescript
|
||||
export const LANGUAGES = {
|
||||
// bestehende Sprachen...
|
||||
xx: { nativeName: 'Sprachname', emoji: '🇽🇽' }, // Sprachname in der Originalsprache und entsprechendes Flaggen-Emoji
|
||||
// bestehende Sprachen...
|
||||
xx: { nativeName: 'Sprachname', emoji: '🇽🇽' }, // Sprachname in der Originalsprache und entsprechendes Flaggen-Emoji
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -78,15 +79,13 @@ Verwende für das Emoji die entsprechende Länderflagge im Unicode-Format (z.B.
|
|||
Füge die neue Sprache zu den Ressourcen in der i18n-Initialisierung hinzu:
|
||||
|
||||
```typescript
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
// bestehende Sprachen...
|
||||
xx: { translation: xx },
|
||||
},
|
||||
// weitere Konfigurationen...
|
||||
});
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
// bestehende Sprachen...
|
||||
xx: { translation: xx },
|
||||
},
|
||||
// weitere Konfigurationen...
|
||||
});
|
||||
```
|
||||
|
||||
## 3. Testen der neuen Sprache
|
||||
|
|
@ -98,6 +97,7 @@ Starte die App und navigiere zu den Einstellungen. Die neue Sprache sollte in de
|
|||
### Schritt 3.2: Testen der Übersetzungen
|
||||
|
||||
Wähle die neue Sprache aus und überprüfe, ob alle Texte korrekt übersetzt werden. Achte besonders auf:
|
||||
|
||||
- Formatierungen (z.B. bei Datumsangaben)
|
||||
- Pluralformen
|
||||
- Spezielle Zeichen
|
||||
|
|
@ -146,25 +146,23 @@ Stelle sicher, dass die Blueprint-Anzeige die Inhalte in der neuen Sprache korre
|
|||
import es from './translations/es.json';
|
||||
|
||||
export const LANGUAGES = {
|
||||
de: { nativeName: 'Deutsch', emoji: '🇩🇪' },
|
||||
en: { nativeName: 'English', emoji: '🇬🇧' },
|
||||
it: { nativeName: 'Italiano', emoji: '🇮🇹' },
|
||||
fr: { nativeName: 'Français', emoji: '🇫🇷' },
|
||||
es: { nativeName: 'Español', emoji: '🇪🇸' },
|
||||
de: { nativeName: 'Deutsch', emoji: '🇩🇪' },
|
||||
en: { nativeName: 'English', emoji: '🇬🇧' },
|
||||
it: { nativeName: 'Italiano', emoji: '🇮🇹' },
|
||||
fr: { nativeName: 'Français', emoji: '🇫🇷' },
|
||||
es: { nativeName: 'Español', emoji: '🇪🇸' },
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
de: { translation: de },
|
||||
en: { translation: en },
|
||||
it: { translation: it },
|
||||
fr: { translation: fr },
|
||||
es: { translation: es },
|
||||
},
|
||||
// weitere Konfigurationen...
|
||||
});
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
de: { translation: de },
|
||||
en: { translation: en },
|
||||
it: { translation: it },
|
||||
fr: { translation: fr },
|
||||
es: { translation: es },
|
||||
},
|
||||
// weitere Konfigurationen...
|
||||
});
|
||||
```
|
||||
|
||||
### 7.2 Beispiel: Hinzufügen von Griechisch
|
||||
|
|
@ -176,19 +174,17 @@ i18n
|
|||
import el from './translations/el.json';
|
||||
|
||||
export const LANGUAGES = {
|
||||
// bestehende Sprachen...
|
||||
el: { nativeName: 'Ελληνικά', emoji: '🇬🇷' },
|
||||
// bestehende Sprachen...
|
||||
el: { nativeName: 'Ελληνικά', emoji: '🇬🇷' },
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
// bestehende Sprachen...
|
||||
el: { translation: el },
|
||||
},
|
||||
// weitere Konfigurationen...
|
||||
});
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
// bestehende Sprachen...
|
||||
el: { translation: el },
|
||||
},
|
||||
// weitere Konfigurationen...
|
||||
});
|
||||
```
|
||||
|
||||
### 7.3 Beispiel: Hinzufügen von Lettisch
|
||||
|
|
@ -200,19 +196,17 @@ i18n
|
|||
import lv from './translations/lv.json';
|
||||
|
||||
export const LANGUAGES = {
|
||||
// bestehende Sprachen...
|
||||
lv: { nativeName: 'Latviešu', emoji: '🇱🇻' },
|
||||
// bestehende Sprachen...
|
||||
lv: { nativeName: 'Latviešu', emoji: '🇱🇻' },
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
// bestehende Sprachen...
|
||||
lv: { translation: lv },
|
||||
},
|
||||
// weitere Konfigurationen...
|
||||
});
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
// bestehende Sprachen...
|
||||
lv: { translation: lv },
|
||||
},
|
||||
// weitere Konfigurationen...
|
||||
});
|
||||
```
|
||||
|
||||
### 7.4 Beispiel: Hinzufügen von Litauisch
|
||||
|
|
@ -224,19 +218,17 @@ i18n
|
|||
import lt from './translations/lt.json';
|
||||
|
||||
export const LANGUAGES = {
|
||||
// bestehende Sprachen...
|
||||
lt: { nativeName: 'Lietuvių', emoji: '🇱🇹' },
|
||||
// bestehende Sprachen...
|
||||
lt: { nativeName: 'Lietuvių', emoji: '🇱🇹' },
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
// bestehende Sprachen...
|
||||
lt: { translation: lt },
|
||||
},
|
||||
// weitere Konfigurationen...
|
||||
});
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
// bestehende Sprachen...
|
||||
lt: { translation: lt },
|
||||
},
|
||||
// weitere Konfigurationen...
|
||||
});
|
||||
```
|
||||
|
||||
### 7.5 Beispiel: Hinzufügen von Slowakisch
|
||||
|
|
@ -248,19 +240,17 @@ i18n
|
|||
import sk from './translations/sk.json';
|
||||
|
||||
export const LANGUAGES = {
|
||||
// bestehende Sprachen...
|
||||
sk: { nativeName: 'Slovenčina', emoji: '🇸🇰' },
|
||||
// bestehende Sprachen...
|
||||
sk: { nativeName: 'Slovenčina', emoji: '🇸🇰' },
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
// bestehende Sprachen...
|
||||
sk: { translation: sk },
|
||||
},
|
||||
// weitere Konfigurationen...
|
||||
});
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
// bestehende Sprachen...
|
||||
sk: { translation: sk },
|
||||
},
|
||||
// weitere Konfigurationen...
|
||||
});
|
||||
```
|
||||
|
||||
### 7.6 Beispiel: Hinzufügen von Slowenisch
|
||||
|
|
@ -272,17 +262,15 @@ i18n
|
|||
import sl from './translations/sl.json';
|
||||
|
||||
export const LANGUAGES = {
|
||||
// bestehende Sprachen...
|
||||
sl: { nativeName: 'Slovenščina', emoji: '🇸🇮' },
|
||||
// bestehende Sprachen...
|
||||
sl: { nativeName: 'Slovenščina', emoji: '🇸🇮' },
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
// bestehende Sprachen...
|
||||
sl: { translation: sl },
|
||||
},
|
||||
// weitere Konfigurationen...
|
||||
});
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
// bestehende Sprachen...
|
||||
sl: { translation: sl },
|
||||
},
|
||||
// weitere Konfigurationen...
|
||||
});
|
||||
```
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ This comprehensive guide provides migration patterns for transitioning from Reac
|
|||
## Framework Philosophy
|
||||
|
||||
### React (Runtime Framework)
|
||||
|
||||
- Uses Virtual DOM for reconciliation
|
||||
- Runtime reactivity through hooks (useState, useEffect, useMemo)
|
||||
- Component-based architecture with JSX
|
||||
|
|
@ -33,6 +34,7 @@ This comprehensive guide provides migration patterns for transitioning from Reac
|
|||
- Requires additional libraries for routing, forms, etc.
|
||||
|
||||
### SvelteKit (Compiler Framework)
|
||||
|
||||
- Compiles components to optimized JavaScript at build time
|
||||
- No Virtual DOM - direct DOM manipulation
|
||||
- Reactivity through runes (compile-time primitives)
|
||||
|
|
@ -48,6 +50,7 @@ This comprehensive guide provides migration patterns for transitioning from Reac
|
|||
### Basic Component Structure
|
||||
|
||||
#### React (Functional Component)
|
||||
|
||||
```jsx
|
||||
// UserProfile.jsx
|
||||
import React, { useState } from 'react';
|
||||
|
|
@ -74,6 +77,7 @@ export default UserProfile;
|
|||
```
|
||||
|
||||
#### SvelteKit (Svelte 5 with Runes)
|
||||
|
||||
```svelte
|
||||
<!-- UserProfile.svelte -->
|
||||
<script>
|
||||
|
|
@ -106,15 +110,15 @@ export default UserProfile;
|
|||
|
||||
### Key Differences
|
||||
|
||||
| Aspect | React | Svelte/SvelteKit |
|
||||
|--------|-------|------------------|
|
||||
| **File Extension** | `.jsx` or `.tsx` | `.svelte` |
|
||||
| Aspect | React | Svelte/SvelteKit |
|
||||
| ------------------- | ---------------------------- | ----------------------------------- |
|
||||
| **File Extension** | `.jsx` or `.tsx` | `.svelte` |
|
||||
| **Template Syntax** | JSX (JavaScript expressions) | HTML-like with `{}` for expressions |
|
||||
| **Props** | Function parameters | `$props()` rune with destructuring |
|
||||
| **State** | `useState` hook | `$state()` rune |
|
||||
| **Styles** | CSS-in-JS or external | Scoped `<style>` block |
|
||||
| **Class Names** | `className` | `class` |
|
||||
| **Imports** | Explicit React import | No framework import needed |
|
||||
| **Props** | Function parameters | `$props()` rune with destructuring |
|
||||
| **State** | `useState` hook | `$state()` rune |
|
||||
| **Styles** | CSS-in-JS or external | Scoped `<style>` block |
|
||||
| **Class Names** | `className` | `class` |
|
||||
| **Imports** | Explicit React import | No framework import needed |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -125,6 +129,7 @@ export default UserProfile;
|
|||
#### 1. State Management
|
||||
|
||||
**React:**
|
||||
|
||||
```jsx
|
||||
const [count, setCount] = useState(0);
|
||||
const [user, setUser] = useState({ name: 'John', age: 30 });
|
||||
|
|
@ -135,6 +140,7 @@ setUser({ ...user, age: 31 }); // Immutable update
|
|||
```
|
||||
|
||||
**Svelte:**
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
|
@ -149,6 +155,7 @@ setUser({ ...user, age: 31 }); // Immutable update
|
|||
#### 2. Computed/Derived Values
|
||||
|
||||
**React:**
|
||||
|
||||
```jsx
|
||||
// useMemo for expensive computations
|
||||
const total = useMemo(() => {
|
||||
|
|
@ -160,6 +167,7 @@ const doubled = count * 2;
|
|||
```
|
||||
|
||||
**Svelte:**
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let items = $state([...]);
|
||||
|
|
@ -177,6 +185,7 @@ const doubled = count * 2;
|
|||
#### 3. Side Effects
|
||||
|
||||
**React:**
|
||||
|
||||
```jsx
|
||||
useEffect(() => {
|
||||
console.log(`Count changed to ${count}`);
|
||||
|
|
@ -189,6 +198,7 @@ useEffect(() => {
|
|||
```
|
||||
|
||||
**Svelte:**
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
$effect(() => {
|
||||
|
|
@ -204,17 +214,17 @@ useEffect(() => {
|
|||
|
||||
### Reactivity Comparison Table
|
||||
|
||||
| Feature | React | Svelte 5 |
|
||||
|---------|-------|----------|
|
||||
| **State** | `useState(0)` | `$state(0)` |
|
||||
| **Computed** | `useMemo()` | `$derived()` |
|
||||
| **Effects** | `useEffect()` | `$effect()` |
|
||||
| **Refs** | `useRef()` | `$state()` (or direct variable) |
|
||||
| **Callback** | `useCallback()` | Not needed (functions are stable) |
|
||||
| **Context** | `useContext()` | Svelte context API or stores |
|
||||
| **Dependency Tracking** | Manual (dependency array) | Automatic (runtime tracking) |
|
||||
| **Mutation** | Immutable updates only | Direct mutation works |
|
||||
| **Deep Reactivity** | No (requires immutable patterns) | Yes (automatic) |
|
||||
| Feature | React | Svelte 5 |
|
||||
| ----------------------- | -------------------------------- | --------------------------------- |
|
||||
| **State** | `useState(0)` | `$state(0)` |
|
||||
| **Computed** | `useMemo()` | `$derived()` |
|
||||
| **Effects** | `useEffect()` | `$effect()` |
|
||||
| **Refs** | `useRef()` | `$state()` (or direct variable) |
|
||||
| **Callback** | `useCallback()` | Not needed (functions are stable) |
|
||||
| **Context** | `useContext()` | Svelte context API or stores |
|
||||
| **Dependency Tracking** | Manual (dependency array) | Automatic (runtime tracking) |
|
||||
| **Mutation** | Immutable updates only | Direct mutation works |
|
||||
| **Deep Reactivity** | No (requires immutable patterns) | Yes (automatic) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -223,6 +233,7 @@ useEffect(() => {
|
|||
### React Router → SvelteKit File-Based Routing
|
||||
|
||||
#### React Router Setup
|
||||
|
||||
```jsx
|
||||
// App.jsx
|
||||
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
|
||||
|
|
@ -253,6 +264,7 @@ function App() {
|
|||
#### SvelteKit File-Based Routing
|
||||
|
||||
**Directory Structure:**
|
||||
|
||||
```
|
||||
src/routes/
|
||||
├── +page.svelte # Home page (/)
|
||||
|
|
@ -273,6 +285,7 @@ src/routes/
|
|||
```
|
||||
|
||||
**Root Layout:**
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<nav>
|
||||
|
|
@ -286,6 +299,7 @@ src/routes/
|
|||
```
|
||||
|
||||
**Dynamic Route:**
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/blog/[slug]/+page.svelte -->
|
||||
<script>
|
||||
|
|
@ -301,16 +315,16 @@ src/routes/
|
|||
|
||||
### Routing Comparison Table
|
||||
|
||||
| Feature | React Router | SvelteKit |
|
||||
|---------|--------------|-----------|
|
||||
| **Route Definition** | JSX `<Route>` components | File system structure |
|
||||
| **Dynamic Routes** | `:param` syntax | `[param]` folder name |
|
||||
| **Navigation** | `<Link>` component | Native `<a>` tags |
|
||||
| **Nested Routes** | `<Outlet>` component | Nested `<slot>` in layouts |
|
||||
| **Layout Wrapping** | Manual wrapper components | `+layout.svelte` files |
|
||||
| **Programmatic Nav** | `useNavigate()` hook | `goto()` from `$app/navigation` |
|
||||
| **Route Guards** | Custom components/HOCs | Load functions with redirects |
|
||||
| **Not Found** | `<Route path="*">` | `+error.svelte` file |
|
||||
| Feature | React Router | SvelteKit |
|
||||
| -------------------- | ------------------------- | ------------------------------- |
|
||||
| **Route Definition** | JSX `<Route>` components | File system structure |
|
||||
| **Dynamic Routes** | `:param` syntax | `[param]` folder name |
|
||||
| **Navigation** | `<Link>` component | Native `<a>` tags |
|
||||
| **Nested Routes** | `<Outlet>` component | Nested `<slot>` in layouts |
|
||||
| **Layout Wrapping** | Manual wrapper components | `+layout.svelte` files |
|
||||
| **Programmatic Nav** | `useNavigate()` hook | `goto()` from `$app/navigation` |
|
||||
| **Route Guards** | Custom components/HOCs | Load functions with redirects |
|
||||
| **Not Found** | `<Route path="*">` | `+error.svelte` file |
|
||||
|
||||
### Migration Strategy
|
||||
|
||||
|
|
@ -328,6 +342,7 @@ src/routes/
|
|||
### Redux/Context API → Svelte Stores
|
||||
|
||||
#### React with Redux
|
||||
|
||||
```jsx
|
||||
// store.js
|
||||
import { createStore } from 'redux';
|
||||
|
|
@ -351,18 +366,15 @@ export const store = createStore(reducer);
|
|||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
function Counter() {
|
||||
const count = useSelector(state => state.count);
|
||||
const count = useSelector((state) => state.count);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
<button onClick={() => dispatch({ type: 'INCREMENT' })}>
|
||||
Count: {count}
|
||||
</button>
|
||||
);
|
||||
return <button onClick={() => dispatch({ type: 'INCREMENT' })}>Count: {count}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
#### React Context API
|
||||
|
||||
```jsx
|
||||
// UserContext.jsx
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
|
|
@ -372,11 +384,7 @@ const UserContext = createContext();
|
|||
export function UserProvider({ children }) {
|
||||
const [user, setUser] = useState(null);
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={{ user, setUser }}>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
);
|
||||
return <UserContext.Provider value={{ user, setUser }}>{children}</UserContext.Provider>;
|
||||
}
|
||||
|
||||
export const useUser = () => useContext(UserContext);
|
||||
|
|
@ -391,6 +399,7 @@ function Profile() {
|
|||
#### Svelte Stores
|
||||
|
||||
**Writable Store:**
|
||||
|
||||
```javascript
|
||||
// stores.js
|
||||
import { writable } from 'svelte/store';
|
||||
|
|
@ -400,6 +409,7 @@ export const user = writable(null);
|
|||
```
|
||||
|
||||
**Component Usage:**
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { count, user } from './stores.js';
|
||||
|
|
@ -420,6 +430,7 @@ export const user = writable(null);
|
|||
```
|
||||
|
||||
**Custom Store (Redux-like):**
|
||||
|
||||
```javascript
|
||||
// counterStore.js
|
||||
import { writable } from 'svelte/store';
|
||||
|
|
@ -429,9 +440,9 @@ function createCounter() {
|
|||
|
||||
return {
|
||||
subscribe,
|
||||
increment: () => update(n => n + 1),
|
||||
decrement: () => update(n => n - 1),
|
||||
reset: () => set(0)
|
||||
increment: () => update((n) => n + 1),
|
||||
decrement: () => update((n) => n - 1),
|
||||
reset: () => set(0),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -439,6 +450,7 @@ export const counter = createCounter();
|
|||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { counter } from './counterStore.js';
|
||||
|
|
@ -451,27 +463,27 @@ export const counter = createCounter();
|
|||
```
|
||||
|
||||
**Derived Store:**
|
||||
|
||||
```javascript
|
||||
// stores.js
|
||||
import { writable, derived } from 'svelte/store';
|
||||
|
||||
export const items = writable([
|
||||
{ id: 1, name: 'Item 1', price: 10 },
|
||||
{ id: 2, name: 'Item 2', price: 20 }
|
||||
{ id: 2, name: 'Item 2', price: 20 },
|
||||
]);
|
||||
|
||||
// Computed value from store(s)
|
||||
export const total = derived(items, $items =>
|
||||
$items.reduce((sum, item) => sum + item.price, 0)
|
||||
);
|
||||
export const total = derived(items, ($items) => $items.reduce((sum, item) => sum + item.price, 0));
|
||||
```
|
||||
|
||||
**Readable Store (for external subscriptions):**
|
||||
|
||||
```javascript
|
||||
// timeStore.js
|
||||
import { readable } from 'svelte/store';
|
||||
|
||||
export const time = readable(new Date(), set => {
|
||||
export const time = readable(new Date(), (set) => {
|
||||
const interval = setInterval(() => {
|
||||
set(new Date());
|
||||
}, 1000);
|
||||
|
|
@ -483,16 +495,16 @@ export const time = readable(new Date(), set => {
|
|||
|
||||
### State Management Comparison
|
||||
|
||||
| Feature | Redux | Context API | Svelte Stores |
|
||||
|---------|-------|-------------|---------------|
|
||||
| **Setup Complexity** | High | Medium | Low |
|
||||
| **Boilerplate** | High | Medium | Low |
|
||||
| **Provider Required** | Yes | Yes | No |
|
||||
| **Auto-subscription** | No | No | Yes (with `$`) |
|
||||
| **Derived State** | Selectors | Manual | `derived()` |
|
||||
| **DevTools** | Yes | Limited | Extension available |
|
||||
| **Server-Side** | Complex | Complex | Simple (`.server.js` suffix) |
|
||||
| **Performance** | Good (with optimization) | Can cause re-renders | Excellent (fine-grained) |
|
||||
| Feature | Redux | Context API | Svelte Stores |
|
||||
| --------------------- | ------------------------ | -------------------- | ---------------------------- |
|
||||
| **Setup Complexity** | High | Medium | Low |
|
||||
| **Boilerplate** | High | Medium | Low |
|
||||
| **Provider Required** | Yes | Yes | No |
|
||||
| **Auto-subscription** | No | No | Yes (with `$`) |
|
||||
| **Derived State** | Selectors | Manual | `derived()` |
|
||||
| **DevTools** | Yes | Limited | Extension available |
|
||||
| **Server-Side** | Complex | Complex | Simple (`.server.js` suffix) |
|
||||
| **Performance** | Good (with optimization) | Can cause re-renders | Excellent (fine-grained) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -501,6 +513,7 @@ export const time = readable(new Date(), set => {
|
|||
### useEffect + fetch → Load Functions
|
||||
|
||||
#### React Data Fetching
|
||||
|
||||
```jsx
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
|
|
@ -542,6 +555,7 @@ function BlogPost({ slug }) {
|
|||
#### SvelteKit Load Functions
|
||||
|
||||
**Server-side Load Function:**
|
||||
|
||||
```javascript
|
||||
// src/routes/blog/[slug]/+page.server.js
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
|
@ -559,7 +573,7 @@ export async function load({ params, fetch }) {
|
|||
const post = await response.json();
|
||||
|
||||
return {
|
||||
post
|
||||
post,
|
||||
};
|
||||
} catch (err) {
|
||||
throw error(500, 'Failed to load post');
|
||||
|
|
@ -568,6 +582,7 @@ export async function load({ params, fetch }) {
|
|||
```
|
||||
|
||||
**Component:**
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/blog/[slug]/+page.svelte -->
|
||||
<script>
|
||||
|
|
@ -583,6 +598,7 @@ export async function load({ params, fetch }) {
|
|||
```
|
||||
|
||||
**Universal Load Function (runs on both server and client):**
|
||||
|
||||
```javascript
|
||||
// src/routes/blog/[slug]/+page.js
|
||||
export async function load({ params, fetch }) {
|
||||
|
|
@ -601,6 +617,7 @@ export async function load({ params, fetch }) {
|
|||
```
|
||||
|
||||
**Load Function with Dependencies:**
|
||||
|
||||
```javascript
|
||||
// src/routes/blog/[slug]/+page.server.js
|
||||
export async function load({ params, fetch, depends }) {
|
||||
|
|
@ -608,8 +625,8 @@ export async function load({ params, fetch, depends }) {
|
|||
depends('app:blog-post');
|
||||
|
||||
const [post, comments] = await Promise.all([
|
||||
fetch(`/api/posts/${params.slug}`).then(r => r.json()),
|
||||
fetch(`/api/posts/${params.slug}/comments`).then(r => r.json())
|
||||
fetch(`/api/posts/${params.slug}`).then((r) => r.json()),
|
||||
fetch(`/api/posts/${params.slug}/comments`).then((r) => r.json()),
|
||||
]);
|
||||
|
||||
return { post, comments };
|
||||
|
|
@ -617,6 +634,7 @@ export async function load({ params, fetch, depends }) {
|
|||
```
|
||||
|
||||
**Invalidating Data from Component:**
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { invalidate } from '$app/navigation';
|
||||
|
|
@ -634,20 +652,21 @@ export async function load({ params, fetch, depends }) {
|
|||
|
||||
### Data Fetching Comparison
|
||||
|
||||
| Feature | React (useEffect) | SvelteKit Load Functions |
|
||||
|---------|-------------------|--------------------------|
|
||||
| **When Runs** | After component mounts | Before page renders |
|
||||
| **SSR Support** | Manual setup | Built-in |
|
||||
| **Loading State** | Manual management | Automatic |
|
||||
| **Error Handling** | Try/catch + state | `error()` helper |
|
||||
| **Waterfalls** | Common problem | Parallel by default |
|
||||
| **Caching** | Manual/React Query | Automatic |
|
||||
| **Revalidation** | Manual/dependencies | `invalidate()` API |
|
||||
| **Type Safety** | Manual typing | Automatic inference |
|
||||
| Feature | React (useEffect) | SvelteKit Load Functions |
|
||||
| ------------------ | ---------------------- | ------------------------ |
|
||||
| **When Runs** | After component mounts | Before page renders |
|
||||
| **SSR Support** | Manual setup | Built-in |
|
||||
| **Loading State** | Manual management | Automatic |
|
||||
| **Error Handling** | Try/catch + state | `error()` helper |
|
||||
| **Waterfalls** | Common problem | Parallel by default |
|
||||
| **Caching** | Manual/React Query | Automatic |
|
||||
| **Revalidation** | Manual/dependencies | `invalidate()` API |
|
||||
| **Type Safety** | Manual typing | Automatic inference |
|
||||
|
||||
### API Routes Comparison
|
||||
|
||||
#### Next.js API Route
|
||||
|
||||
```javascript
|
||||
// pages/api/posts/[slug].js
|
||||
export default async function handler(req, res) {
|
||||
|
|
@ -663,6 +682,7 @@ export default async function handler(req, res) {
|
|||
```
|
||||
|
||||
#### SvelteKit Server Route
|
||||
|
||||
```javascript
|
||||
// src/routes/api/posts/[slug]/+server.js
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
|
@ -691,6 +711,7 @@ export async function DELETE({ params }) {
|
|||
### React vs Svelte Event Syntax
|
||||
|
||||
#### React Event Handling
|
||||
|
||||
```jsx
|
||||
function EventsExample() {
|
||||
const [value, setValue] = useState('');
|
||||
|
|
@ -714,23 +735,19 @@ function EventsExample() {
|
|||
return (
|
||||
<div>
|
||||
<button onClick={handleClick}>Click</button>
|
||||
<button onClick={() => handleClickWithParam(123)}>
|
||||
Click with param
|
||||
</button>
|
||||
<button onClick={() => handleClickWithParam(123)}>Click with param</button>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
<input value={value} onChange={(e) => setValue(e.target.value)} />
|
||||
</form>
|
||||
|
||||
{/* Stop propagation */}
|
||||
<div onClick={() => console.log('Parent')}>
|
||||
<button onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log('Child');
|
||||
}}>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log('Child');
|
||||
}}>
|
||||
Click me
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -740,6 +757,7 @@ function EventsExample() {
|
|||
```
|
||||
|
||||
#### Svelte 5 Event Handling
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let value = $state('');
|
||||
|
|
@ -789,14 +807,11 @@ function EventsExample() {
|
|||
### Component Events
|
||||
|
||||
#### React (Callback Props)
|
||||
|
||||
```jsx
|
||||
// Child.jsx
|
||||
function Child({ onCustomEvent }) {
|
||||
return (
|
||||
<button onClick={() => onCustomEvent({ detail: 'data' })}>
|
||||
Trigger Event
|
||||
</button>
|
||||
);
|
||||
return <button onClick={() => onCustomEvent({ detail: 'data' })}>Trigger Event</button>;
|
||||
}
|
||||
|
||||
// Parent.jsx
|
||||
|
|
@ -810,6 +825,7 @@ function Parent() {
|
|||
```
|
||||
|
||||
#### Svelte 5 (Callback Props - Recommended)
|
||||
|
||||
```svelte
|
||||
<!-- Child.svelte -->
|
||||
<script>
|
||||
|
|
@ -834,15 +850,15 @@ function Parent() {
|
|||
|
||||
### Event Handling Comparison
|
||||
|
||||
| Feature | React | Svelte 5 |
|
||||
|---------|-------|----------|
|
||||
| **Event Names** | camelCase (`onClick`) | lowercase (`onclick`) |
|
||||
| **Prevent Default** | `e.preventDefault()` | Manual or form action |
|
||||
| **Stop Propagation** | `e.stopPropagation()` | Manual in handler |
|
||||
| **Event Modifiers** | Manual in handler | Manual (legacy modifiers removed) |
|
||||
| **Component Events** | Callback props | Callback props (recommended) |
|
||||
| **Performance** | New function per render | Function created once |
|
||||
| **Two-way Binding** | Controlled components | `bind:value` directive |
|
||||
| Feature | React | Svelte 5 |
|
||||
| -------------------- | ----------------------- | --------------------------------- |
|
||||
| **Event Names** | camelCase (`onClick`) | lowercase (`onclick`) |
|
||||
| **Prevent Default** | `e.preventDefault()` | Manual or form action |
|
||||
| **Stop Propagation** | `e.stopPropagation()` | Manual in handler |
|
||||
| **Event Modifiers** | Manual in handler | Manual (legacy modifiers removed) |
|
||||
| **Component Events** | Callback props | Callback props (recommended) |
|
||||
| **Performance** | New function per render | Function created once |
|
||||
| **Two-way Binding** | Controlled components | `bind:value` directive |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -851,6 +867,7 @@ function Parent() {
|
|||
### React Lifecycle Hooks → Svelte Lifecycle
|
||||
|
||||
#### React Lifecycle
|
||||
|
||||
```jsx
|
||||
import { useState, useEffect, useLayoutEffect, useRef } from 'react';
|
||||
|
||||
|
|
@ -895,6 +912,7 @@ function LifecycleExample() {
|
|||
```
|
||||
|
||||
#### Svelte Lifecycle
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { onMount, onDestroy, beforeUpdate, afterUpdate, tick } from 'svelte';
|
||||
|
|
@ -949,14 +967,14 @@ function LifecycleExample() {
|
|||
|
||||
### Lifecycle Comparison Table
|
||||
|
||||
| React Hook | Svelte Function | When It Runs |
|
||||
|------------|-----------------|--------------|
|
||||
| `useEffect(() => {}, [])` | `onMount()` | After component mounts |
|
||||
| `useEffect(() => { return cleanup })` | `onDestroy()` | Before unmount |
|
||||
| `useEffect(() => {}, [deps])` | `$effect()` | When dependencies change |
|
||||
| `useLayoutEffect()` | `beforeUpdate()` | Before DOM updates |
|
||||
| N/A | `afterUpdate()` | After DOM updates |
|
||||
| N/A | `tick()` | Wait for pending updates |
|
||||
| React Hook | Svelte Function | When It Runs |
|
||||
| ------------------------------------- | ---------------- | ------------------------ |
|
||||
| `useEffect(() => {}, [])` | `onMount()` | After component mounts |
|
||||
| `useEffect(() => { return cleanup })` | `onDestroy()` | Before unmount |
|
||||
| `useEffect(() => {}, [deps])` | `$effect()` | When dependencies change |
|
||||
| `useLayoutEffect()` | `beforeUpdate()` | Before DOM updates |
|
||||
| N/A | `afterUpdate()` | After DOM updates |
|
||||
| N/A | `tick()` | Wait for pending updates |
|
||||
|
||||
### Key Differences
|
||||
|
||||
|
|
@ -972,6 +990,7 @@ function LifecycleExample() {
|
|||
### React Forms vs SvelteKit Form Actions
|
||||
|
||||
#### React Form Handling
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react';
|
||||
|
||||
|
|
@ -979,7 +998,7 @@ function ContactForm() {
|
|||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
message: ''
|
||||
message: '',
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
|
@ -987,7 +1006,7 @@ function ContactForm() {
|
|||
const handleChange = (e) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value
|
||||
[e.target.name]: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -1000,7 +1019,7 @@ function ContactForm() {
|
|||
const response = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData)
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
@ -1021,12 +1040,7 @@ function ContactForm() {
|
|||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
disabled={submitting}
|
||||
/>
|
||||
<input name="name" value={formData.name} onChange={handleChange} disabled={submitting} />
|
||||
{errors.name && <span>{errors.name}</span>}
|
||||
|
||||
<input
|
||||
|
|
@ -1057,6 +1071,7 @@ function ContactForm() {
|
|||
#### SvelteKit Form Actions
|
||||
|
||||
**Server Action:**
|
||||
|
||||
```javascript
|
||||
// src/routes/contact/+page.server.js
|
||||
import { fail } from '@sveltejs/kit';
|
||||
|
|
@ -1082,11 +1097,12 @@ export const actions = {
|
|||
await db.contacts.create({ name, email, message });
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Component (works without JavaScript!):**
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/contact/+page.svelte -->
|
||||
<script>
|
||||
|
|
@ -1130,6 +1146,7 @@ export const actions = {
|
|||
```
|
||||
|
||||
**Named Actions:**
|
||||
|
||||
```javascript
|
||||
// src/routes/auth/+page.server.js
|
||||
import { fail } from '@sveltejs/kit';
|
||||
|
|
@ -1145,7 +1162,7 @@ export const actions = {
|
|||
const data = await request.formData();
|
||||
// Handle registration
|
||||
return { success: true };
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -1161,6 +1178,7 @@ export const actions = {
|
|||
```
|
||||
|
||||
**Progressive Enhancement:**
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { enhance } from '$app/forms';
|
||||
|
|
@ -1202,16 +1220,16 @@ export const actions = {
|
|||
|
||||
### Form Handling Comparison
|
||||
|
||||
| Feature | React | SvelteKit |
|
||||
|---------|-------|-----------|
|
||||
| **JavaScript Required** | Yes | No (progressive enhancement) |
|
||||
| **Form State** | Manual with useState | Automatic via `form` prop |
|
||||
| **Validation** | Client-side or manual | Server-side with `fail()` |
|
||||
| **Submission** | Fetch API | Native form submission |
|
||||
| **Loading State** | Manual | Built-in with `use:enhance` |
|
||||
| **Error Handling** | Try/catch + state | Return from action |
|
||||
| **File Uploads** | FormData + fetch | Native FormData |
|
||||
| **Multiple Actions** | Different endpoints | Named actions on same page |
|
||||
| Feature | React | SvelteKit |
|
||||
| ----------------------- | --------------------- | ---------------------------- |
|
||||
| **JavaScript Required** | Yes | No (progressive enhancement) |
|
||||
| **Form State** | Manual with useState | Automatic via `form` prop |
|
||||
| **Validation** | Client-side or manual | Server-side with `fail()` |
|
||||
| **Submission** | Fetch API | Native form submission |
|
||||
| **Loading State** | Manual | Built-in with `use:enhance` |
|
||||
| **Error Handling** | Try/catch + state | Return from action |
|
||||
| **File Uploads** | FormData + fetch | Native FormData |
|
||||
| **Multiple Actions** | Different endpoints | Named actions on same page |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -1220,6 +1238,7 @@ export const actions = {
|
|||
### React (Create React App / Next.js) vs SvelteKit
|
||||
|
||||
#### React Project Structure
|
||||
|
||||
```
|
||||
my-react-app/
|
||||
├── public/
|
||||
|
|
@ -1252,6 +1271,7 @@ my-react-app/
|
|||
```
|
||||
|
||||
#### SvelteKit Project Structure
|
||||
|
||||
```
|
||||
my-sveltekit-app/
|
||||
├── src/
|
||||
|
|
@ -1296,16 +1316,19 @@ my-sveltekit-app/
|
|||
### Key Directories
|
||||
|
||||
#### `src/lib/` ($lib alias)
|
||||
|
||||
- Reusable components, utilities, stores
|
||||
- Imported via `$lib` alias: `import Button from '$lib/components/Button.svelte'`
|
||||
- Shareable across the entire application
|
||||
|
||||
#### `src/lib/server/` ($lib/server alias)
|
||||
|
||||
- Server-only code (database, auth, secrets)
|
||||
- Imported via `$lib/server` alias
|
||||
- SvelteKit ensures this code never reaches the client
|
||||
|
||||
#### `src/routes/`
|
||||
|
||||
- File-based routing structure
|
||||
- `+page.svelte` - Page components
|
||||
- `+page.js` - Universal load functions
|
||||
|
|
@ -1315,23 +1338,25 @@ my-sveltekit-app/
|
|||
- `+error.svelte` - Error pages
|
||||
|
||||
#### `static/`
|
||||
|
||||
- Static assets served at root (robots.txt, favicon, etc.)
|
||||
- Files accessible at `/filename`
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
| React | SvelteKit |
|
||||
|-------|-----------|
|
||||
| `Component.jsx` | `Component.svelte` |
|
||||
| `pages/about.jsx` | `routes/about/+page.svelte` |
|
||||
| `api/posts.js` (Next.js) | `routes/api/posts/+server.js` |
|
||||
| `_app.jsx` (Next.js) | `routes/+layout.svelte` |
|
||||
| `_document.jsx` (Next.js) | `app.html` |
|
||||
| `middleware.js` | `hooks.server.js` |
|
||||
| React | SvelteKit |
|
||||
| ------------------------- | ----------------------------- |
|
||||
| `Component.jsx` | `Component.svelte` |
|
||||
| `pages/about.jsx` | `routes/about/+page.svelte` |
|
||||
| `api/posts.js` (Next.js) | `routes/api/posts/+server.js` |
|
||||
| `_app.jsx` (Next.js) | `routes/+layout.svelte` |
|
||||
| `_document.jsx` (Next.js) | `app.html` |
|
||||
| `middleware.js` | `hooks.server.js` |
|
||||
|
||||
### Code Organization Patterns
|
||||
|
||||
**React Pattern (feature-based):**
|
||||
|
||||
```
|
||||
src/
|
||||
├── features/
|
||||
|
|
@ -1347,6 +1372,7 @@ src/
|
|||
```
|
||||
|
||||
**SvelteKit Pattern (route-based):**
|
||||
|
||||
```
|
||||
src/
|
||||
├── routes/
|
||||
|
|
@ -1385,12 +1411,14 @@ src/
|
|||
### Bundle Size
|
||||
|
||||
**React:**
|
||||
|
||||
- React runtime: ~42KB (minified + gzipped)
|
||||
- React DOM: ~130KB (minified + gzipped)
|
||||
- Total base: ~172KB
|
||||
- Additional libraries (Router, Redux, etc.) add more
|
||||
|
||||
**Svelte:**
|
||||
|
||||
- No runtime (compiler-based)
|
||||
- Component code: ~3-5KB per component (compiled)
|
||||
- Total base: ~5-10KB
|
||||
|
|
@ -1398,16 +1426,17 @@ src/
|
|||
|
||||
### Runtime Performance
|
||||
|
||||
| Metric | React | Svelte |
|
||||
|--------|-------|--------|
|
||||
| **Initial render** | Virtual DOM diffing | Direct DOM manipulation |
|
||||
| **Updates** | Re-render tree + diff | Surgical updates |
|
||||
| **Reactivity** | Runtime hooks | Compile-time analysis |
|
||||
| **Memory** | Higher (VDOM + fiber) | Lower (no abstraction layer) |
|
||||
| Metric | React | Svelte |
|
||||
| ------------------ | --------------------- | ---------------------------- |
|
||||
| **Initial render** | Virtual DOM diffing | Direct DOM manipulation |
|
||||
| **Updates** | Re-render tree + diff | Surgical updates |
|
||||
| **Reactivity** | Runtime hooks | Compile-time analysis |
|
||||
| **Memory** | Higher (VDOM + fiber) | Lower (no abstraction layer) |
|
||||
|
||||
### Optimization Techniques
|
||||
|
||||
#### React Optimizations
|
||||
|
||||
```jsx
|
||||
// Memoization
|
||||
const MemoizedComponent = React.memo(Component);
|
||||
|
|
@ -1423,6 +1452,7 @@ const LazyComponent = lazy(() => import('./Component'));
|
|||
```
|
||||
|
||||
#### Svelte Optimizations
|
||||
|
||||
```svelte
|
||||
<!-- Most optimizations built-in -->
|
||||
<script>
|
||||
|
|
@ -1479,6 +1509,7 @@ const LazyComponent = lazy(() => import('./Component'));
|
|||
### Phase 4: Component Conversion
|
||||
|
||||
For each component:
|
||||
|
||||
- [ ] Remove React imports
|
||||
- [ ] Convert JSX to Svelte template syntax
|
||||
- [ ] Change `className` to `class`
|
||||
|
|
@ -1528,42 +1559,45 @@ For each component:
|
|||
|
||||
### React → Svelte Translation Table
|
||||
|
||||
| React Code | Svelte 5 Code |
|
||||
|------------|---------------|
|
||||
| `import React from 'react'` | No import needed |
|
||||
| `const [x, setX] = useState(0)` | `let x = $state(0)` |
|
||||
| `const y = useMemo(() => x * 2, [x])` | `const y = $derived(x * 2)` |
|
||||
| `useEffect(() => {...}, [x])` | `$effect(() => {...})` |
|
||||
| `const ref = useRef()` | `let ref` |
|
||||
| `<div className="foo">` | `<div class="foo">` |
|
||||
| `<button onClick={fn}>` | `<button onclick={fn}>` |
|
||||
| `<input value={x} onChange={...} />` | `<input bind:value={x} />` |
|
||||
| `{condition && <Component />}` | `{#if condition}<Component />{/if}` |
|
||||
| `{items.map(i => <Item {...i} />)}` | `{#each items as i}<Item {...i} />{/each}` |
|
||||
| `<Component {...props} />` | `<Component {...props} />` (same) |
|
||||
| `const { prop } = props` | `let { prop } = $props()` |
|
||||
| `props.children` | `<slot />` |
|
||||
| `<Link to="/about">` | `<a href="/about">` |
|
||||
| `useNavigate()` | `goto()` from `$app/navigation` |
|
||||
| `useParams()` | `params` from load function |
|
||||
| `useSearchParams()` | `url.searchParams` from load |
|
||||
| React Code | Svelte 5 Code |
|
||||
| ------------------------------------- | ------------------------------------------ |
|
||||
| `import React from 'react'` | No import needed |
|
||||
| `const [x, setX] = useState(0)` | `let x = $state(0)` |
|
||||
| `const y = useMemo(() => x * 2, [x])` | `const y = $derived(x * 2)` |
|
||||
| `useEffect(() => {...}, [x])` | `$effect(() => {...})` |
|
||||
| `const ref = useRef()` | `let ref` |
|
||||
| `<div className="foo">` | `<div class="foo">` |
|
||||
| `<button onClick={fn}>` | `<button onclick={fn}>` |
|
||||
| `<input value={x} onChange={...} />` | `<input bind:value={x} />` |
|
||||
| `{condition && <Component />}` | `{#if condition}<Component />{/if}` |
|
||||
| `{items.map(i => <Item {...i} />)}` | `{#each items as i}<Item {...i} />{/each}` |
|
||||
| `<Component {...props} />` | `<Component {...props} />` (same) |
|
||||
| `const { prop } = props` | `let { prop } = $props()` |
|
||||
| `props.children` | `<slot />` |
|
||||
| `<Link to="/about">` | `<a href="/about">` |
|
||||
| `useNavigate()` | `goto()` from `$app/navigation` |
|
||||
| `useParams()` | `params` from load function |
|
||||
| `useSearchParams()` | `url.searchParams` from load |
|
||||
|
||||
---
|
||||
|
||||
## Additional Resources
|
||||
|
||||
### Official Documentation
|
||||
|
||||
- **SvelteKit Docs**: https://svelte.dev/docs/kit/introduction
|
||||
- **Svelte 5 Docs**: https://svelte.dev/docs/svelte/overview
|
||||
- **Svelte Tutorial**: https://svelte.dev/tutorial
|
||||
- **SvelteKit Examples**: https://svelte.dev/examples
|
||||
|
||||
### Community Resources
|
||||
|
||||
- **Svelte Discord**: https://svelte.dev/chat
|
||||
- **Svelte Reddit**: https://reddit.com/r/sveltejs
|
||||
- **SvelteKit GitHub**: https://github.com/sveltejs/kit
|
||||
|
||||
### Learning Paths
|
||||
|
||||
1. Complete Svelte tutorial (1-2 hours)
|
||||
2. Build a simple SvelteKit app (3-5 hours)
|
||||
3. Migrate one React component to Svelte (1 hour)
|
||||
|
|
@ -1577,6 +1611,7 @@ For each component:
|
|||
Migrating from React to SvelteKit involves learning a new paradigm, but many concepts translate directly:
|
||||
|
||||
**Easier in Svelte:**
|
||||
|
||||
- Less boilerplate (no imports, smaller files)
|
||||
- Automatic reactivity (no dependency arrays)
|
||||
- Built-in routing (no React Router)
|
||||
|
|
@ -1585,6 +1620,7 @@ Migrating from React to SvelteKit involves learning a new paradigm, but many con
|
|||
- Scoped styles by default
|
||||
|
||||
**Requires Adjustment:**
|
||||
|
||||
- File-based routing structure
|
||||
- Load functions instead of useEffect
|
||||
- Different reactivity model (runes vs hooks)
|
||||
|
|
@ -1605,4 +1641,4 @@ Migrating from React to SvelteKit involves learning a new paradigm, but many con
|
|||
- **Target Audience**: React developers with intermediate+ experience
|
||||
- **Migration Scope**: Client-side React apps and Next.js applications
|
||||
- **Svelte Version**: Svelte 5 (with runes)
|
||||
- **SvelteKit Version**: Latest stable (2.x)
|
||||
- **SvelteKit Version**: Latest stable (2.x)
|
||||
|
|
|
|||
|
|
@ -11,12 +11,14 @@ The memo list and memo preview components were not updating in real-time when st
|
|||
## Solution Implemented
|
||||
|
||||
Implemented a **hybrid subscription model** combining:
|
||||
|
||||
1. **postgres_changes subscriptions** - For user-initiated direct updates
|
||||
2. **Broadcast channel subscriptions** - For service_role edge function updates
|
||||
|
||||
### Edge Functions (Backend)
|
||||
|
||||
Edge functions already send broadcasts to `memo-updates-{memoId}` channels when they complete processing:
|
||||
|
||||
- `batch-transcribe-callback/index.ts` - Sends broadcasts after transcription
|
||||
- `headline/index.ts` - Sends broadcasts after headline generation
|
||||
- `translate/index.ts` - Sends broadcasts after translation
|
||||
|
|
@ -25,123 +27,126 @@ Edge functions already send broadcasts to `memo-updates-{memoId}` channels when
|
|||
### Client Components (Frontend)
|
||||
|
||||
**Files Modified:**
|
||||
|
||||
1. `/memoro_app/components/molecules/MemoList.tsx`
|
||||
2. `/memoro_app/components/molecules/MemoPreview.tsx`
|
||||
|
||||
#### MemoList.tsx Changes
|
||||
|
||||
**Import Added:**
|
||||
|
||||
```typescript
|
||||
import { memoRealtimeService } from '~/features/memos/services/memoRealtimeService';
|
||||
```
|
||||
|
||||
**New useEffect Hook (lines 387-437):**
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const unsubscribeFunctions: (() => void)[] = [];
|
||||
const unsubscribeFunctions: (() => void)[] = [];
|
||||
|
||||
// Subscribe to broadcasts for all visible memos
|
||||
memos.forEach((memo) => {
|
||||
const unsubscribe = memoRealtimeService.subscribeToBroadcastChannel(
|
||||
`memo-updates-${memo.id}`,
|
||||
async (payload) => {
|
||||
console.log('MemoList: Received broadcast for memo', memo.id, payload);
|
||||
// Subscribe to broadcasts for all visible memos
|
||||
memos.forEach((memo) => {
|
||||
const unsubscribe = memoRealtimeService.subscribeToBroadcastChannel(
|
||||
`memo-updates-${memo.id}`,
|
||||
async (payload) => {
|
||||
console.log('MemoList: Received broadcast for memo', memo.id, payload);
|
||||
|
||||
try {
|
||||
// Fetch fresh memo data from Supabase
|
||||
const supabase = await getAuthenticatedClient();
|
||||
const { data: updatedMemo, error } = await supabase
|
||||
.from('memos')
|
||||
.select('*')
|
||||
.eq('id', memo.id)
|
||||
.single();
|
||||
try {
|
||||
// Fetch fresh memo data from Supabase
|
||||
const supabase = await getAuthenticatedClient();
|
||||
const { data: updatedMemo, error } = await supabase
|
||||
.from('memos')
|
||||
.select('*')
|
||||
.eq('id', memo.id)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('MemoList: Error fetching updated memo after broadcast:', error);
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
console.error('MemoList: Error fetching updated memo after broadcast:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (updatedMemo) {
|
||||
// Update the memo in the list immediately
|
||||
setMemos(prevMemos =>
|
||||
prevMemos.map(m => m.id === memo.id ? updatedMemo : m)
|
||||
);
|
||||
if (updatedMemo) {
|
||||
// Update the memo in the list immediately
|
||||
setMemos((prevMemos) => prevMemos.map((m) => (m.id === memo.id ? updatedMemo : m)));
|
||||
|
||||
console.log('MemoList: Updated memo from broadcast', {
|
||||
id: updatedMemo.id,
|
||||
title: updatedMemo.title,
|
||||
headlineStatus: updatedMemo.metadata?.processing?.headline_and_intro?.status
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('MemoList: Error processing broadcast update:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log('MemoList: Updated memo from broadcast', {
|
||||
id: updatedMemo.id,
|
||||
title: updatedMemo.title,
|
||||
headlineStatus: updatedMemo.metadata?.processing?.headline_and_intro?.status,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('MemoList: Error processing broadcast update:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
unsubscribeFunctions.push(unsubscribe);
|
||||
});
|
||||
unsubscribeFunctions.push(unsubscribe);
|
||||
});
|
||||
|
||||
// Cleanup on unmount or memo list change
|
||||
return () => {
|
||||
unsubscribeFunctions.forEach(unsub => unsub());
|
||||
};
|
||||
}, [memos.map(m => m.id).join(',')]); // Re-subscribe when memo IDs change
|
||||
// Cleanup on unmount or memo list change
|
||||
return () => {
|
||||
unsubscribeFunctions.forEach((unsub) => unsub());
|
||||
};
|
||||
}, [memos.map((m) => m.id).join(',')]); // Re-subscribe when memo IDs change
|
||||
```
|
||||
|
||||
#### MemoPreview.tsx Changes
|
||||
|
||||
**Import Added:**
|
||||
|
||||
```typescript
|
||||
import { memoRealtimeService } from '~/features/memos/services/memoRealtimeService';
|
||||
```
|
||||
|
||||
**New useEffect Hook (lines 242-287):**
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
if (!memo?.id) return;
|
||||
if (!memo?.id) return;
|
||||
|
||||
const unsubscribe = memoRealtimeService.subscribeToBroadcastChannel(
|
||||
`memo-updates-${memo.id}`,
|
||||
async (payload) => {
|
||||
console.log('MemoPreview: Received broadcast for memo', memo.id, payload);
|
||||
const unsubscribe = memoRealtimeService.subscribeToBroadcastChannel(
|
||||
`memo-updates-${memo.id}`,
|
||||
async (payload) => {
|
||||
console.log('MemoPreview: Received broadcast for memo', memo.id, payload);
|
||||
|
||||
try {
|
||||
// Fetch fresh memo data from Supabase
|
||||
const supabase = await getAuthenticatedClient();
|
||||
const { data: updatedMemo, error } = await supabase
|
||||
.from('memos')
|
||||
.select('*')
|
||||
.eq('id', memo.id)
|
||||
.single();
|
||||
try {
|
||||
// Fetch fresh memo data from Supabase
|
||||
const supabase = await getAuthenticatedClient();
|
||||
const { data: updatedMemo, error } = await supabase
|
||||
.from('memos')
|
||||
.select('*')
|
||||
.eq('id', memo.id)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('MemoPreview: Error fetching updated memo after broadcast:', error);
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
console.error('MemoPreview: Error fetching updated memo after broadcast:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (updatedMemo) {
|
||||
// If this is the latest memo on recording page, update it in the store
|
||||
if (reactToGlobalRecordingStatus) {
|
||||
setLatestMemo(updatedMemo);
|
||||
}
|
||||
if (updatedMemo) {
|
||||
// If this is the latest memo on recording page, update it in the store
|
||||
if (reactToGlobalRecordingStatus) {
|
||||
setLatestMemo(updatedMemo);
|
||||
}
|
||||
|
||||
console.log('MemoPreview: Updated memo from broadcast', {
|
||||
id: updatedMemo.id,
|
||||
title: updatedMemo.title,
|
||||
headlineStatus: updatedMemo.metadata?.processing?.headline_and_intro?.status
|
||||
});
|
||||
console.log('MemoPreview: Updated memo from broadcast', {
|
||||
id: updatedMemo.id,
|
||||
title: updatedMemo.title,
|
||||
headlineStatus: updatedMemo.metadata?.processing?.headline_and_intro?.status,
|
||||
});
|
||||
|
||||
// The useMemoProcessing hook will automatically recalculate displayTitle
|
||||
// based on the updated memo state
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('MemoPreview: Error processing broadcast update:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
// The useMemoProcessing hook will automatically recalculate displayTitle
|
||||
// based on the updated memo state
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('MemoPreview: Error processing broadcast update:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => unsubscribe();
|
||||
return () => unsubscribe();
|
||||
}, [memo?.id, reactToGlobalRecordingStatus, setLatestMemo]);
|
||||
```
|
||||
|
||||
|
|
@ -212,6 +217,7 @@ UI re-renders with new title/status
|
|||
### Expected Console Output
|
||||
|
||||
When broadcast received:
|
||||
|
||||
```
|
||||
MemoList: Received broadcast for memo {memoId} {payload}
|
||||
MemoList: Updated memo from broadcast {id, title, headlineStatus}
|
||||
|
|
|
|||
|
|
@ -19,17 +19,18 @@ The `tokenManager.ts` already handles most scenarios correctly:
|
|||
```typescript
|
||||
// Handles both refresh_in_progress and rotation_in_progress errors
|
||||
if (result.error === 'refresh_in_progress' || result.error === 'rotation_in_progress') {
|
||||
console.debug('TokenManager: Token rotation in progress, waiting...');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
// Retry after waiting
|
||||
console.debug('TokenManager: Token rotation in progress, waiting...');
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
// Retry after waiting
|
||||
}
|
||||
```
|
||||
|
||||
### Non-Retryable Errors
|
||||
|
||||
The following errors indicate permanent failures and should not be retried:
|
||||
|
||||
- `invalid_token` - Token doesn't exist
|
||||
- `token_expired` - Token or grace period expired
|
||||
- `token_expired` - Token or grace period expired
|
||||
- `invalid_token_state` - Token in unexpected state
|
||||
- `token_collision` - Very rare UUID collision
|
||||
|
||||
|
|
@ -63,9 +64,10 @@ To test the grace period implementation:
|
|||
## No Additional Changes Required
|
||||
|
||||
The current frontend implementation is already compatible with the grace period feature because:
|
||||
|
||||
- It properly retries on temporary errors
|
||||
- It saves whatever token is returned
|
||||
- It handles the new `rotation_in_progress` error
|
||||
- It respects permanent failure errors
|
||||
|
||||
The grace period is transparent to the frontend - it just makes the system more resilient to network issues and race conditions.
|
||||
The grace period is transparent to the frontend - it just makes the system more resilient to network issues and race conditions.
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ This document outlines the remaining implementation tasks for the Memoro Spaces
|
|||
## Core API Integration
|
||||
|
||||
### Backend Integration Tasks
|
||||
|
||||
- [ ] Configure space-memo relationship API endpoints on the backend
|
||||
- [ ] Set up proper authentication for all space-related endpoints
|
||||
- [ ] Implement database triggers for space deletion cleanup
|
||||
|
||||
### Frontend Integration Tasks
|
||||
|
||||
- [x] Update space service to use the new API endpoints
|
||||
- [x] Add memo-space linking/unlinking functionality
|
||||
- [x] Create space selector component for memos
|
||||
|
|
@ -18,18 +20,21 @@ This document outlines the remaining implementation tasks for the Memoro Spaces
|
|||
## UI Enhancements
|
||||
|
||||
### Space List View
|
||||
|
||||
- [x] Update spaces list with proper memo counts
|
||||
- [x] Implement space deletion with confirmation
|
||||
- [ ] Add space color selection in creation/edit flow
|
||||
- [ ] Implement space sorting options (by name, date, memo count)
|
||||
|
||||
### Space Detail View
|
||||
|
||||
- [x] Display space details with associated memos
|
||||
- [ ] Add space editing functionality
|
||||
- [ ] Implement memo filtering within a space
|
||||
- [ ] Add space statistics section (creation date, memo count, etc.)
|
||||
|
||||
### Memo-Space Management
|
||||
|
||||
- [x] Add space management option to memo menu
|
||||
- [x] Implement space selection modal for memos
|
||||
- [ ] Show space tags on memo list items
|
||||
|
|
@ -38,11 +43,13 @@ This document outlines the remaining implementation tasks for the Memoro Spaces
|
|||
## Data Management
|
||||
|
||||
### Local Storage
|
||||
|
||||
- [ ] Implement caching for spaces data
|
||||
- [ ] Add offline support for basic space operations
|
||||
- [ ] Create migration path for existing data
|
||||
|
||||
### Synchronization
|
||||
|
||||
- [ ] Implement proper error handling for failed sync operations
|
||||
- [ ] Add background sync for space-memo relationships
|
||||
- [ ] Create conflict resolution strategy for simultaneous edits
|
||||
|
|
@ -50,22 +57,26 @@ This document outlines the remaining implementation tasks for the Memoro Spaces
|
|||
## Performance Optimization
|
||||
|
||||
### Efficiency Improvements
|
||||
|
||||
- [ ] Optimize space listing for large numbers of spaces
|
||||
- [ ] Implement pagination for memos within a space
|
||||
- [ ] Add lazy loading for space contents
|
||||
|
||||
### Memory Management
|
||||
|
||||
- [ ] Optimize space selector component to handle large numbers of spaces
|
||||
- [ ] Implement memory-efficient rendering for space-memo relationships
|
||||
|
||||
## Testing & Quality Assurance
|
||||
|
||||
### Automated Tests
|
||||
|
||||
- [ ] Write unit tests for space service methods
|
||||
- [ ] Create integration tests for space-memo relationships
|
||||
- [ ] Implement UI component tests for space-related components
|
||||
|
||||
### Manual Testing
|
||||
|
||||
- [x] Create comprehensive testing guide
|
||||
- [ ] Test on multiple device sizes (mobile, tablet)
|
||||
- [ ] Verify proper handling of edge cases
|
||||
|
|
@ -74,11 +85,13 @@ This document outlines the remaining implementation tasks for the Memoro Spaces
|
|||
## Documentation
|
||||
|
||||
### User Documentation
|
||||
|
||||
- [ ] Create user guide for spaces feature
|
||||
- [ ] Add tooltips and help text for space operations
|
||||
- [ ] Document limitations and best practices
|
||||
|
||||
### Developer Documentation
|
||||
|
||||
- [x] Update API documentation with new endpoints
|
||||
- [x] Document component architecture
|
||||
- [ ] Create troubleshooting guide for common issues
|
||||
|
|
@ -87,6 +100,7 @@ This document outlines the remaining implementation tasks for the Memoro Spaces
|
|||
## Next Iteration Features
|
||||
|
||||
### Future Enhancements
|
||||
|
||||
- [ ] Implement space sharing between users
|
||||
- [ ] Add nested spaces/subspaces
|
||||
- [ ] Create space templates
|
||||
|
|
@ -104,17 +118,20 @@ This document outlines the remaining implementation tasks for the Memoro Spaces
|
|||
## Implementation Notes
|
||||
|
||||
### Key Components
|
||||
|
||||
- `SpaceService`: API integration for spaces
|
||||
- `SpaceContext`: Context provider for spaces state
|
||||
- `SpaceSelector`: Component for selecting spaces for a memo
|
||||
- `SpaceLinkSelector`: Modal for linking memos to spaces
|
||||
|
||||
### State Management
|
||||
|
||||
- Spaces state is managed through the `SpaceContext`
|
||||
- Space-memo relationships use stateful operations with optimistic updates
|
||||
- Error handling includes retry mechanisms and user feedback
|
||||
|
||||
### API Requirements
|
||||
|
||||
- All endpoints require a valid JWT token
|
||||
- Error responses follow standard HTTP status codes
|
||||
- Successful operations return appropriate confirmation data
|
||||
- Successful operations return appropriate confirmation data
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ This guide outlines the steps to test the spaces functionality in the Memoro app
|
|||
## Test Environment Setup
|
||||
|
||||
Before testing, ensure you have:
|
||||
|
||||
- A development build of the Memoro app running
|
||||
- At least one test user account
|
||||
- A few test memos created
|
||||
|
||||
You can enable mock data for testing without a backend by setting the environment variable:
|
||||
|
||||
```
|
||||
EXPO_PUBLIC_USE_MOCK_DATA=true
|
||||
```
|
||||
|
|
@ -19,6 +21,7 @@ EXPO_PUBLIC_USE_MOCK_DATA=true
|
|||
### 1. Spaces List View
|
||||
|
||||
#### Create a New Space
|
||||
|
||||
- Navigate to the Spaces tab
|
||||
- Tap "Create New Space" button
|
||||
- Enter a space name (e.g., "Test Space")
|
||||
|
|
@ -26,6 +29,7 @@ EXPO_PUBLIC_USE_MOCK_DATA=true
|
|||
- **Expected**: A new space should appear in the list
|
||||
|
||||
#### Space List Display
|
||||
|
||||
- Check that the spaces list shows:
|
||||
- Space name
|
||||
- Description (if any)
|
||||
|
|
@ -34,10 +38,11 @@ EXPO_PUBLIC_USE_MOCK_DATA=true
|
|||
- **Expected**: All spaces should be listed with correct information
|
||||
|
||||
#### Delete a Space
|
||||
|
||||
- Long press or tap on a space to open the action menu
|
||||
- Select "Delete" from the menu
|
||||
- Confirm deletion in the alert dialog
|
||||
- **Expected**:
|
||||
- **Expected**:
|
||||
- A confirmation dialog should show the space name
|
||||
- After confirmation, the space should be removed from the list
|
||||
- Success message should appear
|
||||
|
|
@ -45,11 +50,13 @@ EXPO_PUBLIC_USE_MOCK_DATA=true
|
|||
### 2. Space Detail View
|
||||
|
||||
#### Navigate to Space Detail
|
||||
|
||||
- Tap on a space in the spaces list
|
||||
- Select "View Details" from the menu
|
||||
- **Expected**: Space detail screen should open showing space information
|
||||
|
||||
#### Space Information Display
|
||||
|
||||
- Check that the space detail view shows:
|
||||
- Space name
|
||||
- Description
|
||||
|
|
@ -58,11 +65,13 @@ EXPO_PUBLIC_USE_MOCK_DATA=true
|
|||
- **Expected**: All information should be displayed correctly
|
||||
|
||||
#### View Memos in a Space
|
||||
|
||||
- Scroll down to see the "Memos in this Space" section
|
||||
- **Expected**: All memos associated with the space should be listed
|
||||
- **Expected**: If no memos are associated, an appropriate message should be shown
|
||||
|
||||
#### Add New Memo to Space
|
||||
|
||||
- Tap "Create New Memo" button
|
||||
- **Expected**: Navigation to memo creation screen
|
||||
- **Note**: This may not be fully implemented yet
|
||||
|
|
@ -70,17 +79,19 @@ EXPO_PUBLIC_USE_MOCK_DATA=true
|
|||
### 3. Memo-Space Management
|
||||
|
||||
#### Link a Memo to Spaces
|
||||
|
||||
- Navigate to a memo detail view
|
||||
- Tap the menu (three dots) icon
|
||||
- Select "Manage Spaces" from the menu
|
||||
- Check/uncheck spaces to link/unlink the memo
|
||||
- Tap "Save"
|
||||
- **Expected**:
|
||||
- **Expected**:
|
||||
- Space selector modal should open showing all available spaces
|
||||
- Spaces should be checkable/uncheckable
|
||||
- After saving, changes should persist
|
||||
|
||||
#### Unlink a Memo from Spaces
|
||||
|
||||
- Navigate to a memo detail view
|
||||
- Tap the menu icon
|
||||
- Select "Manage Spaces"
|
||||
|
|
@ -89,6 +100,7 @@ EXPO_PUBLIC_USE_MOCK_DATA=true
|
|||
- **Expected**: The memo should no longer be associated with the unchecked space
|
||||
|
||||
#### Verify Space-Memo Relationships
|
||||
|
||||
- Link a memo to a space
|
||||
- Navigate to that space's detail view
|
||||
- **Expected**: The linked memo should appear in the space's memo list
|
||||
|
|
@ -98,16 +110,19 @@ EXPO_PUBLIC_USE_MOCK_DATA=true
|
|||
### 4. Error Handling
|
||||
|
||||
#### Network Errors
|
||||
|
||||
- Turn off internet connection
|
||||
- Attempt to create a space or link a memo to a space
|
||||
- **Expected**: Appropriate error message should be displayed
|
||||
- **Expected**: The app should not crash
|
||||
|
||||
#### Invalid Operations
|
||||
|
||||
- Try to delete a space that has memos (if that's restricted)
|
||||
- **Expected**: Appropriate warning or confirmation message
|
||||
|
||||
#### Recovery from Errors
|
||||
|
||||
- Cause an error (e.g., by network disconnect)
|
||||
- Restore connectivity
|
||||
- Retry the operation
|
||||
|
|
@ -116,18 +131,21 @@ EXPO_PUBLIC_USE_MOCK_DATA=true
|
|||
## Edge Cases
|
||||
|
||||
### Space with Many Memos
|
||||
|
||||
- Create a space
|
||||
- Link 10+ memos to it
|
||||
- View the space detail
|
||||
- **Expected**: All memos should load and display correctly
|
||||
|
||||
### Long Names and Descriptions
|
||||
|
||||
- Create a space with a very long name (>50 characters)
|
||||
- Add a long description (>200 characters)
|
||||
- View in both list and detail views
|
||||
- **Expected**: Text should be properly truncated or wrapped
|
||||
|
||||
### Empty States
|
||||
|
||||
- Delete all spaces
|
||||
- Check spaces list
|
||||
- **Expected**: An appropriate "no spaces" message should be displayed
|
||||
|
|
@ -144,10 +162,11 @@ EXPO_PUBLIC_USE_MOCK_DATA=true
|
|||
## Reporting Issues
|
||||
|
||||
If you encounter any issues during testing, please document:
|
||||
|
||||
1. The steps to reproduce the issue
|
||||
2. What you expected to happen
|
||||
3. What actually happened
|
||||
4. Any error messages displayed
|
||||
5. Screenshots if applicable
|
||||
|
||||
Report these issues to the development team via the project management system.
|
||||
Report these issues to the development team via the project management system.
|
||||
|
|
|
|||
|
|
@ -14,33 +14,36 @@ Diese Dokumentation enthält eine Übersicht aller Blueprints und Prompts in der
|
|||
|
||||
In der Datenbank sind aktuell 2 Blueprints vorhanden:
|
||||
|
||||
| ID | Name (DE) | Name (EN) | Öffentlich | Erstellt am |
|
||||
|----|-----------|-----------|------------|-------------|
|
||||
| c05913ba-d062-477b-b3a4-183aed7f655a | Textanalyse | Text Analysis | Ja | 2025-05-08 11:09:22 |
|
||||
| e7f9a2d8-3b45-4c12-9876-5432abcdef01 | Kreatives Schreiben | Creative Writing | Ja | 2025-05-15 23:20:58 |
|
||||
| ID | Name (DE) | Name (EN) | Öffentlich | Erstellt am |
|
||||
| ------------------------------------ | ------------------- | ---------------- | ---------- | ------------------- |
|
||||
| c05913ba-d062-477b-b3a4-183aed7f655a | Textanalyse | Text Analysis | Ja | 2025-05-08 11:09:22 |
|
||||
| e7f9a2d8-3b45-4c12-9876-5432abcdef01 | Kreatives Schreiben | Creative Writing | Ja | 2025-05-15 23:20:58 |
|
||||
|
||||
### Textanalyse / Text Analysis
|
||||
|
||||
**ID:** c05913ba-d062-477b-b3a4-183aed7f655a
|
||||
|
||||
**Beschreibung:**
|
||||
|
||||
- DE: "Blueprint für die Analyse und Zusammenfassung von Texten"
|
||||
- EN: "Blueprint for analyzing and summarizing texts"
|
||||
|
||||
**Advice-Tipps:**
|
||||
|
||||
- Metadata:
|
||||
- Version: 1.0
|
||||
- Zuletzt aktualisiert: 2025-05-16T01:14:51+02:00
|
||||
- Unterstützte Sprachen: Deutsch, Englisch
|
||||
|
||||
| Tipp-ID | Reihenfolge | Inhalt (DE) | Inhalt (EN) |
|
||||
|---------|-------------|-------------|-------------|
|
||||
| tip1 | 1 | Sprechen Sie klar und deutlich, um eine präzise Textanalyse zu ermöglichen. | Speak clearly and distinctly to enable precise text analysis. |
|
||||
| tip2 | 2 | Strukturieren Sie Ihre Gedanken in logische Abschnitte für bessere Übersichtlichkeit. | Structure your thoughts into logical sections for better clarity. |
|
||||
| tip3 | 3 | Fassen Sie wichtige Punkte zusammen und heben Sie Schlüsselerkenntnisse hervor. | Summarize important points and highlight key insights. |
|
||||
| tip4 | 4 | Verwenden Sie Schlüsselwörter für eine bessere Kategorisierung Ihrer Texte. | Use keywords for better categorization of your texts. |
|
||||
| Tipp-ID | Reihenfolge | Inhalt (DE) | Inhalt (EN) |
|
||||
| ------- | ----------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| tip1 | 1 | Sprechen Sie klar und deutlich, um eine präzise Textanalyse zu ermöglichen. | Speak clearly and distinctly to enable precise text analysis. |
|
||||
| tip2 | 2 | Strukturieren Sie Ihre Gedanken in logische Abschnitte für bessere Übersichtlichkeit. | Structure your thoughts into logical sections for better clarity. |
|
||||
| tip3 | 3 | Fassen Sie wichtige Punkte zusammen und heben Sie Schlüsselerkenntnisse hervor. | Summarize important points and highlight key insights. |
|
||||
| tip4 | 4 | Verwenden Sie Schlüsselwörter für eine bessere Kategorisierung Ihrer Texte. | Use keywords for better categorization of your texts. |
|
||||
|
||||
**Verknüpfte Prompts:**
|
||||
|
||||
- Zusammenfassung / Summary
|
||||
- To Dos / To Dos
|
||||
|
||||
|
|
@ -49,23 +52,26 @@ In der Datenbank sind aktuell 2 Blueprints vorhanden:
|
|||
**ID:** e7f9a2d8-3b45-4c12-9876-5432abcdef01
|
||||
|
||||
**Beschreibung:**
|
||||
|
||||
- DE: "Entwickle kreative Texte, Geschichten und Ideen für verschiedene Formate"
|
||||
- EN: "Develop creative texts, stories and ideas for various formats"
|
||||
|
||||
**Advice-Tipps:**
|
||||
|
||||
- Metadata:
|
||||
- Version: 1.0
|
||||
- Zuletzt aktualisiert: 2025-05-16T01:20:19+02:00
|
||||
- Unterstützte Sprachen: Deutsch, Englisch
|
||||
|
||||
| Tipp-ID | Reihenfolge | Inhalt (DE) | Inhalt (EN) |
|
||||
|---------|-------------|-------------|-------------|
|
||||
| tip1 | 1 | Beschreibe deine Ideen detailliert mit lebendigen Bildern und konkreten Details. | Describe your ideas in detail with vivid imagery and concrete details. |
|
||||
| tip2 | 2 | Nutze verschiedene Perspektiven, um deine Geschichte aus unterschiedlichen Blickwinkeln zu betrachten. | Use different perspectives to view your story from various angles. |
|
||||
| tip3 | 3 | Experimentiere mit verschiedenen Erzählstilen und finde deinen eigenen kreativen Ausdruck. | Experiment with different narrative styles and find your own creative expression. |
|
||||
| tip4 | 4 | Achte auf den Rhythmus deiner Sprache - kurze Sätze für Spannung, längere für ruhige Momente. | Pay attention to the rhythm of your language - short sentences for tension, longer ones for calm moments. |
|
||||
| Tipp-ID | Reihenfolge | Inhalt (DE) | Inhalt (EN) |
|
||||
| ------- | ----------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
|
||||
| tip1 | 1 | Beschreibe deine Ideen detailliert mit lebendigen Bildern und konkreten Details. | Describe your ideas in detail with vivid imagery and concrete details. |
|
||||
| tip2 | 2 | Nutze verschiedene Perspektiven, um deine Geschichte aus unterschiedlichen Blickwinkeln zu betrachten. | Use different perspectives to view your story from various angles. |
|
||||
| tip3 | 3 | Experimentiere mit verschiedenen Erzählstilen und finde deinen eigenen kreativen Ausdruck. | Experiment with different narrative styles and find your own creative expression. |
|
||||
| tip4 | 4 | Achte auf den Rhythmus deiner Sprache - kurze Sätze für Spannung, längere für ruhige Momente. | Pay attention to the rhythm of your language - short sentences for tension, longer ones for calm moments. |
|
||||
|
||||
**Verknüpfte Prompts:**
|
||||
|
||||
- Kurzgeschichte / Short Story
|
||||
- Charakterprofil / Character Profile
|
||||
- Szenenbeschreibung / Scene Description
|
||||
|
|
@ -76,55 +82,62 @@ In der Datenbank sind aktuell 2 Blueprints vorhanden:
|
|||
|
||||
In der Datenbank sind aktuell 7 Prompts vorhanden:
|
||||
|
||||
| ID | Titel (DE) | Titel (EN) | Öffentlich | Erstellt am |
|
||||
|----|------------|------------|------------|-------------|
|
||||
| 9043d4d0-86b0-41b7-b5d7-7077e5cabacd | Zusammenfassung | Summary | Ja | 2025-05-08 11:04:22 |
|
||||
| da7bca61-a5c1-48f9-910b-b6eadcbcab17 | To Dos | To Dos | Ja | 2025-05-08 11:06:08 |
|
||||
| a1b2c3d4-e5f6-4a3b-8c7d-9e0f1a2b3c4d | Kurzgeschichte | Short Story | Ja | 2025-05-15 23:21:14 |
|
||||
| b2c3d4e5-f6a7-5b4c-9d8e-0f1a2b3c4d5e | Charakterprofil | Character Profile | Ja | 2025-05-15 23:21:14 |
|
||||
| c3d4e5f6-a7b8-6c5d-0e9f-1a2b3c4d5e6f | Szenenbeschreibung | Scene Description | Ja | 2025-05-15 23:21:14 |
|
||||
| d4e5f6a7-b8c9-7d6e-1f0a-2b3c4d5e6f7a | Dialog | Dialogue | Ja | 2025-05-15 23:21:14 |
|
||||
| e5f6a7b8-c9d0-8e7f-2a1b-3c4d5e6f7a8b | Gedicht/Songtext | Poem/Lyrics | Ja | 2025-05-15 23:21:14 |
|
||||
| ID | Titel (DE) | Titel (EN) | Öffentlich | Erstellt am |
|
||||
| ------------------------------------ | ------------------ | ----------------- | ---------- | ------------------- |
|
||||
| 9043d4d0-86b0-41b7-b5d7-7077e5cabacd | Zusammenfassung | Summary | Ja | 2025-05-08 11:04:22 |
|
||||
| da7bca61-a5c1-48f9-910b-b6eadcbcab17 | To Dos | To Dos | Ja | 2025-05-08 11:06:08 |
|
||||
| a1b2c3d4-e5f6-4a3b-8c7d-9e0f1a2b3c4d | Kurzgeschichte | Short Story | Ja | 2025-05-15 23:21:14 |
|
||||
| b2c3d4e5-f6a7-5b4c-9d8e-0f1a2b3c4d5e | Charakterprofil | Character Profile | Ja | 2025-05-15 23:21:14 |
|
||||
| c3d4e5f6-a7b8-6c5d-0e9f-1a2b3c4d5e6f | Szenenbeschreibung | Scene Description | Ja | 2025-05-15 23:21:14 |
|
||||
| d4e5f6a7-b8c9-7d6e-1f0a-2b3c4d5e6f7a | Dialog | Dialogue | Ja | 2025-05-15 23:21:14 |
|
||||
| e5f6a7b8-c9d0-8e7f-2a1b-3c4d5e6f7a8b | Gedicht/Songtext | Poem/Lyrics | Ja | 2025-05-15 23:21:14 |
|
||||
|
||||
### Detaillierte Prompt-Informationen
|
||||
|
||||
#### Zusammenfassung / Summary
|
||||
|
||||
- **ID:** 9043d4d0-86b0-41b7-b5d7-7077e5cabacd
|
||||
- **Prompt-Text:**
|
||||
- DE: "Fasse den folgenden Text ausführlich zusammen"
|
||||
- EN: "Summarize the following text in detail"
|
||||
|
||||
#### To Dos / To Dos
|
||||
|
||||
- **ID:** da7bca61-a5c1-48f9-910b-b6eadcbcab17
|
||||
- **Prompt-Text:**
|
||||
- DE: "Erstelle eine Liste mit Aufgaben aus dem folgenden Text"
|
||||
- EN: "Create a list of tasks from the following text"
|
||||
|
||||
#### Kurzgeschichte / Short Story
|
||||
|
||||
- **ID:** a1b2c3d4-e5f6-4a3b-8c7d-9e0f1a2b3c4d
|
||||
- **Prompt-Text:**
|
||||
- DE: "Entwickle eine kurze Geschichte basierend auf den folgenden Elementen"
|
||||
- EN: "Develop a short story based on the following elements"
|
||||
|
||||
#### Charakterprofil / Character Profile
|
||||
|
||||
- **ID:** b2c3d4e5-f6a7-5b4c-9d8e-0f1a2b3c4d5e
|
||||
- **Prompt-Text:**
|
||||
- DE: "Erstelle einen Charakter mit Hintergrundgeschichte, Persönlichkeit und Motivation"
|
||||
- EN: "Create a character with background story, personality and motivation"
|
||||
|
||||
#### Szenenbeschreibung / Scene Description
|
||||
|
||||
- **ID:** c3d4e5f6-a7b8-6c5d-0e9f-1a2b3c4d5e6f
|
||||
- **Prompt-Text:**
|
||||
- DE: "Beschreibe eine Szene mit allen Sinneseindrücken (Sehen, Hören, Riechen, Schmecken, Fühlen)"
|
||||
- EN: "Describe a scene with all sensory impressions (sight, sound, smell, taste, touch)"
|
||||
|
||||
#### Dialog / Dialogue
|
||||
|
||||
- **ID:** d4e5f6a7-b8c9-7d6e-1f0a-2b3c4d5e6f7a
|
||||
- **Prompt-Text:**
|
||||
- DE: "Entwickle einen Dialog zwischen zwei Charakteren mit unterschiedlichen Perspektiven"
|
||||
- EN: "Develop a dialogue between two characters with different perspectives"
|
||||
|
||||
#### Gedicht/Songtext / Poem/Lyrics
|
||||
|
||||
- **ID:** e5f6a7b8-c9d0-8e7f-2a1b-3c4d5e6f7a8b
|
||||
- **Prompt-Text:**
|
||||
- DE: "Schreibe ein Gedicht oder einen Songtext zu einem bestimmten Thema oder Gefühl"
|
||||
|
|
@ -134,16 +147,16 @@ In der Datenbank sind aktuell 7 Prompts vorhanden:
|
|||
|
||||
Die folgende Tabelle zeigt, welche Prompts mit welchen Blueprints verknüpft sind:
|
||||
|
||||
| Blueprint | Prompt | Erstellt am |
|
||||
|-----------|--------|-------------|
|
||||
| Textanalyse / Text Analysis | Zusammenfassung / Summary | 2025-05-08 11:09:29 |
|
||||
| Textanalyse / Text Analysis | To Dos / To Dos | 2025-05-08 11:09:29 |
|
||||
| Kreatives Schreiben / Creative Writing | Kurzgeschichte / Short Story | 2025-05-15 23:21:14 |
|
||||
| Kreatives Schreiben / Creative Writing | Charakterprofil / Character Profile | 2025-05-15 23:21:14 |
|
||||
| Blueprint | Prompt | Erstellt am |
|
||||
| -------------------------------------- | -------------------------------------- | ------------------- |
|
||||
| Textanalyse / Text Analysis | Zusammenfassung / Summary | 2025-05-08 11:09:29 |
|
||||
| Textanalyse / Text Analysis | To Dos / To Dos | 2025-05-08 11:09:29 |
|
||||
| Kreatives Schreiben / Creative Writing | Kurzgeschichte / Short Story | 2025-05-15 23:21:14 |
|
||||
| Kreatives Schreiben / Creative Writing | Charakterprofil / Character Profile | 2025-05-15 23:21:14 |
|
||||
| Kreatives Schreiben / Creative Writing | Szenenbeschreibung / Scene Description | 2025-05-15 23:21:14 |
|
||||
| Kreatives Schreiben / Creative Writing | Dialog / Dialogue | 2025-05-15 23:21:14 |
|
||||
| Kreatives Schreiben / Creative Writing | Gedicht/Songtext / Poem/Lyrics | 2025-05-15 23:21:14 |
|
||||
| Kreatives Schreiben / Creative Writing | Dialog / Dialogue | 2025-05-15 23:21:14 |
|
||||
| Kreatives Schreiben / Creative Writing | Gedicht/Songtext / Poem/Lyrics | 2025-05-15 23:21:14 |
|
||||
|
||||
---
|
||||
|
||||
*Letzte Aktualisierung: 16. Mai 2025*
|
||||
_Letzte Aktualisierung: 16. Mai 2025_
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ Diese Anleitung erklärt, wie Sie iOS Home Screen Widgets und Live Activities f
|
|||
## Voraussetzungen
|
||||
|
||||
### System Requirements
|
||||
|
||||
- **macOS 15 Sequoia** oder höher
|
||||
- **Xcode 16** oder höher
|
||||
- **CocoaPods 1.16.2** oder höher (Ruby 3.2.0+)
|
||||
|
|
@ -15,6 +16,7 @@ Diese Anleitung erklärt, wie Sie iOS Home Screen Widgets und Live Activities f
|
|||
- Apple Developer Account (für Device Testing)
|
||||
|
||||
### Development Build
|
||||
|
||||
Widgets funktionieren **nicht** mit Expo Go. Sie benötigen einen Development Build:
|
||||
|
||||
```bash
|
||||
|
|
@ -40,6 +42,7 @@ npx create-target widget
|
|||
```
|
||||
|
||||
Dies generiert automatisch:
|
||||
|
||||
- `/targets/widget/` Verzeichnis mit Swift-Dateien
|
||||
- Basis Widget-Konfiguration
|
||||
- Integration in app.json
|
||||
|
|
@ -48,29 +51,27 @@ Dies generiert automatisch:
|
|||
|
||||
```json
|
||||
{
|
||||
"expo": {
|
||||
"name": "Quote App",
|
||||
"slug": "quote-app",
|
||||
"version": "1.0.0",
|
||||
"platforms": ["ios"],
|
||||
"ios": {
|
||||
"bundleIdentifier": "com.yourcompany.quoteapp",
|
||||
"supportsTablet": true,
|
||||
"entitlements": {
|
||||
"com.apple.security.application-groups": [
|
||||
"group.com.yourcompany.quoteapp.widget"
|
||||
]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
[
|
||||
"@bacons/apple-targets",
|
||||
{
|
||||
"appleTeamId": "YOUR_TEAM_ID"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
"expo": {
|
||||
"name": "Quote App",
|
||||
"slug": "quote-app",
|
||||
"version": "1.0.0",
|
||||
"platforms": ["ios"],
|
||||
"ios": {
|
||||
"bundleIdentifier": "com.yourcompany.quoteapp",
|
||||
"supportsTablet": true,
|
||||
"entitlements": {
|
||||
"com.apple.security.application-groups": ["group.com.yourcompany.quoteapp.widget"]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
[
|
||||
"@bacons/apple-targets",
|
||||
{
|
||||
"appleTeamId": "YOUR_TEAM_ID"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -79,30 +80,25 @@ Dies generiert automatisch:
|
|||
```javascript
|
||||
/** @type {import('@bacons/apple-targets/app.plugin').Config} */
|
||||
module.exports = {
|
||||
type: "widget",
|
||||
name: "QuotesWidget",
|
||||
bundleIdentifier: "$(PRODUCT_BUNDLE_IDENTIFIER).widget",
|
||||
deploymentTarget: "16.0",
|
||||
icon: "../../assets/widget-icon.png",
|
||||
colors: {
|
||||
$accent: {
|
||||
color: "#007AFF",
|
||||
darkColor: "#0A84FF"
|
||||
},
|
||||
$widgetBackground: {
|
||||
color: "#FFFFFF",
|
||||
darkColor: "#1C1C1E"
|
||||
}
|
||||
},
|
||||
entitlements: {
|
||||
"com.apple.security.application-groups": [
|
||||
"group.com.yourcompany.quoteapp.widget"
|
||||
]
|
||||
},
|
||||
frameworks: [
|
||||
"SwiftUI",
|
||||
"WidgetKit"
|
||||
]
|
||||
type: 'widget',
|
||||
name: 'QuotesWidget',
|
||||
bundleIdentifier: '$(PRODUCT_BUNDLE_IDENTIFIER).widget',
|
||||
deploymentTarget: '16.0',
|
||||
icon: '../../assets/widget-icon.png',
|
||||
colors: {
|
||||
$accent: {
|
||||
color: '#007AFF',
|
||||
darkColor: '#0A84FF',
|
||||
},
|
||||
$widgetBackground: {
|
||||
color: '#FFFFFF',
|
||||
darkColor: '#1C1C1E',
|
||||
},
|
||||
},
|
||||
entitlements: {
|
||||
'com.apple.security.application-groups': ['group.com.yourcompany.quoteapp.widget'],
|
||||
},
|
||||
frameworks: ['SwiftUI', 'WidgetKit'],
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -114,35 +110,35 @@ Falls Sie mehr Kontrolle benötigen, können Sie einen eigenen Config Plugin ers
|
|||
|
||||
```javascript
|
||||
const {
|
||||
withXcodeProject,
|
||||
withDangerousMod,
|
||||
withEntitlementsPlist,
|
||||
withInfoPlist,
|
||||
IOSConfig
|
||||
withXcodeProject,
|
||||
withDangerousMod,
|
||||
withEntitlementsPlist,
|
||||
withInfoPlist,
|
||||
IOSConfig,
|
||||
} = require('@expo/config-plugins');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function withIOSWidget(config) {
|
||||
// App Group hinzufügen
|
||||
config = withEntitlementsPlist(config, async (config) => {
|
||||
config.modResults['com.apple.security.application-groups'] = [
|
||||
`group.${config.ios.bundleIdentifier}.widget`
|
||||
];
|
||||
return config;
|
||||
});
|
||||
// App Group hinzufügen
|
||||
config = withEntitlementsPlist(config, async (config) => {
|
||||
config.modResults['com.apple.security.application-groups'] = [
|
||||
`group.${config.ios.bundleIdentifier}.widget`,
|
||||
];
|
||||
return config;
|
||||
});
|
||||
|
||||
// Widget Target zum Xcode Project hinzufügen
|
||||
config = withXcodeProject(config, async (config) => {
|
||||
const project = config.modResults;
|
||||
|
||||
// Widget Target Configuration
|
||||
// (Detaillierte Implementation hier)
|
||||
|
||||
return config;
|
||||
});
|
||||
// Widget Target zum Xcode Project hinzufügen
|
||||
config = withXcodeProject(config, async (config) => {
|
||||
const project = config.modResults;
|
||||
|
||||
return config;
|
||||
// Widget Target Configuration
|
||||
// (Detaillierte Implementation hier)
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
module.exports = withIOSWidget;
|
||||
|
|
@ -167,7 +163,7 @@ struct QuoteEntry: TimelineEntry {
|
|||
// Widget Datenprovider
|
||||
struct QuoteProvider: TimelineProvider {
|
||||
let userDefaults = UserDefaults(suiteName: "group.com.yourcompany.quoteapp.widget")
|
||||
|
||||
|
||||
func placeholder(in context: Context) -> QuoteEntry {
|
||||
QuoteEntry(
|
||||
date: Date(),
|
||||
|
|
@ -176,18 +172,18 @@ struct QuoteProvider: TimelineProvider {
|
|||
category: "Innovation"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (QuoteEntry) -> ()) {
|
||||
let entry = getQuoteFromStorage() ?? placeholder(in: context)
|
||||
completion(entry)
|
||||
}
|
||||
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||
var entries: [QuoteEntry] = []
|
||||
|
||||
|
||||
// Quotes aus UserDefaults laden
|
||||
let quotes = loadQuotesFromUserDefaults()
|
||||
|
||||
|
||||
// Timeline mit stündlichen Updates erstellen
|
||||
let currentDate = Date()
|
||||
for hourOffset in 0..<24 {
|
||||
|
|
@ -200,11 +196,11 @@ struct QuoteProvider: TimelineProvider {
|
|||
category: quote.category
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
||||
completion(timeline)
|
||||
}
|
||||
|
||||
|
||||
private func loadQuotesFromUserDefaults() -> [QuoteModel] {
|
||||
guard let data = userDefaults?.data(forKey: "savedQuotes"),
|
||||
let quotes = try? JSONDecoder().decode([QuoteModel].self, from: data) else {
|
||||
|
|
@ -212,14 +208,14 @@ struct QuoteProvider: TimelineProvider {
|
|||
}
|
||||
return quotes
|
||||
}
|
||||
|
||||
|
||||
private func defaultQuotes() -> [QuoteModel] {
|
||||
return [
|
||||
QuoteModel(quote: "Innovation distinguishes between a leader and a follower.",
|
||||
author: "Steve Jobs",
|
||||
QuoteModel(quote: "Innovation distinguishes between a leader and a follower.",
|
||||
author: "Steve Jobs",
|
||||
category: "Leadership"),
|
||||
QuoteModel(quote: "The only way to do great work is to love what you do.",
|
||||
author: "Steve Jobs",
|
||||
QuoteModel(quote: "The only way to do great work is to love what you do.",
|
||||
author: "Steve Jobs",
|
||||
category: "Motivation")
|
||||
]
|
||||
}
|
||||
|
|
@ -229,7 +225,7 @@ struct QuoteProvider: TimelineProvider {
|
|||
struct QuotesWidgetView : View {
|
||||
var entry: QuoteProvider.Entry
|
||||
@Environment(\.widgetFamily) var family
|
||||
|
||||
|
||||
var body: some View {
|
||||
switch family {
|
||||
case .systemSmall:
|
||||
|
|
@ -247,16 +243,16 @@ struct QuotesWidgetView : View {
|
|||
// Small Widget Layout
|
||||
struct SmallWidgetView: View {
|
||||
let entry: QuoteEntry
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(entry.quote)
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.lineLimit(3)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
Text("— \(entry.author)")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.secondary)
|
||||
|
|
@ -276,7 +272,7 @@ struct SmallWidgetView: View {
|
|||
// Medium Widget Layout
|
||||
struct MediumWidgetView: View {
|
||||
let entry: QuoteEntry
|
||||
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
|
|
@ -285,19 +281,19 @@ struct MediumWidgetView: View {
|
|||
.font(.system(size: 10, weight: .semibold))
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
|
||||
|
||||
Text(entry.quote)
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.lineLimit(3)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
|
||||
Text("— \(entry.author)")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
Image(systemName: "quote.bubble.fill")
|
||||
.font(.system(size: 30))
|
||||
.foregroundColor(.blue.opacity(0.3))
|
||||
|
|
@ -313,7 +309,7 @@ struct MediumWidgetView: View {
|
|||
@main
|
||||
struct QuotesWidget: Widget {
|
||||
let kind: String = "QuotesWidget"
|
||||
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: QuoteProvider()) { entry in
|
||||
QuotesWidgetView(entry: entry)
|
||||
|
|
@ -353,69 +349,61 @@ import SharedGroupPreferences from 'react-native-shared-group-preferences';
|
|||
const APP_GROUP = 'group.com.yourcompany.quoteapp.widget';
|
||||
|
||||
export class WidgetDataManager {
|
||||
static async saveQuotesToWidget(quotes: Quote[]): Promise<void> {
|
||||
try {
|
||||
const quotesData = JSON.stringify(quotes);
|
||||
await SharedGroupPreferences.setItem(
|
||||
'savedQuotes',
|
||||
quotesData,
|
||||
APP_GROUP
|
||||
);
|
||||
|
||||
// Widget Update triggern (iOS 14+)
|
||||
if (Platform.OS === 'ios') {
|
||||
const WidgetKit = NativeModules.WidgetKit;
|
||||
WidgetKit?.reloadAllTimelines();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save quotes to widget:', error);
|
||||
}
|
||||
}
|
||||
static async saveQuotesToWidget(quotes: Quote[]): Promise<void> {
|
||||
try {
|
||||
const quotesData = JSON.stringify(quotes);
|
||||
await SharedGroupPreferences.setItem('savedQuotes', quotesData, APP_GROUP);
|
||||
|
||||
static async saveDailyQuote(quote: Quote): Promise<void> {
|
||||
try {
|
||||
const quoteData = JSON.stringify({
|
||||
...quote,
|
||||
date: new Date().toISOString()
|
||||
});
|
||||
|
||||
await SharedGroupPreferences.setItem(
|
||||
'dailyQuote',
|
||||
quoteData,
|
||||
APP_GROUP
|
||||
);
|
||||
|
||||
// Widget aktualisieren
|
||||
this.refreshWidget();
|
||||
} catch (error) {
|
||||
console.error('Failed to save daily quote:', error);
|
||||
}
|
||||
}
|
||||
// Widget Update triggern (iOS 14+)
|
||||
if (Platform.OS === 'ios') {
|
||||
const WidgetKit = NativeModules.WidgetKit;
|
||||
WidgetKit?.reloadAllTimelines();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save quotes to widget:', error);
|
||||
}
|
||||
}
|
||||
|
||||
static async saveUserPreferences(preferences: WidgetPreferences): Promise<void> {
|
||||
try {
|
||||
await SharedGroupPreferences.setItem(
|
||||
'widgetPreferences',
|
||||
JSON.stringify(preferences),
|
||||
APP_GROUP
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to save widget preferences:', error);
|
||||
}
|
||||
}
|
||||
static async saveDailyQuote(quote: Quote): Promise<void> {
|
||||
try {
|
||||
const quoteData = JSON.stringify({
|
||||
...quote,
|
||||
date: new Date().toISOString(),
|
||||
});
|
||||
|
||||
static refreshWidget(): void {
|
||||
if (Platform.OS === 'ios') {
|
||||
NativeModules.WidgetKit?.reloadAllTimelines();
|
||||
}
|
||||
}
|
||||
await SharedGroupPreferences.setItem('dailyQuote', quoteData, APP_GROUP);
|
||||
|
||||
// Widget aktualisieren
|
||||
this.refreshWidget();
|
||||
} catch (error) {
|
||||
console.error('Failed to save daily quote:', error);
|
||||
}
|
||||
}
|
||||
|
||||
static async saveUserPreferences(preferences: WidgetPreferences): Promise<void> {
|
||||
try {
|
||||
await SharedGroupPreferences.setItem(
|
||||
'widgetPreferences',
|
||||
JSON.stringify(preferences),
|
||||
APP_GROUP
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to save widget preferences:', error);
|
||||
}
|
||||
}
|
||||
|
||||
static refreshWidget(): void {
|
||||
if (Platform.OS === 'ios') {
|
||||
NativeModules.WidgetKit?.reloadAllTimelines();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface WidgetPreferences {
|
||||
updateFrequency: 'hourly' | 'daily' | 'manual';
|
||||
categories: string[];
|
||||
theme: 'light' | 'dark' | 'auto';
|
||||
fontSize: 'small' | 'medium' | 'large';
|
||||
updateFrequency: 'hourly' | 'daily' | 'manual';
|
||||
categories: string[];
|
||||
theme: 'light' | 'dark' | 'auto';
|
||||
fontSize: 'small' | 'medium' | 'large';
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -426,34 +414,32 @@ import { WidgetDataManager } from './widgetDataManager';
|
|||
|
||||
// In Ihrem bestehenden Store
|
||||
const useQuotesStore = create<QuotesState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// ... existing state ...
|
||||
|
||||
toggleFavorite: async (quoteId: string) => {
|
||||
set((state) => {
|
||||
const newFavorites = state.favorites.includes(quoteId)
|
||||
? state.favorites.filter(id => id !== quoteId)
|
||||
: [...state.favorites, quoteId];
|
||||
|
||||
// Widget mit aktualisierten Favoriten updaten
|
||||
const favoriteQuotes = state.quotes.filter(q =>
|
||||
newFavorites.includes(q.id)
|
||||
);
|
||||
WidgetDataManager.saveQuotesToWidget(favoriteQuotes);
|
||||
|
||||
return { favorites: newFavorites };
|
||||
});
|
||||
},
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// ... existing state ...
|
||||
|
||||
setDailyQuote: async (quote: Quote) => {
|
||||
// Daily Quote im Widget speichern
|
||||
await WidgetDataManager.saveDailyQuote(quote);
|
||||
set({ dailyQuote: quote });
|
||||
},
|
||||
}),
|
||||
// ... persist config ...
|
||||
)
|
||||
toggleFavorite: async (quoteId: string) => {
|
||||
set((state) => {
|
||||
const newFavorites = state.favorites.includes(quoteId)
|
||||
? state.favorites.filter((id) => id !== quoteId)
|
||||
: [...state.favorites, quoteId];
|
||||
|
||||
// Widget mit aktualisierten Favoriten updaten
|
||||
const favoriteQuotes = state.quotes.filter((q) => newFavorites.includes(q.id));
|
||||
WidgetDataManager.saveQuotesToWidget(favoriteQuotes);
|
||||
|
||||
return { favorites: newFavorites };
|
||||
});
|
||||
},
|
||||
|
||||
setDailyQuote: async (quote: Quote) => {
|
||||
// Daily Quote im Widget speichern
|
||||
await WidgetDataManager.saveDailyQuote(quote);
|
||||
set({ dailyQuote: quote });
|
||||
},
|
||||
})
|
||||
// ... persist config ...
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
|
|
@ -469,21 +455,21 @@ import WidgetKit
|
|||
|
||||
@objc(WidgetKitModule)
|
||||
class WidgetKitModule: NSObject {
|
||||
|
||||
|
||||
@objc
|
||||
func reloadAllTimelines() {
|
||||
if #available(iOS 14.0, *) {
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc
|
||||
func reloadTimelines(_ widgetKind: String) {
|
||||
if #available(iOS 14.0, *) {
|
||||
WidgetCenter.shared.reloadTimelines(ofKind: widgetKind)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc
|
||||
static func requiresMainQueueSetup() -> Bool {
|
||||
return false
|
||||
|
|
@ -509,6 +495,7 @@ RCT_EXTERN_METHOD(reloadTimelines:(NSString *)widgetKind)
|
|||
### 1. Widget Sizes und Layouts
|
||||
|
||||
iOS unterstützt verschiedene Widget-Größen:
|
||||
|
||||
- **Small**: 2x2 Grid (minimaler Inhalt)
|
||||
- **Medium**: 4x2 Grid (mehr Details)
|
||||
- **Large**: 4x4 Grid (vollständiger Inhalt)
|
||||
|
|
@ -520,7 +507,7 @@ iOS unterstützt verschiedene Widget-Größen:
|
|||
// In QuoteProvider
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||
let updatePolicy: TimelineReloadPolicy
|
||||
|
||||
|
||||
// Update-Frequenz basierend auf User Preferences
|
||||
if let updateFrequency = userDefaults?.string(forKey: "updateFrequency") {
|
||||
switch updateFrequency {
|
||||
|
|
@ -534,7 +521,7 @@ func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) ->
|
|||
} else {
|
||||
updatePolicy = .atEnd
|
||||
}
|
||||
|
||||
|
||||
let timeline = Timeline(entries: entries, policy: updatePolicy)
|
||||
completion(timeline)
|
||||
}
|
||||
|
|
@ -548,7 +535,7 @@ Widget-Taps können die App mit spezifischen Inhalten öffnen:
|
|||
// Widget View mit Link
|
||||
struct QuoteWidgetView: View {
|
||||
var entry: QuoteEntry
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
// Widget Content
|
||||
|
|
@ -565,20 +552,20 @@ import { Linking } from 'react-native';
|
|||
|
||||
// In App.tsx
|
||||
useEffect(() => {
|
||||
const handleDeepLink = (url: string) => {
|
||||
const route = url.replace('quoteapp://', '');
|
||||
if (route.startsWith('quote/')) {
|
||||
const quoteId = route.replace('quote/', '');
|
||||
// Navigate to quote details
|
||||
navigation.navigate('QuoteDetail', { quoteId });
|
||||
}
|
||||
};
|
||||
const handleDeepLink = (url: string) => {
|
||||
const route = url.replace('quoteapp://', '');
|
||||
if (route.startsWith('quote/')) {
|
||||
const quoteId = route.replace('quote/', '');
|
||||
// Navigate to quote details
|
||||
navigation.navigate('QuoteDetail', { quoteId });
|
||||
}
|
||||
};
|
||||
|
||||
const subscription = Linking.addEventListener('url', ({ url }) => {
|
||||
handleDeepLink(url);
|
||||
});
|
||||
const subscription = Linking.addEventListener('url', ({ url }) => {
|
||||
handleDeepLink(url);
|
||||
});
|
||||
|
||||
return () => subscription.remove();
|
||||
return () => subscription.remove();
|
||||
}, []);
|
||||
```
|
||||
|
||||
|
|
@ -594,7 +581,7 @@ struct QuoteLiveActivityAttributes: ActivityAttributes {
|
|||
var author: String
|
||||
var expiresAt: Date
|
||||
}
|
||||
|
||||
|
||||
var category: String
|
||||
}
|
||||
|
||||
|
|
@ -624,6 +611,7 @@ npx expo run:ios
|
|||
### 2. Debug Console Logs
|
||||
|
||||
In widget.swift:
|
||||
|
||||
```swift
|
||||
import os.log
|
||||
|
||||
|
|
@ -648,7 +636,7 @@ struct QuotesWidget_Previews: PreviewProvider {
|
|||
category: "Preview"
|
||||
))
|
||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
||||
|
||||
|
||||
QuotesWidgetView(entry: QuoteEntry(
|
||||
date: Date(),
|
||||
quote: "Preview Quote",
|
||||
|
|
@ -666,6 +654,7 @@ struct QuotesWidget_Previews: PreviewProvider {
|
|||
### Problem 1: Widget zeigt keine Daten
|
||||
|
||||
**Lösung:**
|
||||
|
||||
1. Überprüfen Sie App Group Configuration
|
||||
2. Stellen Sie sicher, dass beide Targets dieselbe App Group verwenden
|
||||
3. Prüfen Sie UserDefaults Suite Name
|
||||
|
|
@ -673,20 +662,22 @@ struct QuotesWidget_Previews: PreviewProvider {
|
|||
### Problem 2: Widget Updates nicht
|
||||
|
||||
**Lösung:**
|
||||
|
||||
```typescript
|
||||
// Force Widget Reload
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
const forceWidgetUpdate = () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
NativeModules.WidgetKit?.reloadAllTimelines();
|
||||
}
|
||||
if (Platform.OS === 'ios') {
|
||||
NativeModules.WidgetKit?.reloadAllTimelines();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Problem 3: Build Fehler nach Widget Addition
|
||||
|
||||
**Lösung:**
|
||||
|
||||
```bash
|
||||
# Clean Build
|
||||
cd ios
|
||||
|
|
@ -699,6 +690,7 @@ npx expo run:ios --clear
|
|||
### Problem 4: Widget erscheint nicht in Widget Gallery
|
||||
|
||||
**Lösung:**
|
||||
|
||||
- Minimum Deployment Target prüfen (iOS 14.0+)
|
||||
- Info.plist Einträge verifizieren
|
||||
- Bundle Identifier Format überprüfen
|
||||
|
|
@ -723,17 +715,17 @@ func prepareImageForWidget(_ image: UIImage, size: CGSize) -> UIImage? {
|
|||
```typescript
|
||||
// Efficient Data Storage
|
||||
class WidgetCache {
|
||||
static async cacheQuotes(quotes: Quote[]) {
|
||||
// Nur die nötigen Felder speichern
|
||||
const minimalQuotes = quotes.map(q => ({
|
||||
id: q.id,
|
||||
quote: q.quote.substring(0, 200), // Limit text length
|
||||
author: q.author,
|
||||
category: q.category
|
||||
}));
|
||||
|
||||
await WidgetDataManager.saveQuotesToWidget(minimalQuotes);
|
||||
}
|
||||
static async cacheQuotes(quotes: Quote[]) {
|
||||
// Nur die nötigen Felder speichern
|
||||
const minimalQuotes = quotes.map((q) => ({
|
||||
id: q.id,
|
||||
quote: q.quote.substring(0, 200), // Limit text length
|
||||
author: q.author,
|
||||
category: q.category,
|
||||
}));
|
||||
|
||||
await WidgetDataManager.saveQuotesToWidget(minimalQuotes);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -743,7 +735,7 @@ class WidgetCache {
|
|||
// Intelligente Timeline Generation
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||
let entries: [QuoteEntry]
|
||||
|
||||
|
||||
if context.isPreview {
|
||||
// Minimal entries for preview
|
||||
entries = [getPlaceholderEntry()]
|
||||
|
|
@ -754,7 +746,7 @@ func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) ->
|
|||
// Standard timeline
|
||||
entries = generateEntries(count: 24)
|
||||
}
|
||||
|
||||
|
||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
||||
completion(timeline)
|
||||
}
|
||||
|
|
@ -789,16 +781,19 @@ func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) ->
|
|||
## Ressourcen und Weiterführende Links
|
||||
|
||||
### Offizielle Dokumentation
|
||||
|
||||
- [Apple WidgetKit Documentation](https://developer.apple.com/documentation/widgetkit)
|
||||
- [Expo Config Plugins](https://docs.expo.dev/config-plugins/introduction/)
|
||||
- [React Native Share Extension](https://github.com/alinz/react-native-share-extension)
|
||||
|
||||
### Community Resources
|
||||
|
||||
- [Evan Bacon's Apple Targets Plugin](https://github.com/EvanBacon/expo-apple-targets)
|
||||
- [EAS Widget Example](https://github.com/gaishimo/eas-widget-example)
|
||||
- [React Native Widget Bridge](https://github.com/fasky-software/react-native-widget-bridge)
|
||||
|
||||
### Tutorials und Beispiele
|
||||
|
||||
- [SwiftUI Widget Tutorial](https://www.hackingwithswift.com/books/ios-swiftui/introduction-to-widgetkit)
|
||||
- [Expo Managed Workflow Widgets](https://www.peterarontoth.com/posts/interactive-widgets-in-expo-managed-workflows)
|
||||
|
||||
|
|
@ -813,4 +808,4 @@ Die Implementation von iOS Widgets in einer Expo React Native App erfordert:
|
|||
5. **UserDefaults/SharedGroupPreferences** für Kommunikation
|
||||
6. **Timeline Provider** für Content Updates
|
||||
|
||||
Mit dieser Anleitung sollten Sie in der Lage sein, funktionale und ansprechende Widgets für Ihre Quotes App zu erstellen, die nahtlos mit Ihrer React Native App kommunizieren.
|
||||
Mit dieser Anleitung sollten Sie in der Lage sein, funktionale und ansprechende Widgets für Ihre Quotes App zu erstellen, die nahtlos mit Ihrer React Native App kommunizieren.
|
||||
|
|
|
|||
|
|
@ -70,26 +70,29 @@ npm install react-native-shared-group-preferences
|
|||
**Beschreibung:** Zeigt das zuletzt erstellte Memo mit Titel, Datum und Quick Actions.
|
||||
|
||||
**Größen:**
|
||||
|
||||
- **Small (2x2):** Nur Titel + Datum
|
||||
- **Medium (4x2):** Titel + Intro + Datum + Space Badge
|
||||
- **Large (4x4):** Titel + Vollständiger Intro + Transcript Preview + Metadata
|
||||
|
||||
**Daten:**
|
||||
|
||||
```typescript
|
||||
interface LatestMemoWidget {
|
||||
memoId: string;
|
||||
title: string;
|
||||
intro?: string;
|
||||
transcriptPreview?: string; // First 200 chars
|
||||
createdAt: Date;
|
||||
spaceName?: string;
|
||||
spaceColor?: string;
|
||||
audioLength?: number;
|
||||
hasTranscript: boolean;
|
||||
memoId: string;
|
||||
title: string;
|
||||
intro?: string;
|
||||
transcriptPreview?: string; // First 200 chars
|
||||
createdAt: Date;
|
||||
spaceName?: string;
|
||||
spaceColor?: string;
|
||||
audioLength?: number;
|
||||
hasTranscript: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**Deep Links:**
|
||||
|
||||
- Widget Tap → Öffnet Memo Detail (`memoro://memo/{id}`)
|
||||
- Space Badge → Öffnet Space (`memoro://space/{id}`)
|
||||
|
||||
|
|
@ -100,26 +103,29 @@ interface LatestMemoWidget {
|
|||
**Beschreibung:** Grid-Layout mit gepinnten Memos für schnellen Zugriff.
|
||||
|
||||
**Größen:**
|
||||
|
||||
- **Small (2x2):** 1 Memo (Featured)
|
||||
- **Medium (4x2):** 2-3 Memos (Horizontal Stack)
|
||||
- **Large (4x4):** 4-6 Memos (Grid Layout)
|
||||
|
||||
**Daten:**
|
||||
|
||||
```typescript
|
||||
interface PinnedMemosWidget {
|
||||
memos: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
snippet: string; // First 50 chars
|
||||
isPinned: boolean;
|
||||
spaceColor?: string;
|
||||
createdAt: Date;
|
||||
}>;
|
||||
totalCount: number;
|
||||
memos: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
snippet: string; // First 50 chars
|
||||
isPinned: boolean;
|
||||
spaceColor?: string;
|
||||
createdAt: Date;
|
||||
}>;
|
||||
totalCount: number;
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Tappable Cards → Öffnet jeweiliges Memo
|
||||
- Empty State → "No pinned memos" mit Link zu App
|
||||
- Sortierung nach Pin-Datum (neueste zuerst)
|
||||
|
|
@ -131,23 +137,27 @@ interface PinnedMemosWidget {
|
|||
**Beschreibung:** One-Tap Recording Start mit minimalistischem Design.
|
||||
|
||||
**Größen:**
|
||||
|
||||
- **Small (2x2):** Großer Aufnahme-Button + Mikrofon Icon
|
||||
- ~~Medium~~ (nicht sinnvoll für diesen Type)
|
||||
- ~~Large~~ (nicht sinnvoll für diesen Type)
|
||||
|
||||
**Daten:**
|
||||
|
||||
```typescript
|
||||
interface QuickRecordWidget {
|
||||
selectedSpaceId?: string;
|
||||
selectedBlueprintId?: string;
|
||||
recordingLanguages?: string[];
|
||||
selectedSpaceId?: string;
|
||||
selectedBlueprintId?: string;
|
||||
recordingLanguages?: string[];
|
||||
}
|
||||
```
|
||||
|
||||
**Deep Links:**
|
||||
|
||||
- Widget Tap → Öffnet Recording Screen mit vorausgewählten Settings
|
||||
|
||||
**Visual Design:**
|
||||
|
||||
- Großes Mikrofon-Icon (SF Symbol)
|
||||
- Gradient Background (Theme Colors)
|
||||
- Text: "Start Recording" oder "Aufnehmen"
|
||||
|
|
@ -160,31 +170,34 @@ interface QuickRecordWidget {
|
|||
**Beschreibung:** Übersicht über Nutzungsstatistiken und Achievements.
|
||||
|
||||
**Größen:**
|
||||
|
||||
- **Small (2x2):** Memo Count + Trend Icon
|
||||
- **Medium (4x2):** Count + Duration + Last Week Stats
|
||||
- **Large (4x4):** Detailed Stats + Chart + Breakdown by Space
|
||||
|
||||
**Daten:**
|
||||
|
||||
```typescript
|
||||
interface StatisticsWidget {
|
||||
totalMemos: number;
|
||||
totalDuration: number; // seconds
|
||||
thisWeekCount: number;
|
||||
thisMonthCount: number;
|
||||
favoriteCount: number;
|
||||
spaceBreakdown?: Array<{
|
||||
spaceName: string;
|
||||
count: number;
|
||||
color: string;
|
||||
}>;
|
||||
longestMemo?: {
|
||||
title: string;
|
||||
duration: number;
|
||||
};
|
||||
totalMemos: number;
|
||||
totalDuration: number; // seconds
|
||||
thisWeekCount: number;
|
||||
thisMonthCount: number;
|
||||
favoriteCount: number;
|
||||
spaceBreakdown?: Array<{
|
||||
spaceName: string;
|
||||
count: number;
|
||||
color: string;
|
||||
}>;
|
||||
longestMemo?: {
|
||||
title: string;
|
||||
duration: number;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Trend Indicators (↑↓)
|
||||
- Weekly Progress Bar
|
||||
- Tap → Öffnet Statistics Screen (neu zu bauen)
|
||||
|
|
@ -200,16 +213,17 @@ interface StatisticsWidget {
|
|||
**Update Frequency:** Täglich um Mitternacht
|
||||
|
||||
**Daten:**
|
||||
|
||||
```typescript
|
||||
interface InspirationWidget {
|
||||
randomMemo: {
|
||||
id: string;
|
||||
title: string;
|
||||
snippet: string;
|
||||
createdAt: Date;
|
||||
tags?: string[];
|
||||
};
|
||||
refreshDate: Date;
|
||||
randomMemo: {
|
||||
id: string;
|
||||
title: string;
|
||||
snippet: string;
|
||||
createdAt: Date;
|
||||
tags?: string[];
|
||||
};
|
||||
refreshDate: Date;
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -318,6 +332,7 @@ interface InspirationWidget {
|
|||
**Ziel:** Live Activities für aktive Aufnahmen
|
||||
|
||||
**Features:**
|
||||
|
||||
- Live recording duration
|
||||
- Pause/Resume controls
|
||||
- Waveform visualization
|
||||
|
|
@ -416,26 +431,24 @@ memoro/
|
|||
|
||||
```json
|
||||
{
|
||||
"expo": {
|
||||
"ios": {
|
||||
"bundleIdentifier": "com.memo.beta",
|
||||
"appleTeamId": "ZB76J8YWG6",
|
||||
"entitlements": {
|
||||
"com.apple.security.application-groups": [
|
||||
"group.com.memo.beta.widget"
|
||||
]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
// ... existing plugins ...
|
||||
[
|
||||
"@bacons/apple-targets",
|
||||
{
|
||||
"appleTeamId": "ZB76J8YWG6"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
"expo": {
|
||||
"ios": {
|
||||
"bundleIdentifier": "com.memo.beta",
|
||||
"appleTeamId": "ZB76J8YWG6",
|
||||
"entitlements": {
|
||||
"com.apple.security.application-groups": ["group.com.memo.beta.widget"]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
// ... existing plugins ...
|
||||
[
|
||||
"@bacons/apple-targets",
|
||||
{
|
||||
"appleTeamId": "ZB76J8YWG6"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -444,30 +457,25 @@ memoro/
|
|||
```javascript
|
||||
/** @type {import('@bacons/apple-targets/app.plugin').Config} */
|
||||
module.exports = {
|
||||
type: "widget",
|
||||
name: "MemoroWidget",
|
||||
bundleIdentifier: "$(PRODUCT_BUNDLE_IDENTIFIER).widget",
|
||||
deploymentTarget: "16.0",
|
||||
icon: "../../assets/widgets/widget-icon.png",
|
||||
colors: {
|
||||
$accent: {
|
||||
color: "#007AFF", // iOS Blue
|
||||
darkColor: "#0A84FF" // iOS Blue Dark
|
||||
},
|
||||
$widgetBackground: {
|
||||
color: "#FFFFFF",
|
||||
darkColor: "#1C1C1E"
|
||||
}
|
||||
},
|
||||
entitlements: {
|
||||
"com.apple.security.application-groups": [
|
||||
"group.com.memo.beta.widget"
|
||||
]
|
||||
},
|
||||
frameworks: [
|
||||
"SwiftUI",
|
||||
"WidgetKit"
|
||||
]
|
||||
type: 'widget',
|
||||
name: 'MemoroWidget',
|
||||
bundleIdentifier: '$(PRODUCT_BUNDLE_IDENTIFIER).widget',
|
||||
deploymentTarget: '16.0',
|
||||
icon: '../../assets/widgets/widget-icon.png',
|
||||
colors: {
|
||||
$accent: {
|
||||
color: '#007AFF', // iOS Blue
|
||||
darkColor: '#0A84FF', // iOS Blue Dark
|
||||
},
|
||||
$widgetBackground: {
|
||||
color: '#FFFFFF',
|
||||
darkColor: '#1C1C1E',
|
||||
},
|
||||
},
|
||||
entitlements: {
|
||||
'com.apple.security.application-groups': ['group.com.memo.beta.widget'],
|
||||
},
|
||||
frameworks: ['SwiftUI', 'WidgetKit'],
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -486,125 +494,125 @@ import { NativeModules, Platform } from 'react-native';
|
|||
const APP_GROUP = 'group.com.memo.beta.widget';
|
||||
|
||||
export interface WidgetMemoData {
|
||||
id: string;
|
||||
title: string;
|
||||
intro?: string;
|
||||
transcriptPreview?: string;
|
||||
createdAt: string; // ISO format
|
||||
spaceName?: string;
|
||||
spaceColor?: string;
|
||||
audioLength?: number;
|
||||
hasTranscript: boolean;
|
||||
id: string;
|
||||
title: string;
|
||||
intro?: string;
|
||||
transcriptPreview?: string;
|
||||
createdAt: string; // ISO format
|
||||
spaceName?: string;
|
||||
spaceColor?: string;
|
||||
audioLength?: number;
|
||||
hasTranscript: boolean;
|
||||
}
|
||||
|
||||
export interface WidgetPinnedMemo {
|
||||
id: string;
|
||||
title: string;
|
||||
snippet: string;
|
||||
isPinned: boolean;
|
||||
spaceColor?: string;
|
||||
createdAt: string;
|
||||
id: string;
|
||||
title: string;
|
||||
snippet: string;
|
||||
isPinned: boolean;
|
||||
spaceColor?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface WidgetStatistics {
|
||||
totalMemos: number;
|
||||
totalDuration: number;
|
||||
thisWeekCount: number;
|
||||
thisMonthCount: number;
|
||||
favoriteCount: number;
|
||||
spaceBreakdown?: Array<{
|
||||
spaceName: string;
|
||||
count: number;
|
||||
color: string;
|
||||
}>;
|
||||
totalMemos: number;
|
||||
totalDuration: number;
|
||||
thisWeekCount: number;
|
||||
thisMonthCount: number;
|
||||
favoriteCount: number;
|
||||
spaceBreakdown?: Array<{
|
||||
spaceName: string;
|
||||
count: number;
|
||||
color: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export class WidgetDataManager {
|
||||
/**
|
||||
* Update latest memo data for widget
|
||||
*/
|
||||
static async updateLatestMemo(memo: WidgetMemoData): Promise<void> {
|
||||
try {
|
||||
const data = JSON.stringify(memo);
|
||||
await SharedGroupPreferences.setItem('latestMemo', data, APP_GROUP);
|
||||
this.refreshWidget();
|
||||
} catch (error) {
|
||||
console.error('[WidgetDataManager] Failed to update latest memo:', error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Update latest memo data for widget
|
||||
*/
|
||||
static async updateLatestMemo(memo: WidgetMemoData): Promise<void> {
|
||||
try {
|
||||
const data = JSON.stringify(memo);
|
||||
await SharedGroupPreferences.setItem('latestMemo', data, APP_GROUP);
|
||||
this.refreshWidget();
|
||||
} catch (error) {
|
||||
console.error('[WidgetDataManager] Failed to update latest memo:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update pinned memos for widget
|
||||
*/
|
||||
static async updatePinnedMemos(memos: WidgetPinnedMemo[]): Promise<void> {
|
||||
try {
|
||||
// Only send top 6 pinned memos (for large widget)
|
||||
const limitedMemos = memos.slice(0, 6);
|
||||
const data = JSON.stringify(limitedMemos);
|
||||
await SharedGroupPreferences.setItem('pinnedMemos', data, APP_GROUP);
|
||||
this.refreshWidget();
|
||||
} catch (error) {
|
||||
console.error('[WidgetDataManager] Failed to update pinned memos:', error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Update pinned memos for widget
|
||||
*/
|
||||
static async updatePinnedMemos(memos: WidgetPinnedMemo[]): Promise<void> {
|
||||
try {
|
||||
// Only send top 6 pinned memos (for large widget)
|
||||
const limitedMemos = memos.slice(0, 6);
|
||||
const data = JSON.stringify(limitedMemos);
|
||||
await SharedGroupPreferences.setItem('pinnedMemos', data, APP_GROUP);
|
||||
this.refreshWidget();
|
||||
} catch (error) {
|
||||
console.error('[WidgetDataManager] Failed to update pinned memos:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update statistics for widget
|
||||
*/
|
||||
static async updateStatistics(stats: WidgetStatistics): Promise<void> {
|
||||
try {
|
||||
const data = JSON.stringify(stats);
|
||||
await SharedGroupPreferences.setItem('statistics', data, APP_GROUP);
|
||||
this.refreshWidget();
|
||||
} catch (error) {
|
||||
console.error('[WidgetDataManager] Failed to update statistics:', error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Update statistics for widget
|
||||
*/
|
||||
static async updateStatistics(stats: WidgetStatistics): Promise<void> {
|
||||
try {
|
||||
const data = JSON.stringify(stats);
|
||||
await SharedGroupPreferences.setItem('statistics', data, APP_GROUP);
|
||||
this.refreshWidget();
|
||||
} catch (error) {
|
||||
console.error('[WidgetDataManager] Failed to update statistics:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update recording preferences for Quick Record widget
|
||||
*/
|
||||
static async updateRecordingPreferences(preferences: {
|
||||
selectedSpaceId?: string;
|
||||
selectedBlueprintId?: string;
|
||||
recordingLanguages?: string[];
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const data = JSON.stringify(preferences);
|
||||
await SharedGroupPreferences.setItem('recordingPreferences', data, APP_GROUP);
|
||||
this.refreshWidget();
|
||||
} catch (error) {
|
||||
console.error('[WidgetDataManager] Failed to update recording preferences:', error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Update recording preferences for Quick Record widget
|
||||
*/
|
||||
static async updateRecordingPreferences(preferences: {
|
||||
selectedSpaceId?: string;
|
||||
selectedBlueprintId?: string;
|
||||
recordingLanguages?: string[];
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const data = JSON.stringify(preferences);
|
||||
await SharedGroupPreferences.setItem('recordingPreferences', data, APP_GROUP);
|
||||
this.refreshWidget();
|
||||
} catch (error) {
|
||||
console.error('[WidgetDataManager] Failed to update recording preferences:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger widget refresh (iOS 14+)
|
||||
*/
|
||||
static refreshWidget(): void {
|
||||
if (Platform.OS === 'ios') {
|
||||
try {
|
||||
NativeModules.WidgetKit?.reloadAllTimelines();
|
||||
} catch (error) {
|
||||
console.warn('[WidgetDataManager] Could not reload widget timelines:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Trigger widget refresh (iOS 14+)
|
||||
*/
|
||||
static refreshWidget(): void {
|
||||
if (Platform.OS === 'ios') {
|
||||
try {
|
||||
NativeModules.WidgetKit?.reloadAllTimelines();
|
||||
} catch (error) {
|
||||
console.warn('[WidgetDataManager] Could not reload widget timelines:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all widget data
|
||||
*/
|
||||
static async clearWidgetData(): Promise<void> {
|
||||
try {
|
||||
await SharedGroupPreferences.setItem('latestMemo', '', APP_GROUP);
|
||||
await SharedGroupPreferences.setItem('pinnedMemos', '', APP_GROUP);
|
||||
await SharedGroupPreferences.setItem('statistics', '', APP_GROUP);
|
||||
await SharedGroupPreferences.setItem('recordingPreferences', '', APP_GROUP);
|
||||
this.refreshWidget();
|
||||
} catch (error) {
|
||||
console.error('[WidgetDataManager] Failed to clear widget data:', error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Clear all widget data
|
||||
*/
|
||||
static async clearWidgetData(): Promise<void> {
|
||||
try {
|
||||
await SharedGroupPreferences.setItem('latestMemo', '', APP_GROUP);
|
||||
await SharedGroupPreferences.setItem('pinnedMemos', '', APP_GROUP);
|
||||
await SharedGroupPreferences.setItem('statistics', '', APP_GROUP);
|
||||
await SharedGroupPreferences.setItem('recordingPreferences', '', APP_GROUP);
|
||||
this.refreshWidget();
|
||||
} catch (error) {
|
||||
console.error('[WidgetDataManager] Failed to clear widget data:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -621,41 +629,41 @@ import { WidgetDataManager } from '~/features/widgets/services/widgetDataManager
|
|||
|
||||
// In setLatestMemo action:
|
||||
setLatestMemo: (memo: Memo | null) => {
|
||||
set({ latestMemo: memo });
|
||||
set({ latestMemo: memo });
|
||||
|
||||
// Update widget data
|
||||
if (memo) {
|
||||
WidgetDataManager.updateLatestMemo({
|
||||
id: memo.id,
|
||||
title: memo.title,
|
||||
intro: memo.metadata?.intro?.substring(0, 200),
|
||||
transcriptPreview: memo.source?.transcript?.substring(0, 200),
|
||||
createdAt: memo.created_at || new Date().toISOString(),
|
||||
spaceName: memo.space?.name,
|
||||
spaceColor: memo.space?.color,
|
||||
audioLength: memo.source?.duration_seconds,
|
||||
hasTranscript: !!memo.source?.transcript,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Update widget data
|
||||
if (memo) {
|
||||
WidgetDataManager.updateLatestMemo({
|
||||
id: memo.id,
|
||||
title: memo.title,
|
||||
intro: memo.metadata?.intro?.substring(0, 200),
|
||||
transcriptPreview: memo.source?.transcript?.substring(0, 200),
|
||||
createdAt: memo.created_at || new Date().toISOString(),
|
||||
spaceName: memo.space?.name,
|
||||
spaceColor: memo.space?.color,
|
||||
audioLength: memo.source?.duration_seconds,
|
||||
hasTranscript: !!memo.source?.transcript,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// In togglePin action:
|
||||
togglePin: async (memoId: string) => {
|
||||
// ... existing pin logic ...
|
||||
// ... existing pin logic ...
|
||||
|
||||
// Update widget with pinned memos
|
||||
const pinnedMemos = state.memos.filter(m => m.is_pinned);
|
||||
WidgetDataManager.updatePinnedMemos(
|
||||
pinnedMemos.map(m => ({
|
||||
id: m.id,
|
||||
title: m.title,
|
||||
snippet: m.source?.transcript?.substring(0, 50) || '',
|
||||
isPinned: true,
|
||||
spaceColor: m.space?.color,
|
||||
createdAt: m.created_at || new Date().toISOString(),
|
||||
}))
|
||||
);
|
||||
}
|
||||
// Update widget with pinned memos
|
||||
const pinnedMemos = state.memos.filter((m) => m.is_pinned);
|
||||
WidgetDataManager.updatePinnedMemos(
|
||||
pinnedMemos.map((m) => ({
|
||||
id: m.id,
|
||||
title: m.title,
|
||||
snippet: m.source?.transcript?.substring(0, 50) || '',
|
||||
isPinned: true,
|
||||
spaceColor: m.space?.color,
|
||||
createdAt: m.created_at || new Date().toISOString(),
|
||||
}))
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -973,6 +981,7 @@ extension Color {
|
|||
**Format:** `memoro://[screen]/[id]`
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `memoro://memo/abc-123` → Opens memo detail
|
||||
- `memoro://space/xyz-789` → Opens space view
|
||||
- `memoro://record` → Opens recording screen
|
||||
|
|
@ -987,38 +996,38 @@ import { Linking } from 'react-native';
|
|||
import { useRouter } from 'expo-router';
|
||||
|
||||
export default function RootLayout() {
|
||||
const router = useRouter();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// Handle deep links from widgets
|
||||
const handleDeepLink = (url: string) => {
|
||||
const route = url.replace('memoro://', '');
|
||||
useEffect(() => {
|
||||
// Handle deep links from widgets
|
||||
const handleDeepLink = (url: string) => {
|
||||
const route = url.replace('memoro://', '');
|
||||
|
||||
if (route.startsWith('memo/')) {
|
||||
const memoId = route.replace('memo/', '');
|
||||
router.push(`/(protected)/(memo)/${memoId}`);
|
||||
} else if (route.startsWith('space/')) {
|
||||
const spaceId = route.replace('space/', '');
|
||||
router.push(`/(protected)/(space)/${spaceId}`);
|
||||
} else if (route === 'record') {
|
||||
router.push('/(protected)/(tabs)/');
|
||||
}
|
||||
};
|
||||
if (route.startsWith('memo/')) {
|
||||
const memoId = route.replace('memo/', '');
|
||||
router.push(`/(protected)/(memo)/${memoId}`);
|
||||
} else if (route.startsWith('space/')) {
|
||||
const spaceId = route.replace('space/', '');
|
||||
router.push(`/(protected)/(space)/${spaceId}`);
|
||||
} else if (route === 'record') {
|
||||
router.push('/(protected)/(tabs)/');
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for deep links
|
||||
const subscription = Linking.addEventListener('url', ({ url }) => {
|
||||
handleDeepLink(url);
|
||||
});
|
||||
// Listen for deep links
|
||||
const subscription = Linking.addEventListener('url', ({ url }) => {
|
||||
handleDeepLink(url);
|
||||
});
|
||||
|
||||
// Handle initial URL if app was closed
|
||||
Linking.getInitialURL().then((url) => {
|
||||
if (url) handleDeepLink(url);
|
||||
});
|
||||
// Handle initial URL if app was closed
|
||||
Linking.getInitialURL().then((url) => {
|
||||
if (url) handleDeepLink(url);
|
||||
});
|
||||
|
||||
return () => subscription.remove();
|
||||
}, [router]);
|
||||
return () => subscription.remove();
|
||||
}, [router]);
|
||||
|
||||
// ... rest of layout
|
||||
// ... rest of layout
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -1034,31 +1043,31 @@ export default function RootLayout() {
|
|||
import { WidgetDataManager } from '../widgetDataManager';
|
||||
|
||||
describe('WidgetDataManager', () => {
|
||||
it('should update latest memo data', async () => {
|
||||
const mockMemo = {
|
||||
id: 'test-123',
|
||||
title: 'Test Memo',
|
||||
intro: 'Test intro',
|
||||
createdAt: new Date().toISOString(),
|
||||
hasTranscript: true,
|
||||
};
|
||||
it('should update latest memo data', async () => {
|
||||
const mockMemo = {
|
||||
id: 'test-123',
|
||||
title: 'Test Memo',
|
||||
intro: 'Test intro',
|
||||
createdAt: new Date().toISOString(),
|
||||
hasTranscript: true,
|
||||
};
|
||||
|
||||
await WidgetDataManager.updateLatestMemo(mockMemo);
|
||||
// Assert SharedGroupPreferences was called
|
||||
});
|
||||
await WidgetDataManager.updateLatestMemo(mockMemo);
|
||||
// Assert SharedGroupPreferences was called
|
||||
});
|
||||
|
||||
it('should limit pinned memos to 6', async () => {
|
||||
const manyMemos = Array.from({ length: 10 }, (_, i) => ({
|
||||
id: `memo-${i}`,
|
||||
title: `Memo ${i}`,
|
||||
snippet: 'snippet',
|
||||
isPinned: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
}));
|
||||
it('should limit pinned memos to 6', async () => {
|
||||
const manyMemos = Array.from({ length: 10 }, (_, i) => ({
|
||||
id: `memo-${i}`,
|
||||
title: `Memo ${i}`,
|
||||
snippet: 'snippet',
|
||||
isPinned: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
await WidgetDataManager.updatePinnedMemos(manyMemos);
|
||||
// Assert only 6 memos were saved
|
||||
});
|
||||
await WidgetDataManager.updatePinnedMemos(manyMemos);
|
||||
// Assert only 6 memos were saved
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -1118,18 +1127,21 @@ describe('WidgetDataManager', () => {
|
|||
### Translation Keys
|
||||
|
||||
**Widget Titles:**
|
||||
|
||||
- `widget.latest_memo.title` = "Letztes Memo" / "Latest Memo"
|
||||
- `widget.pinned_memos.title` = "Gepinnte Memos" / "Pinned Memos"
|
||||
- `widget.quick_record.title` = "Aufnahme" / "Record"
|
||||
- `widget.statistics.title` = "Statistiken" / "Statistics"
|
||||
|
||||
**Widget Descriptions:**
|
||||
|
||||
- `widget.latest_memo.description` = "Zeigt dein zuletzt erstelltes Memo"
|
||||
- `widget.pinned_memos.description` = "Schnellzugriff auf gepinnte Memos"
|
||||
- `widget.quick_record.description` = "Starte eine Aufnahme mit einem Tap"
|
||||
- `widget.statistics.description` = "Übersicht deiner Nutzungsstatistiken"
|
||||
|
||||
**UI Text:**
|
||||
|
||||
- `widget.common.no_memos` = "Noch keine Memos" / "No memos yet"
|
||||
- `widget.common.today` = "Heute" / "Today"
|
||||
- `widget.common.yesterday` = "Gestern" / "Yesterday"
|
||||
|
|
@ -1141,6 +1153,7 @@ describe('WidgetDataManager', () => {
|
|||
### Beta Testing (2 Wochen)
|
||||
|
||||
**Phase 1:**
|
||||
|
||||
- Internal team testing (5 developers)
|
||||
- TestFlight beta with 10 users
|
||||
- Collect feedback on:
|
||||
|
|
@ -1150,6 +1163,7 @@ describe('WidgetDataManager', () => {
|
|||
- Performance
|
||||
|
||||
**Phase 2:**
|
||||
|
||||
- Extended TestFlight beta (50 users)
|
||||
- Monitor analytics:
|
||||
- Widget add rate
|
||||
|
|
@ -1160,6 +1174,7 @@ describe('WidgetDataManager', () => {
|
|||
### Production Release
|
||||
|
||||
**Criteria for Launch:**
|
||||
|
||||
- ✅ All widgets functional in 3 sizes
|
||||
- ✅ Deep links working 100%
|
||||
- ✅ No memory leaks
|
||||
|
|
@ -1168,6 +1183,7 @@ describe('WidgetDataManager', () => {
|
|||
- ✅ Documentation complete
|
||||
|
||||
**Release Notes:**
|
||||
|
||||
```
|
||||
🎉 Neu: iOS Home Screen Widgets!
|
||||
|
||||
|
|
@ -1212,22 +1228,22 @@ Tippe auf ein Widget, um direkt zur App zu springen!
|
|||
|
||||
### Technical Risks
|
||||
|
||||
| Risk | Impact | Probability | Mitigation |
|
||||
|------|--------|-------------|------------|
|
||||
| Widget doesn't update | High | Medium | Comprehensive testing, fallback to manual refresh |
|
||||
| Deep links fail | High | Low | URL validation, error handling, fallback to app home |
|
||||
| Memory issues | Medium | Medium | Profile early, set size limits, optimize data |
|
||||
| Data sync delays | Medium | Medium | Cache last known state, show loading states |
|
||||
| Build complexity | Low | Medium | Follow @bacons/apple-targets docs, seek help early |
|
||||
| Risk | Impact | Probability | Mitigation |
|
||||
| --------------------- | ------ | ----------- | ---------------------------------------------------- |
|
||||
| Widget doesn't update | High | Medium | Comprehensive testing, fallback to manual refresh |
|
||||
| Deep links fail | High | Low | URL validation, error handling, fallback to app home |
|
||||
| Memory issues | Medium | Medium | Profile early, set size limits, optimize data |
|
||||
| Data sync delays | Medium | Medium | Cache last known state, show loading states |
|
||||
| Build complexity | Low | Medium | Follow @bacons/apple-targets docs, seek help early |
|
||||
|
||||
### User Experience Risks
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
|------|--------|------------|
|
||||
| Widget confusion | Medium | Clear labels, contextual help, onboarding |
|
||||
| Data privacy concerns | High | Document data handling, no sensitive data in widgets |
|
||||
| Inconsistent UI | Low | Follow Apple HIG, use system fonts/colors |
|
||||
| Outdated widget data | Medium | Implement refresh logic, show last update time |
|
||||
| Risk | Impact | Mitigation |
|
||||
| --------------------- | ------ | ---------------------------------------------------- |
|
||||
| Widget confusion | Medium | Clear labels, contextual help, onboarding |
|
||||
| Data privacy concerns | High | Document data handling, no sensitive data in widgets |
|
||||
| Inconsistent UI | Low | Follow Apple HIG, use system fonts/colors |
|
||||
| Outdated widget data | Medium | Implement refresh logic, show last update time |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -1297,16 +1313,19 @@ Tippe auf ein Widget, um direkt zur App zu springen!
|
|||
### A. Widget Size Guidelines
|
||||
|
||||
**Small Widget (2x2 Grid)**
|
||||
|
||||
- Dimensions: 158x158 pts (iPhone 14)
|
||||
- Safe Area: 16pt padding all sides
|
||||
- Max Content: ~3 lines of text
|
||||
|
||||
**Medium Widget (4x2 Grid)**
|
||||
|
||||
- Dimensions: 338x158 pts (iPhone 14)
|
||||
- Safe Area: 16pt padding all sides
|
||||
- Max Content: ~5 lines of text, 1 image
|
||||
|
||||
**Large Widget (4x4 Grid)**
|
||||
|
||||
- Dimensions: 338x354 pts (iPhone 14)
|
||||
- Safe Area: 16pt padding all sides
|
||||
- Max Content: Full memo preview, multiple elements
|
||||
|
|
@ -1314,12 +1333,14 @@ Tippe auf ein Widget, um direkt zur App zu springen!
|
|||
### B. Color Palette
|
||||
|
||||
**Primary Colors:**
|
||||
|
||||
- Blue: `#007AFF` (Light), `#0A84FF` (Dark)
|
||||
- Green: `#34C759` (Success)
|
||||
- Red: `#FF3B30` (Error)
|
||||
- Gray: `#8E8E93` (Secondary Text)
|
||||
|
||||
**Space Colors:** (from existing Memoro theme)
|
||||
|
||||
- Use existing space color palette
|
||||
- Ensure WCAG AA contrast ratios
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# Zeego Kompatibilitätsprobleme und Alternativen-Analyse
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
Datum: 30. September 2025
|
||||
|
||||
Unser Projekt nutzt aktuell Zeego v3.0.6 für Context Menus und Dropdown Menus. Mit dem Upgrade auf Expo SDK 54 (React Native 0.81) und iOS 26 treten kritische Kompatibilitätsprobleme auf, die die App am Starten hindern.
|
||||
|
|
@ -18,12 +19,14 @@ react-native-ios-context-menu could not be found within the project
|
|||
### Zeego Kompatibilitätsstatus
|
||||
|
||||
**Zeego Version 3.x Kompatibilität:**
|
||||
|
||||
- ✅ React Native: 0.76 oder 0.77
|
||||
- ✅ Expo SDK: 52+
|
||||
- ❌ React Native 0.81 (SDK 54): **Nicht offiziell unterstützt**
|
||||
- ❌ iOS 26: **Keine offizielle Kompatibilität**
|
||||
|
||||
**Abhängigkeiten:**
|
||||
|
||||
- `react-native-menu`: 1.2.2
|
||||
- `react-native-ios-context-menu`: 3.1.0
|
||||
- `react-native-ios-utilities`: 5.1.2
|
||||
|
|
@ -53,11 +56,13 @@ Laut GitHub Issue #173 auf dem Zeego Repository:
|
|||
Zeego wird an **15 Stellen** im Projekt verwendet:
|
||||
|
||||
### Context Menus (4 Verwendungen)
|
||||
|
||||
- `components/organisms/Memory.tsx`
|
||||
- `components/molecules/PromptPreview.tsx`
|
||||
- `components/molecules/MemoPreview.tsx`
|
||||
|
||||
### Dropdown Menus (11 Verwendungen)
|
||||
|
||||
- `components/molecules/TableOfContentsMenu.tsx`
|
||||
- `components/atoms/Pill.tsx`
|
||||
- `features/subscription/SubscriptionMenu.tsx`
|
||||
|
|
@ -74,11 +79,13 @@ Zeego wird an **15 Stellen** im Projekt verwendet:
|
|||
Warten, bis Zeego offiziell Expo SDK 54 und React Native 0.81 unterstützt.
|
||||
|
||||
**Vorteile:**
|
||||
|
||||
- ✅ Keine Code-Änderungen nötig
|
||||
- ✅ Behält bestehende API und Funktionalität
|
||||
- ✅ Native Performance bleibt erhalten
|
||||
|
||||
**Nachteile:**
|
||||
|
||||
- ❌ Unbekannter Zeitrahmen
|
||||
- ❌ `react-native-ios-context-menu` ist im Maintenance Mode (Autor macht kein OSS mehr)
|
||||
- ❌ Blockiert SDK 54 Upgrade
|
||||
|
|
@ -97,6 +104,7 @@ Warten, bis Zeego offiziell Expo SDK 54 und React Native 0.81 unterstützt.
|
|||
Ersetze Dropdown- und Context-Menus durch `@expo/react-native-action-sheet` (bereits im Projekt als Dependency).
|
||||
|
||||
**Vorteile:**
|
||||
|
||||
- ✅ **Bereits im Projekt vorhanden** (`@expo/react-native-action-sheet@^4.1.1`)
|
||||
- ✅ Offiziell von Expo maintained
|
||||
- ✅ 100% Expo SDK 54 kompatibel
|
||||
|
|
@ -106,6 +114,7 @@ Ersetze Dropdown- und Context-Menus durch `@expo/react-native-action-sheet` (ber
|
|||
- ✅ Keine zusätzlichen Dependencies
|
||||
|
||||
**Nachteile:**
|
||||
|
||||
- ❌ **Limitierte Funktionalität** - keine verschachtelten Menüs, keine Icons, keine Checkboxen
|
||||
- ❌ Andere UX - Modal von unten statt Context Menu
|
||||
- ❌ Funktioniert nicht auf Web (nur mobil)
|
||||
|
|
@ -118,31 +127,36 @@ Ersetze Dropdown- und Context-Menus durch `@expo/react-native-action-sheet` (ber
|
|||
import * as DropdownMenu from 'zeego/dropdown-menu';
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Item key="delete" onSelect={handleDelete}>
|
||||
<DropdownMenu.ItemTitle>Löschen</DropdownMenu.ItemTitle>
|
||||
<DropdownMenu.ItemIcon ios={{ name: 'trash' }} />
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Item key="delete" onSelect={handleDelete}>
|
||||
<DropdownMenu.ItemTitle>Löschen</DropdownMenu.ItemTitle>
|
||||
<DropdownMenu.ItemIcon ios={{ name: 'trash' }} />
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>;
|
||||
|
||||
// Nachher (Action Sheet)
|
||||
import { useActionSheet } from '@expo/react-native-action-sheet';
|
||||
|
||||
const { showActionSheetWithOptions } = useActionSheet();
|
||||
|
||||
<Button onPress={() => {
|
||||
showActionSheetWithOptions({
|
||||
options: ['Löschen', 'Abbrechen'],
|
||||
destructiveButtonIndex: 0,
|
||||
cancelButtonIndex: 1,
|
||||
}, (buttonIndex) => {
|
||||
if (buttonIndex === 0) handleDelete();
|
||||
});
|
||||
}} />
|
||||
<Button
|
||||
onPress={() => {
|
||||
showActionSheetWithOptions(
|
||||
{
|
||||
options: ['Löschen', 'Abbrechen'],
|
||||
destructiveButtonIndex: 0,
|
||||
cancelButtonIndex: 1,
|
||||
},
|
||||
(buttonIndex) => {
|
||||
if (buttonIndex === 0) handleDelete();
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>;
|
||||
```
|
||||
|
||||
**Zeitaufwand:** 6-8 Stunden (alle 15 Verwendungen anpassen)
|
||||
|
|
@ -159,12 +173,14 @@ const { showActionSheetWithOptions } = useActionSheet();
|
|||
Cross-platform Context Menu Library (iOS + Android).
|
||||
|
||||
**Vorteile:**
|
||||
|
||||
- ✅ Context Menus auf iOS und Android
|
||||
- ✅ Einfachere API als Zeego
|
||||
- ✅ Native UI auf beiden Plattformen
|
||||
- ✅ Unterstützt Icons und Submenus
|
||||
|
||||
**Nachteile:**
|
||||
|
||||
- ❌ **Wartungsprobleme** - viele offene Issues
|
||||
- ❌ **Keine bestätigte New Architecture Unterstützung**
|
||||
- ❌ **Unklare SDK 54 Kompatibilität**
|
||||
|
|
@ -172,6 +188,7 @@ Cross-platform Context Menu Library (iOS + Android).
|
|||
- ❌ Nur Context Menus, keine Dropdown Menus
|
||||
|
||||
**Status der Library:**
|
||||
|
||||
- Latest Version: 1.19.0 (vor 5 Monaten)
|
||||
- Maintenance: "Not nearly as good" laut Community-Feedback
|
||||
- Signifikante offene Issues
|
||||
|
|
@ -190,15 +207,19 @@ Cross-platform Context Menu Library (iOS + Android).
|
|||
Direkte Verwendung von Platform-spezifischen APIs ohne Wrapper Library.
|
||||
|
||||
**iOS:**
|
||||
|
||||
- `@react-native-menu/menu@2.0.0` für iOS Context Menus (UIMenu)
|
||||
|
||||
**Android:**
|
||||
|
||||
- `@react-native-menu/menu@2.0.0` für Android PopupMenu
|
||||
|
||||
**Web:**
|
||||
|
||||
- Radix UI Dropdown Menu / Context Menu
|
||||
|
||||
**Vorteile:**
|
||||
|
||||
- ✅ Maximale Kontrolle
|
||||
- ✅ Native Features und Performance
|
||||
- ✅ Keine Wrapper-Dependencies
|
||||
|
|
@ -206,6 +227,7 @@ Direkte Verwendung von Platform-spezifischen APIs ohne Wrapper Library.
|
|||
- ✅ Expo config plugin vorhanden
|
||||
|
||||
**Nachteile:**
|
||||
|
||||
- ❌ **3 verschiedene Implementierungen** (iOS, Android, Web)
|
||||
- ❌ Viel Boilerplate Code
|
||||
- ❌ Höherer Wartungsaufwand
|
||||
|
|
@ -219,9 +241,9 @@ import { ContextMenuView } from '@react-native-menu/menu'; // iOS/Android
|
|||
import * as RadixDropdown from '@radix-ui/react-dropdown-menu'; // Web
|
||||
|
||||
const Menu = Platform.select({
|
||||
ios: IOSContextMenu,
|
||||
android: AndroidContextMenu,
|
||||
web: RadixWebMenu,
|
||||
ios: IOSContextMenu,
|
||||
android: AndroidContextMenu,
|
||||
web: RadixWebMenu,
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -239,18 +261,21 @@ const Menu = Platform.select({
|
|||
Kombiniere mehrere Libraries basierend auf Use Case.
|
||||
|
||||
**Strategie:**
|
||||
|
||||
- **Einfache Dropdown Menus:** Action Sheet
|
||||
- **iOS Context Menus (kritisch):** `@react-native-menu/menu@2.0.0`
|
||||
- **Android:** Action Sheet oder `@react-native-menu/menu`
|
||||
- **Web:** Radix UI oder Action Sheet-ähnliche Modals
|
||||
|
||||
**Vorteile:**
|
||||
|
||||
- ✅ Best-of-both-worlds Ansatz
|
||||
- ✅ Sofort funktionsfähig
|
||||
- ✅ Optimiert für jeden Use Case
|
||||
- ✅ Schrittweise Migration möglich
|
||||
|
||||
**Nachteile:**
|
||||
|
||||
- ❌ Höhere Code-Komplexität
|
||||
- ❌ Mehr Dependencies
|
||||
- ❌ Inkonsistente UX möglich
|
||||
|
|
@ -267,6 +292,7 @@ Kombiniere mehrere Libraries basierend auf Use Case.
|
|||
Baue eigene Menu-Komponenten mit React Native Modals, Pressables und Animations.
|
||||
|
||||
**Vorteile:**
|
||||
|
||||
- ✅ Volle Kontrolle über UX
|
||||
- ✅ Cross-platform konsistent
|
||||
- ✅ Keine nativen Dependencies
|
||||
|
|
@ -274,6 +300,7 @@ Baue eigene Menu-Komponenten mit React Native Modals, Pressables und Animations.
|
|||
- ✅ Keine Kompatibilitätsprobleme
|
||||
|
||||
**Nachteile:**
|
||||
|
||||
- ❌ **Sehr hoher Zeitaufwand** (20-30 Stunden)
|
||||
- ❌ Kein natives Look-and-Feel
|
||||
- ❌ Performance-Optimierung nötig
|
||||
|
|
@ -290,16 +317,16 @@ Baue eigene Menu-Komponenten mit React Native Modals, Pressables und Animations.
|
|||
|
||||
## Vergleichsmatrix
|
||||
|
||||
| Kriterium | Option 1: Warten | Option 2: Action Sheet | Option 3: context-menu-view | Option 4: Native APIs | Option 5: Hybrid | Option 6: Custom |
|
||||
|-----------|------------------|------------------------|------------------------------|----------------------|------------------|------------------|
|
||||
| **Zeitaufwand** | 0h (Wartezeit) | 6-8h | 8-10h | 12-16h | 10-14h | 20-30h |
|
||||
| **SDK 54 Ready** | ❌ Nein | ✅ Ja | ⚠️ Unklar | ✅ Ja | ✅ Ja | ✅ Ja |
|
||||
| **New Arch Support** | ⚠️ Teils | ✅ Ja | ❌ Unklar | ✅ Ja | ✅ Ja | ✅ Ja |
|
||||
| **Native Look** | ✅ Ja | ✅ Ja | ✅ Ja | ✅ Ja | ✅ Ja | ❌ Nein |
|
||||
| **Web Support** | ✅ Ja | ❌ Nein | ❌ Nein | ✅ Ja | ⚠️ Teils | ✅ Ja |
|
||||
| **Maintenance** | ⚠️ Extern | ✅ Niedrig | ❌ Hoch | ⚠️ Mittel | ⚠️ Mittel | ❌ Sehr Hoch |
|
||||
| **Feature-Vollständigkeit** | ✅✅✅ | ⚠️ | ✅✅ | ✅✅✅ | ✅✅✅ | ✅✅✅ |
|
||||
| **Risiko** | 🔴 Hoch | 🟢 Niedrig | 🟡 Mittel-Hoch | 🟡 Mittel | 🟡 Mittel | 🔴 Hoch |
|
||||
| Kriterium | Option 1: Warten | Option 2: Action Sheet | Option 3: context-menu-view | Option 4: Native APIs | Option 5: Hybrid | Option 6: Custom |
|
||||
| --------------------------- | ---------------- | ---------------------- | --------------------------- | --------------------- | ---------------- | ---------------- |
|
||||
| **Zeitaufwand** | 0h (Wartezeit) | 6-8h | 8-10h | 12-16h | 10-14h | 20-30h |
|
||||
| **SDK 54 Ready** | ❌ Nein | ✅ Ja | ⚠️ Unklar | ✅ Ja | ✅ Ja | ✅ Ja |
|
||||
| **New Arch Support** | ⚠️ Teils | ✅ Ja | ❌ Unklar | ✅ Ja | ✅ Ja | ✅ Ja |
|
||||
| **Native Look** | ✅ Ja | ✅ Ja | ✅ Ja | ✅ Ja | ✅ Ja | ❌ Nein |
|
||||
| **Web Support** | ✅ Ja | ❌ Nein | ❌ Nein | ✅ Ja | ⚠️ Teils | ✅ Ja |
|
||||
| **Maintenance** | ⚠️ Extern | ✅ Niedrig | ❌ Hoch | ⚠️ Mittel | ⚠️ Mittel | ❌ Sehr Hoch |
|
||||
| **Feature-Vollständigkeit** | ✅✅✅ | ⚠️ | ✅✅ | ✅✅✅ | ✅✅✅ | ✅✅✅ |
|
||||
| **Risiko** | 🔴 Hoch | 🟢 Niedrig | 🟡 Mittel-Hoch | 🟡 Mittel | 🟡 Mittel | 🔴 Hoch |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -347,30 +374,36 @@ Baue eigene Menu-Komponenten mit React Native Modals, Pressables und Animations.
|
|||
**Ziel:** SDK 54 kompatible App mit Action Sheets
|
||||
|
||||
#### Tag 1: Vorbereitung
|
||||
|
||||
1. **Backup erstellen**
|
||||
|
||||
```bash
|
||||
git checkout -b migration/zeego-to-actionsheet
|
||||
```
|
||||
|
||||
2. **Action Sheet Hook vorbereiten**
|
||||
|
||||
```tsx
|
||||
// hooks/useMenu.ts
|
||||
import { useActionSheet } from '@expo/react-native-action-sheet';
|
||||
|
||||
export const useMenu = () => {
|
||||
const { showActionSheetWithOptions } = useActionSheet();
|
||||
const { showActionSheetWithOptions } = useActionSheet();
|
||||
|
||||
return {
|
||||
showMenu: (options: MenuOption[]) => {
|
||||
showActionSheetWithOptions({
|
||||
options: options.map(o => o.title),
|
||||
destructiveButtonIndex: options.findIndex(o => o.destructive),
|
||||
cancelButtonIndex: options.length - 1,
|
||||
}, (index) => {
|
||||
options[index]?.onSelect?.();
|
||||
});
|
||||
}
|
||||
};
|
||||
return {
|
||||
showMenu: (options: MenuOption[]) => {
|
||||
showActionSheetWithOptions(
|
||||
{
|
||||
options: options.map((o) => o.title),
|
||||
destructiveButtonIndex: options.findIndex((o) => o.destructive),
|
||||
cancelButtonIndex: options.length - 1,
|
||||
},
|
||||
(index) => {
|
||||
options[index]?.onSelect?.();
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -378,14 +411,15 @@ Baue eigene Menu-Komponenten mit React Native Modals, Pressables und Animations.
|
|||
```tsx
|
||||
// types/menu.ts
|
||||
export interface MenuOption {
|
||||
title: string;
|
||||
onSelect?: () => void;
|
||||
destructive?: boolean;
|
||||
disabled?: boolean;
|
||||
title: string;
|
||||
onSelect?: () => void;
|
||||
destructive?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
#### Tag 2: Migration Dropdown Menus (11 Komponenten)
|
||||
|
||||
1. `features/menus/HeaderMenu.tsx`
|
||||
2. `features/menus/MemoMenu.tsx`
|
||||
3. `features/menus/MemoHeaderMenu.tsx`
|
||||
|
|
@ -395,12 +429,14 @@ Baue eigene Menu-Komponenten mit React Native Modals, Pressables und Animations.
|
|||
7. Weitere Dropdown-Komponenten
|
||||
|
||||
**Pro Komponente:**
|
||||
|
||||
- Zeego Imports entfernen
|
||||
- Action Sheet Hook einbinden
|
||||
- Trigger Button anpassen
|
||||
- Menu Options Array erstellen
|
||||
|
||||
#### Tag 3: Migration Context Menus (4 Komponenten)
|
||||
|
||||
1. `components/organisms/Memory.tsx`
|
||||
2. `components/molecules/PromptPreview.tsx`
|
||||
3. `components/molecules/MemoPreview.tsx`
|
||||
|
|
@ -408,6 +444,7 @@ Baue eigene Menu-Komponenten mit React Native Modals, Pressables und Animations.
|
|||
**Temporär:** Action Sheet mit Long Press Trigger
|
||||
|
||||
#### Tag 3 Nachmittag: Testing
|
||||
|
||||
- iOS Testing (Simulator + Device)
|
||||
- Android Testing (Emulator + Device)
|
||||
- Funktionale Tests aller Menu-Interaktionen
|
||||
|
|
@ -422,20 +459,21 @@ Baue eigene Menu-Komponenten mit React Native Modals, Pressables und Animations.
|
|||
#### Woche 1: Setup + iOS Implementation
|
||||
|
||||
**Dependency Installation:**
|
||||
|
||||
```bash
|
||||
npm install @react-native-menu/menu@2.0.0
|
||||
```
|
||||
|
||||
**app.json Plugin:**
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": [
|
||||
"@react-native-menu/menu"
|
||||
]
|
||||
"plugins": ["@react-native-menu/menu"]
|
||||
}
|
||||
```
|
||||
|
||||
**Rebuild:**
|
||||
|
||||
```bash
|
||||
npx expo prebuild --clean
|
||||
npx expo run:ios
|
||||
|
|
@ -450,40 +488,43 @@ import { ContextMenuView } from '@react-native-menu/menu';
|
|||
import { useMenu } from '~/hooks/useMenu'; // Action Sheet fallback
|
||||
|
||||
export const Memory = ({ memory }) => {
|
||||
const { showMenu } = useMenu();
|
||||
const { showMenu } = useMenu();
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
return (
|
||||
<ContextMenuView
|
||||
actions={[
|
||||
{ id: 'share', title: 'Teilen', image: 'square.and.arrow.up' },
|
||||
{ id: 'delete', title: 'Löschen', destructive: true },
|
||||
]}
|
||||
onPressAction={({ nativeEvent }) => {
|
||||
if (nativeEvent.actionKey === 'delete') handleDelete();
|
||||
if (nativeEvent.actionKey === 'share') handleShare();
|
||||
}}
|
||||
>
|
||||
<MemoryContent memory={memory} />
|
||||
</ContextMenuView>
|
||||
);
|
||||
}
|
||||
if (Platform.OS === 'ios') {
|
||||
return (
|
||||
<ContextMenuView
|
||||
actions={[
|
||||
{ id: 'share', title: 'Teilen', image: 'square.and.arrow.up' },
|
||||
{ id: 'delete', title: 'Löschen', destructive: true },
|
||||
]}
|
||||
onPressAction={({ nativeEvent }) => {
|
||||
if (nativeEvent.actionKey === 'delete') handleDelete();
|
||||
if (nativeEvent.actionKey === 'share') handleShare();
|
||||
}}
|
||||
>
|
||||
<MemoryContent memory={memory} />
|
||||
</ContextMenuView>
|
||||
);
|
||||
}
|
||||
|
||||
// Android/Web Fallback: Long Press -> Action Sheet
|
||||
return (
|
||||
<Pressable
|
||||
onLongPress={() => showMenu([
|
||||
{ title: 'Teilen', onSelect: handleShare },
|
||||
{ title: 'Löschen', onSelect: handleDelete, destructive: true },
|
||||
])}
|
||||
>
|
||||
<MemoryContent memory={memory} />
|
||||
</Pressable>
|
||||
);
|
||||
// Android/Web Fallback: Long Press -> Action Sheet
|
||||
return (
|
||||
<Pressable
|
||||
onLongPress={() =>
|
||||
showMenu([
|
||||
{ title: 'Teilen', onSelect: handleShare },
|
||||
{ title: 'Löschen', onSelect: handleDelete, destructive: true },
|
||||
])
|
||||
}
|
||||
>
|
||||
<MemoryContent memory={memory} />
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### Woche 2: Testing + Optimierung
|
||||
|
||||
- iOS Context Menu Testing
|
||||
- Android Fallback Testing
|
||||
- Performance Testing
|
||||
|
|
@ -494,11 +535,13 @@ export const Memory = ({ memory }) => {
|
|||
### Phase 3: Monitoring & Evaluation (Ongoing)
|
||||
|
||||
**Zeego Tracking:**
|
||||
|
||||
- GitHub Issue #173 monitoren
|
||||
- Release Notes von Zeego beobachten
|
||||
- Bei SDK 54 Support: Evaluation ob Rückmigration lohnt
|
||||
|
||||
**Kriterien für Rückmigration zu Zeego:**
|
||||
|
||||
- ✅ Offizieller Expo SDK 54 Support
|
||||
- ✅ React Native 0.81+ Support
|
||||
- ✅ Stabile Version (keine Beta)
|
||||
|
|
@ -524,11 +567,7 @@ npm uninstall @react-native-menu/menu # Falls installiert als Zeego Dependency
|
|||
import { ActionSheetProvider } from '@expo/react-native-action-sheet';
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<ActionSheetProvider>
|
||||
{/* Rest der App */}
|
||||
</ActionSheetProvider>
|
||||
);
|
||||
return <ActionSheetProvider>{/* Rest der App */}</ActionSheetProvider>;
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -540,44 +579,47 @@ import { useActionSheet } from '@expo/react-native-action-sheet';
|
|||
import { useCallback } from 'react';
|
||||
|
||||
export interface MenuOption {
|
||||
title: string;
|
||||
onSelect?: () => void;
|
||||
destructive?: boolean;
|
||||
disabled?: boolean;
|
||||
icon?: string; // Für zukünftige native menu implementation
|
||||
title: string;
|
||||
onSelect?: () => void;
|
||||
destructive?: boolean;
|
||||
disabled?: boolean;
|
||||
icon?: string; // Für zukünftige native menu implementation
|
||||
}
|
||||
|
||||
export const useMenu = () => {
|
||||
const { showActionSheetWithOptions } = useActionSheet();
|
||||
const { showActionSheetWithOptions } = useActionSheet();
|
||||
|
||||
const showMenu = useCallback((
|
||||
options: MenuOption[],
|
||||
config?: {
|
||||
title?: string;
|
||||
message?: string;
|
||||
}
|
||||
) => {
|
||||
const actionOptions = options.map(o => o.title);
|
||||
const cancelButtonIndex = actionOptions.length - 1;
|
||||
const destructiveButtonIndex = options.findIndex(o => o.destructive);
|
||||
const showMenu = useCallback(
|
||||
(
|
||||
options: MenuOption[],
|
||||
config?: {
|
||||
title?: string;
|
||||
message?: string;
|
||||
}
|
||||
) => {
|
||||
const actionOptions = options.map((o) => o.title);
|
||||
const cancelButtonIndex = actionOptions.length - 1;
|
||||
const destructiveButtonIndex = options.findIndex((o) => o.destructive);
|
||||
|
||||
showActionSheetWithOptions(
|
||||
{
|
||||
options: actionOptions,
|
||||
cancelButtonIndex,
|
||||
destructiveButtonIndex: destructiveButtonIndex >= 0 ? destructiveButtonIndex : undefined,
|
||||
title: config?.title,
|
||||
message: config?.message,
|
||||
},
|
||||
(buttonIndex) => {
|
||||
if (buttonIndex !== undefined && buttonIndex !== cancelButtonIndex) {
|
||||
options[buttonIndex]?.onSelect?.();
|
||||
}
|
||||
}
|
||||
);
|
||||
}, [showActionSheetWithOptions]);
|
||||
showActionSheetWithOptions(
|
||||
{
|
||||
options: actionOptions,
|
||||
cancelButtonIndex,
|
||||
destructiveButtonIndex: destructiveButtonIndex >= 0 ? destructiveButtonIndex : undefined,
|
||||
title: config?.title,
|
||||
message: config?.message,
|
||||
},
|
||||
(buttonIndex) => {
|
||||
if (buttonIndex !== undefined && buttonIndex !== cancelButtonIndex) {
|
||||
options[buttonIndex]?.onSelect?.();
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
[showActionSheetWithOptions]
|
||||
);
|
||||
|
||||
return { showMenu };
|
||||
return { showMenu };
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -586,32 +628,40 @@ export const useMenu = () => {
|
|||
## Risiken & Mitigationen
|
||||
|
||||
### Risiko 1: UX Verschlechterung
|
||||
|
||||
**Problem:** Action Sheets haben andere UX als Context/Dropdown Menus
|
||||
|
||||
**Mitigation:**
|
||||
|
||||
- User Testing durchführen
|
||||
- Feedback sammeln
|
||||
- Bei kritischen Features: Native Context Menus (Phase 2)
|
||||
|
||||
### Risiko 2: Web Support
|
||||
|
||||
**Problem:** Action Sheets funktionieren nicht auf Web
|
||||
|
||||
**Mitigation:**
|
||||
|
||||
- Falls Web wichtig: Phase 2 mit Radix UI für Web
|
||||
- Oder: Web-spezifische Dropdown Implementierung mit React Native Web Modals
|
||||
|
||||
### Risiko 3: Feature Loss
|
||||
|
||||
**Problem:** Icons, Checkboxes, verschachtelte Menus gehen verloren
|
||||
|
||||
**Mitigation:**
|
||||
|
||||
- Dokumentiere fehlende Features
|
||||
- Priorisiere nach Business Impact
|
||||
- Alternative UI Patterns für kritische Features (z.B. separate Settings Screens statt Inline Checkboxes)
|
||||
|
||||
### Risiko 4: Zukünftige Zeego-Updates
|
||||
|
||||
**Problem:** Wenn Zeego später SDK 54 Support erhält, haben wir doppelten Aufwand
|
||||
|
||||
**Mitigation:**
|
||||
|
||||
- Code modular halten (useMenu Hook als Abstraction)
|
||||
- Zeego Monitoring als Teil des Prozesses
|
||||
- Rückmigration nur wenn klarer Mehrwert
|
||||
|
|
@ -621,21 +671,25 @@ export const useMenu = () => {
|
|||
## Entscheidungshilfe
|
||||
|
||||
### Wähle **Option 1 (Warten)** wenn:
|
||||
|
||||
- ❌ Nicht empfohlen - zu hohes Risiko
|
||||
|
||||
### Wähle **Option 2 (Action Sheet)** wenn:
|
||||
|
||||
- ✅ Schnelle Lösung benötigt (2-3 Tage)
|
||||
- ✅ SDK 54 Upgrade blockiert
|
||||
- ✅ Basic Menu-Funktionalität ausreichend
|
||||
- ✅ Team-Kapazität limitiert
|
||||
|
||||
### Wähle **Option 4 (Native APIs)** wenn:
|
||||
|
||||
- ✅ Context Menu UX kritisch
|
||||
- ✅ Platform-spezifische Features benötigt
|
||||
- ✅ Team hat 2-3 Wochen Zeit
|
||||
- ✅ Web Support wichtig
|
||||
|
||||
### Wähle **Option 2 + 4 Hybrid (Empfohlen)** wenn:
|
||||
|
||||
- ✅ Best-of-both-worlds gewünscht
|
||||
- ✅ Schrittweise Migration möglich
|
||||
- ✅ Risiko-Minimierung wichtig
|
||||
|
|
@ -657,18 +711,22 @@ export const useMenu = () => {
|
|||
## Ressourcen & Links
|
||||
|
||||
### Zeego
|
||||
|
||||
- [Zeego GitHub Issue #173 (Expo 54 Build fails)](https://github.com/nandorojo/zeego/issues/173)
|
||||
- [Zeego Docs - Compatibility Table](https://zeego.dev/start)
|
||||
|
||||
### Action Sheet
|
||||
|
||||
- [@expo/react-native-action-sheet Docs](https://docs.expo.dev/versions/latest/sdk/action-sheet/)
|
||||
- [GitHub Repository](https://github.com/expo/react-native-action-sheet)
|
||||
|
||||
### Native Menu
|
||||
|
||||
- [@react-native-menu/menu v2.0.0](https://www.npmjs.com/package/@react-native-menu/menu)
|
||||
- [GitHub Repository](https://github.com/react-native-menu/menu)
|
||||
|
||||
### Expo SDK 54
|
||||
|
||||
- [Expo SDK 54 Changelog](https://expo.dev/changelog/sdk-54)
|
||||
- [Expo SDK 54 Upgrade Guide](https://expo.dev/blog/expo-sdk-upgrade-guide)
|
||||
|
||||
|
|
@ -677,6 +735,7 @@ export const useMenu = () => {
|
|||
## Appendix: Zeego Usage in Project
|
||||
|
||||
### Context Menu Verwendungen
|
||||
|
||||
1. **components/organisms/Memory.tsx** (Zeile 17)
|
||||
- Use Case: Long-press auf Memory für Actions (Share, Delete, etc.)
|
||||
- Kritikalität: Hoch - Kern-Feature
|
||||
|
|
@ -690,6 +749,7 @@ export const useMenu = () => {
|
|||
- Kritikalität: Hoch
|
||||
|
||||
### Dropdown Menu Verwendungen
|
||||
|
||||
4. **features/menus/HeaderMenu.tsx** (Zeile 8)
|
||||
- Use Case: App Header Menu
|
||||
- Kritikalität: Mittel
|
||||
|
|
@ -723,4 +783,4 @@ export const useMenu = () => {
|
|||
**Dokument erstellt:** 30. September 2025
|
||||
**Letzte Aktualisierung:** 30. September 2025
|
||||
**Autor:** Claude Code Analyse
|
||||
**Status:** ✅ Bereit für Team Review
|
||||
**Status:** ✅ Bereit für Team Review
|
||||
|
|
|
|||
|
|
@ -3,17 +3,20 @@
|
|||
## Abgeschlossen ✅
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [x] Zeego deinstalliert
|
||||
- [x] react-native-ios-context-menu deinstalliert
|
||||
- [x] react-native-ios-utilities deinstalliert
|
||||
- [x] @react-native-menu/menu@2.0.0 bereits vorhanden
|
||||
|
||||
### Utility Components
|
||||
|
||||
- [x] `config/menuActions.ts` - Zentralisierte Menu Actions
|
||||
- [x] `utils/menuBuilder.ts` - Menu Action Builder
|
||||
- [x] `components/ui/NativeMenu.tsx` - Wiederverwendbare Wrapper-Komponente
|
||||
|
||||
### Dropdown Menus (3/11 migriert)
|
||||
|
||||
- [x] `features/menus/HeaderMenu.tsx`
|
||||
- [x] `features/menus/MemoMenu.tsx`
|
||||
- [x] `features/menus/MemoHeaderMenu.tsx`
|
||||
|
|
@ -24,6 +27,7 @@
|
|||
- [ ] Weitere 4 Komponenten (noch zu identifizieren)
|
||||
|
||||
### Context Menus (0/4 migriert)
|
||||
|
||||
- [ ] `components/organisms/Memory.tsx`
|
||||
- [ ] `components/molecules/PromptPreview.tsx`
|
||||
- [ ] `components/molecules/MemoPreview.tsx`
|
||||
|
|
@ -40,6 +44,7 @@
|
|||
## Migration Pattern
|
||||
|
||||
### Dropdown Menu (Tap)
|
||||
|
||||
```tsx
|
||||
// Vorher
|
||||
import * as DropdownMenu from 'zeego/dropdown-menu';
|
||||
|
|
@ -48,29 +53,31 @@ import * as DropdownMenu from 'zeego/dropdown-menu';
|
|||
import { MenuView } from '@react-native-menu/menu';
|
||||
|
||||
<MenuView
|
||||
actions={actions}
|
||||
onPressAction={({ nativeEvent }) => handleAction(nativeEvent.event)}
|
||||
shouldOpenOnLongPress={false} // Dropdown = tap
|
||||
actions={actions}
|
||||
onPressAction={({ nativeEvent }) => handleAction(nativeEvent.event)}
|
||||
shouldOpenOnLongPress={false} // Dropdown = tap
|
||||
>
|
||||
{children}
|
||||
</MenuView>
|
||||
{children}
|
||||
</MenuView>;
|
||||
```
|
||||
|
||||
### Context Menu (Long Press)
|
||||
|
||||
```tsx
|
||||
<MenuView
|
||||
actions={actions}
|
||||
onPressAction={({ nativeEvent }) => handleAction(nativeEvent.event)}
|
||||
shouldOpenOnLongPress={true} // Context = long press (default)
|
||||
actions={actions}
|
||||
onPressAction={({ nativeEvent }) => handleAction(nativeEvent.event)}
|
||||
shouldOpenOnLongPress={true} // Context = long press (default)
|
||||
>
|
||||
{children}
|
||||
{children}
|
||||
</MenuView>
|
||||
```
|
||||
|
||||
## Web Fallbacks
|
||||
|
||||
Alle migrierten Komponenten behalten ihre bestehenden Web-Implementierungen (Custom Modals).
|
||||
|
||||
---
|
||||
|
||||
**Status:** In Progress (3/15 Komponenten migriert)
|
||||
**Letzte Aktualisierung:** 30. September 2025
|
||||
**Letzte Aktualisierung:** 30. September 2025
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue