refactor: restructure

monorepo with apps/ and services/
  directories
This commit is contained in:
Wuesteon 2025-11-26 03:03:24 +01:00
parent 25824ed0ac
commit ff80aeec1f
4062 changed files with 2592 additions and 1278 deletions

View file

@ -0,0 +1,342 @@
name: Deploy Backend to Cloud Run
on:
push:
branches: [main]
paths:
- 'backend/**'
- '.github/workflows/deploy-backend.yml'
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy to'
type: choice
required: true
default: 'production'
options:
- production
- staging
env:
PROJECT_ID: mana-core-453821
REGION: europe-west3
ARTIFACT_REGISTRY: europe-west3-docker.pkg.dev
SERVICE_NAME: manadeck-backend
REPOSITORY_NAME: manadeck-backend
WORKING_DIR: backend
jobs:
test:
name: Test & Build Verification
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
defaults:
run:
working-directory: ${{ env.WORKING_DIR }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Configure git for private packages
env:
GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
run: |
git config --global url."https://${GH_TOKEN}@github.com/".insteadOf ssh://git@github.com/
git config --global url."https://${GH_TOKEN}@github.com/".insteadOf git@github.com:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: ${{ env.WORKING_DIR }}/package-lock.json
- name: Patch package-lock.json with authenticated URLs
env:
GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
working-directory: ${{ env.WORKING_DIR }}
run: |
# Handle both SSH and HTTPS URLs
if grep -q "git+ssh://git@github.com" package-lock.json; then
echo "⚠️ SSH URLs found - patching to HTTPS with token..."
sed -i "s|git+ssh://git@github.com/Memo-2023/|git+https://${GH_TOKEN}@github.com/Memo-2023/|g" package-lock.json
echo "✓ Lockfile patched successfully"
else
echo "⚠️ HTTPS URLs found - injecting token..."
sed -i "s|git+https://github.com/Memo-2023/|git+https://${GH_TOKEN}@github.com/Memo-2023/|g" package-lock.json
echo "✓ Token injected successfully"
fi
- name: Install dependencies
working-directory: ${{ env.WORKING_DIR }}
run: npm ci --legacy-peer-deps
- name: Type check & build
working-directory: ${{ env.WORKING_DIR }}
run: npm run build
- name: Run tests
working-directory: ${{ env.WORKING_DIR }}
run: npm test
continue-on-error: true
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: ${{ env.WORKING_DIR }}/dist
retention-days: 1
build-and-deploy:
name: Build & Deploy to Cloud Run
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Google Cloud Auth
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY_PROD }}
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
with:
version: 'latest'
- name: Configure Docker for Artifact Registry
run: |
gcloud auth configure-docker ${{ env.ARTIFACT_REGISTRY }}
- name: Generate version tag
id: version
run: |
SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7)
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
VERSION="v${TIMESTAMP}-${SHORT_SHA}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT
- name: Build and Push Docker Image
env:
DOCKER_BUILDKIT: 1
GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
working-directory: ${{ env.WORKING_DIR }}
run: |
IMAGE_NAME="${{ env.ARTIFACT_REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.REPOSITORY_NAME }}/${{ env.SERVICE_NAME }}"
IMAGE_TAG="${IMAGE_NAME}:${{ steps.version.outputs.version }}"
LATEST_TAG="${IMAGE_NAME}:latest"
SHA_TAG="${IMAGE_NAME}:${{ steps.version.outputs.short_sha }}"
echo "Building image: ${IMAGE_TAG}"
docker build \
-t ${IMAGE_TAG} \
-t ${LATEST_TAG} \
-t ${SHA_TAG} \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg VCS_REF=${{ github.sha }} \
--build-arg VERSION=${{ steps.version.outputs.version }} \
--secret id=github_token,env=GH_TOKEN \
.
echo "Pushing images..."
docker push ${IMAGE_TAG}
docker push ${LATEST_TAG}
docker push ${SHA_TAG}
echo "image_tag=${IMAGE_TAG}" >> $GITHUB_ENV
echo "version=${{ steps.version.outputs.version }}" >> $GITHUB_ENV
- name: Deploy to Cloud Run
id: deploy
run: |
echo "Deploying to Cloud Run..."
echo "Service account: ${{ secrets.CLOUD_RUN_SERVICE_ACCOUNT }}"
gcloud run deploy ${{ env.SERVICE_NAME }} \
--image="${{ env.image_tag }}" \
--project=${{ env.PROJECT_ID }} \
--region=${{ env.REGION }} \
--platform=managed \
--allow-unauthenticated \
--min-instances=0 \
--max-instances=10 \
--memory=512Mi \
--cpu=1 \
--timeout=300 \
--concurrency=80 \
--port=8080 \
--service-account=${{ secrets.CLOUD_RUN_SERVICE_ACCOUNT }} \
--set-env-vars="NODE_ENV=production" \
--set-secrets="MANA_SERVICE_URL=MANA_SERVICE_URL:latest,APP_ID=MANADECK_APP_ID:latest,MANA_SUPABASE_SECRET_KEY=MANA_SUPABASE_SECRET_KEY:latest,SUPABASE_URL=MANADECK_SUPABASE_URL:latest,SUPABASE_ANON_KEY=MANADECK_SUPABASE_ANON_KEY:latest,SUPABASE_SERVICE_KEY=MANADECK_SUPABASE_SERVICE_KEY:latest" \
--labels="environment=production,commit=${{ steps.version.outputs.short_sha }},version=${{ env.version }}"
# Ensure 100% traffic goes to the new revision
echo "Routing traffic to latest revision..."
gcloud run services update-traffic ${{ env.SERVICE_NAME }} \
--to-latest \
--project=${{ env.PROJECT_ID }} \
--region=${{ env.REGION }} \
--platform=managed
echo "✅ Traffic routed to latest revision"
- name: Get Service URL
id: service-url
run: |
SERVICE_URL=$(gcloud run services describe ${{ env.SERVICE_NAME }} \
--project=${{ env.PROJECT_ID }} \
--region=${{ env.REGION }} \
--format='value(status.url)')
echo "service_url=${SERVICE_URL}" >> $GITHUB_OUTPUT
echo "## 🚀 Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Service**: \`${{ env.SERVICE_NAME }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **URL**: ${SERVICE_URL}" >> $GITHUB_STEP_SUMMARY
echo "- **Version**: \`${{ env.version }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Image**: \`${{ env.image_tag }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Region**: \`${{ env.REGION }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Commit**: \`${{ steps.version.outputs.short_sha }}\`" >> $GITHUB_STEP_SUMMARY
- name: Wait for deployment
run: sleep 15
- name: Health Check
id: health-check
run: |
SERVICE_URL="${{ steps.service-url.outputs.service_url }}"
echo "Testing health endpoint: ${SERVICE_URL}/health/live"
MAX_RETRIES=5
RETRY_COUNT=0
SUCCESS=false
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" ${SERVICE_URL}/health/live || echo "000")
if [ "$HTTP_CODE" = "200" ]; then
echo "✅ Health check passed (HTTP $HTTP_CODE)"
SUCCESS=true
break
else
echo "⚠️ Health check attempt $((RETRY_COUNT + 1))/$MAX_RETRIES failed (HTTP $HTTP_CODE)"
RETRY_COUNT=$((RETRY_COUNT + 1))
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
echo "Retrying in 10 seconds..."
sleep 10
fi
fi
done
if [ "$SUCCESS" = false ]; then
echo "❌ Health check failed after $MAX_RETRIES attempts"
exit 1
fi
- name: Liveness Check
run: |
SERVICE_URL="${{ steps.service-url.outputs.service_url }}"
echo "Testing liveness endpoint: ${SERVICE_URL}/health/live"
RESPONSE=$(curl -s ${SERVICE_URL}/health/live)
echo "Response: $RESPONSE"
STATUS=$(echo $RESPONSE | jq -r '.status')
if [ "$STATUS" = "ok" ]; then
echo "✅ Liveness check passed"
else
echo "❌ Liveness check failed"
exit 1
fi
- name: Deployment Notification
if: success()
run: |
echo "## ✅ Deployment Successful" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Service is healthy and ready to receive traffic." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Quick Links" >> $GITHUB_STEP_SUMMARY
echo "- [Service URL](${{ steps.service-url.outputs.service_url }})" >> $GITHUB_STEP_SUMMARY
echo "- [Health Check](${{ steps.service-url.outputs.service_url }}/health)" >> $GITHUB_STEP_SUMMARY
echo "- [Cloud Run Console](https://console.cloud.google.com/run/detail/${{ env.REGION }}/${{ env.SERVICE_NAME }}/metrics?project=${{ env.PROJECT_ID }})" >> $GITHUB_STEP_SUMMARY
rollback:
name: Rollback on Failure
runs-on: ubuntu-latest
if: failure() && needs.build-and-deploy.result == 'failure'
needs: build-and-deploy
steps:
- name: Google Cloud Auth
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY_PROD }}
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
with:
version: 'latest'
- name: Get Previous Revision
id: get-revision
run: |
REVISIONS=$(gcloud run revisions list \
--service=${{ env.SERVICE_NAME }} \
--project=${{ env.PROJECT_ID }} \
--region=${{ env.REGION }} \
--format="value(name)" \
--limit=2)
PREV_REVISION=$(echo "$REVISIONS" | tail -n 1)
if [ -z "$PREV_REVISION" ]; then
echo "❌ No previous revision found for rollback"
exit 1
fi
echo "prev_revision=${PREV_REVISION}" >> $GITHUB_OUTPUT
echo "Found previous revision: ${PREV_REVISION}"
- name: Rollback to Previous Revision
run: |
echo "Rolling back to revision: ${{ steps.get-revision.outputs.prev_revision }}"
gcloud run services update-traffic ${{ env.SERVICE_NAME }} \
--to-revisions=${{ steps.get-revision.outputs.prev_revision }}=100 \
--project=${{ env.PROJECT_ID }} \
--region=${{ env.REGION }}
echo "## ⚠️ Deployment Failed - Rollback Executed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Service has been rolled back to revision: \`${{ steps.get-revision.outputs.prev_revision }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Please check the logs and fix the issues before redeploying." >> $GITHUB_STEP_SUMMARY
- name: Verify Rollback
run: |
sleep 10
SERVICE_URL=$(gcloud run services describe ${{ env.SERVICE_NAME }} \
--project=${{ env.PROJECT_ID }} \
--region=${{ env.REGION }} \
--format='value(status.url)')
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" ${SERVICE_URL}/health/live)
if [ $HTTP_CODE -eq 200 ]; then
echo "✅ Rollback successful - service is healthy"
else
echo "❌ Rollback verification failed - service may be unhealthy"
exit 1
fi

31
apps/manadeck/.gitignore vendored Normal file
View file

@ -0,0 +1,31 @@
node_modules/
.expo/
dist/
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
# expo router
expo-env.d.ts
# firebase/supabase/vexo
.env
ios
android
# macOS
.DS_Store
# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*
backend/node_modules
apps/mobile/node_modules
apps/web/node_modules
apps/landing/node_modules

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,506 @@
# Manadeck Credit System
This document explains how the Mana Core credit system is integrated into Manadeck.
## Overview
Manadeck uses **Mana** as its credit currency to charge for operations like deck creation, card generation, and AI features. The credit system is powered by [Mana Core](https://github.com/Memo-2023/mana-core-nestjs-package), which provides:
- Credit validation before operations
- Credit consumption after successful operations
- Real-time balance tracking
- Transaction history
- App-level usage tracking
## Credit Costs
| Operation | Cost (Mana) | Description |
|-----------|-------------|-------------|
| Deck Creation | 10 | Create a new deck |
| Card Creation | 2 | Add a single card to a deck |
| AI Card Generation | 5 | Generate a card using AI |
| Deck Export | 3 | Export a deck to various formats |
These costs are defined in `backend/src/config/credit-operations.ts`.
## Architecture
```
┌─────────────┐
│ Frontend │
│ (React Native)
└─────┬───────┘
│ 1. Create Deck Request
│ POST /api/decks
│ { name, description }
┌─────────────┐
│ Backend │
│ (NestJS) │
├─────────────┤
│ API │ 2. Validate Credits (10 mana)
│ Controller │ ├─ Has credits? → Continue
│ │ └─ No credits? → Return 400
│ │
│ │ 3. Create Deck (business logic)
│ │
│ │ 4. Consume Credits (10 mana)
│ │
│ │ 5. Return Success + Credits Used
└─────┬───────┘
┌─────────────┐
│ Mana Core │ - Validate balance
│ Service │ - Deduct credits
│ │ - Record transaction
└─────────────┘
```
## Backend Integration
### 1. Service Key Configuration
The backend needs a service key from Mana Core to perform credit operations.
**backend/.env**:
```env
SERVICE_KEY=your-service-key-from-mana-core
```
**backend/src/app.module.ts**:
```typescript
ManaCoreModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
serviceKey: configService.get<string>('SERVICE_KEY', ''),
// ... other config
}),
inject: [ConfigService],
})
```
### 2. Credit Operations
**backend/src/config/credit-operations.ts**:
```typescript
export enum CreditOperationType {
DECK_CREATION = 'deck_creation',
CARD_CREATION = 'card_creation',
// Add more as needed
}
export const CREDIT_COSTS: Record<CreditOperationType, number> = {
[CreditOperationType.DECK_CREATION]: 10,
[CreditOperationType.CARD_CREATION]: 2,
};
```
### 3. Controller Implementation
**backend/src/controllers/api.controller.ts**:
```typescript
import { CreditClientService } from '@mana-core/nestjs-integration/services';
import { CreditOperationType, getCreditCost } from '../config/credit-operations';
@Controller('api')
@UseGuards(AuthGuard)
export class ApiController {
constructor(private readonly creditClient: CreditClientService) {}
@Post('decks')
async createDeck(@CurrentUser() user: any, @Body() deckData: any) {
const operationType = CreditOperationType.DECK_CREATION;
const creditCost = getCreditCost(operationType);
// 1. Validate credits BEFORE operation
const validation = await this.creditClient.validateCredits(
user.id,
operationType,
creditCost,
);
if (!validation.hasCredits) {
throw new BadRequestException({
error: 'insufficient_credits',
message: `Insufficient mana. Required: ${creditCost}, Available: ${validation.availableCredits}`,
requiredCredits: creditCost,
availableCredits: validation.availableCredits,
});
}
// 2. Perform the operation
const newDeck = await this.createDeckInDatabase(deckData);
// 3. Consume credits AFTER success
await this.creditClient.consumeCredits(
user.id,
operationType,
creditCost,
`Created deck: ${deckData.name}`,
{ deckId: newDeck.id },
);
return { success: true, deck: newDeck, creditsUsed: creditCost };
}
}
```
### 4. Credit Balance Endpoint
Get user's current credit balance:
```typescript
@Get('credits/balance')
async getCreditBalance(@CurrentUser() user: any) {
const balance = await this.creditClient.getCreditBalance(user.id);
return {
userId: user.id,
balance: balance.balance || 0,
currency: 'mana',
};
}
```
## Frontend Integration
### 1. Types
**apps/mobile/types/credits.ts**:
```typescript
export interface InsufficientCreditsError {
error: 'insufficient_credits';
message: string;
requiredCredits: number;
availableCredits: number;
operation?: string;
}
export function isInsufficientCreditsError(error: any): boolean {
return error && error.error === 'insufficient_credits';
}
```
### 2. Credit Service
**apps/mobile/services/creditService.ts**:
```typescript
export const creditService = {
async getBalance(): Promise<number> {
const response = await get(`${API_URL}/api/credits/balance`);
return response.balance || 0;
},
};
```
### 3. Insufficient Credits Modal
**apps/mobile/components/InsufficientCreditsModal.tsx**:
A pre-built modal component that displays:
- Required vs available credits
- Shortfall amount
- "Get More Mana" button (optional)
- Cancel button
### 4. Hook for Easy Integration
**apps/mobile/hooks/useInsufficientCredits.ts**:
```typescript
export function useInsufficientCredits() {
// ... state management
return {
visible,
requiredCredits,
availableCredits,
operation,
handleCreditError, // Automatically shows modal for credit errors
hideInsufficientCredits,
};
}
```
### 5. Usage Example
**In any screen that creates a deck**:
```typescript
import { useInsufficientCredits } from '../hooks/useInsufficientCredits';
import { InsufficientCreditsModal } from '../components/InsufficientCreditsModal';
import { creditService } from '../services/creditService';
function DeckCreationScreen() {
const [creditBalance, setCreditBalance] = useState(0);
const insufficientCredits = useInsufficientCredits();
// Load balance
useEffect(() => {
creditService.getBalance().then(setCreditBalance);
}, []);
const handleCreateDeck = async () => {
try {
const response = await post('/api/decks', deckData);
Alert.alert('Success', `Deck created! ${response.creditsUsed} mana used.`);
// Refresh balance
const newBalance = await creditService.getBalance();
setCreditBalance(newBalance);
} catch (error) {
// Automatically shows modal if it's a credit error
if (!insufficientCredits.handleCreditError(error)) {
// Handle other errors
Alert.alert('Error', error.message);
}
}
};
return (
<View>
{/* Show balance */}
<Text>Your Mana: {creditBalance} ⚡</Text>
{/* Create deck button */}
<Button title="Create Deck (10 mana)" onPress={handleCreateDeck} />
{/* Insufficient credits modal */}
<InsufficientCreditsModal
visible={insufficientCredits.visible}
requiredCredits={insufficientCredits.requiredCredits}
availableCredits={insufficientCredits.availableCredits}
operation={insufficientCredits.operation}
onClose={insufficientCredits.hideInsufficientCredits}
onPurchase={() => {/* Navigate to purchase */}}
/>
</View>
);
}
```
## Error Handling
### Backend Error Response
When a user has insufficient credits:
```json
{
"error": "insufficient_credits",
"message": "Insufficient mana. Required: 10, Available: 5",
"requiredCredits": 10,
"availableCredits": 5,
"operation": "Create a new deck"
}
```
Status code: **400 Bad Request**
### Frontend Error Handling
The `useInsufficientCredits` hook automatically detects credit errors:
```typescript
const insufficientCredits = useInsufficientCredits();
try {
await createDeck();
} catch (error) {
// Returns true if it was a credit error (modal shown automatically)
const wasCreditError = insufficientCredits.handleCreditError(error);
if (!wasCreditError) {
// Handle other errors
Alert.alert('Error', error.message);
}
}
```
## Testing
### Manual Testing Steps
1. **Check credit balance**:
```bash
curl -H "Authorization: Bearer $TOKEN" \
https://manadeck-backend-111768794939.europe-west3.run.app/api/credits/balance
```
2. **Create deck with sufficient credits**:
```bash
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Test Deck","description":"Testing"}' \
https://manadeck-backend-111768794939.europe-west3.run.app/api/decks
```
3. **Create deck with insufficient credits**:
- Repeat step 2 until balance is < 10
- Should return 400 with `insufficient_credits` error
### Unit Testing
Mock the `CreditClientService` in tests:
```typescript
{
provide: CreditClientService,
useValue: {
validateCredits: jest.fn().mockResolvedValue({
hasCredits: true,
availableCredits: 100,
}),
consumeCredits: jest.fn().mockResolvedValue({
success: true,
transactionId: 'txn_123',
}),
},
}
```
## Best Practices
### ✅ DO
1. **Always validate BEFORE the operation**
```typescript
const validation = await creditClient.validateCredits(...);
if (!validation.hasCredits) throw error;
// Then do the operation
const result = await performOperation();
// Then consume
await creditClient.consumeCredits(...);
```
2. **Consume AFTER success**
- Only consume credits if the operation succeeds
- This prevents charging users for failed operations
3. **Include metadata in consumption**
```typescript
await creditClient.consumeCredits(
userId,
operationType,
cost,
'Human-readable description',
{ deckId, deckName } // Useful for audit logs
);
```
4. **Refresh balance after operations**
- Show users their updated balance
- Prevents confusion about remaining credits
### ❌ DON'T
1. **Don't consume without validation**
```typescript
// BAD: What if they don't have credits?
await performOperation();
await creditClient.consumeCredits(...);
```
2. **Don't consume before the operation**
```typescript
// BAD: They'll be charged even if operation fails
await creditClient.consumeCredits(...);
await performOperation();
```
3. **Don't hardcode credit costs**
```typescript
// BAD
const cost = 10;
// GOOD
const cost = getCreditCost(CreditOperationType.DECK_CREATION);
```
## Adding New Billable Operations
To add a new operation (e.g., "AI Card Description"):
### 1. Backend
**backend/src/config/credit-operations.ts**:
```typescript
export enum CreditOperationType {
// ... existing
AI_CARD_DESCRIPTION = 'ai_card_description',
}
export const CREDIT_COSTS: Record<CreditOperationType, number> = {
// ... existing
[CreditOperationType.AI_CARD_DESCRIPTION]: 3,
};
```
### 2. Controller
```typescript
@Post('cards/:id/generate-description')
async generateDescription(@CurrentUser() user: any, @Param('id') cardId: string) {
const operationType = CreditOperationType.AI_CARD_DESCRIPTION;
const cost = getCreditCost(operationType);
// Validate
const validation = await this.creditClient.validateCredits(user.id, operationType, cost);
if (!validation.hasCredits) throw insufficientCreditsError;
// Perform
const description = await this.aiService.generateDescription(cardId);
// Consume
await this.creditClient.consumeCredits(
user.id,
operationType,
cost,
`Generated description for card ${cardId}`,
{ cardId, description: description.substring(0, 50) }
);
return { description, creditsUsed: cost };
}
```
### 3. Frontend
Use the same pattern as deck creation - the modal and hooks work for all operations automatically!
## Troubleshooting
### "Service key not configured" error
**Problem**: Credits operations fail with authentication error.
**Solution**: Add `SERVICE_KEY` to your `.env` file:
```env
SERVICE_KEY=your-actual-service-key-from-mana-core
```
### Credits not being deducted
**Problem**: Operations succeed but credits don't decrease.
**Check**:
1. Is `consumeCredits()` being called?
2. Is it being called AFTER the operation succeeds?
3. Check backend logs for errors
### Frontend not showing insufficient credits modal
**Problem**: Users get an error but modal doesn't appear.
**Check**:
1. Is `useInsufficientCredits` hook being used?
2. Is `handleCreditError()` being called in the catch block?
3. Is `InsufficientCreditsModal` component rendered with hook props?
## Resources
- [Mana Core Documentation](https://docs.mana-core.com)
- [Mana Core NestJS Package](https://github.com/Memo-2023/mana-core-nestjs-package)
- [Integration Guide](./MANA_CORE_INTEGRATION_GUIDE.md)
- [Example Implementation](./apps/mobile/examples/DeckCreationExample.tsx)

View file

@ -0,0 +1,298 @@
# Manadeck Backend Deployment Checklist
This checklist ensures you have everything configured for automated deployment.
## ✅ Prerequisites
### 1. GitHub Secrets (Required)
Go to `https://github.com/Memo-2023/manadeck` → Settings → Secrets and variables → Actions
Add these secrets:
| Secret Name | Description | How to Get |
|------------|-------------|------------|
| `GCP_SA_KEY_PROD` | Service account JSON key for Cloud Run deployment | See "Create Service Account" below |
| `CLOUD_RUN_SERVICE_ACCOUNT` | Service account email | `manadeck-backend-sa@memo-2c4c4.iam.gserviceaccount.com` |
| `GH_PERSONAL_TOKEN` | GitHub Personal Access Token for private packages | See "Create GitHub PAT" below |
#### Create Service Account
```bash
# 1. Create service account
gcloud iam service-accounts create manadeck-backend-sa \
--display-name="Manadeck Backend Service Account" \
--project=memo-2c4c4
# 2. Grant permissions
SA_EMAIL="manadeck-backend-sa@memo-2c4c4.iam.gserviceaccount.com"
gcloud projects add-iam-policy-binding memo-2c4c4 \
--member="serviceAccount:${SA_EMAIL}" \
--role="roles/run.admin"
gcloud projects add-iam-policy-binding memo-2c4c4 \
--member="serviceAccount:${SA_EMAIL}" \
--role="roles/iam.serviceAccountUser"
gcloud projects add-iam-policy-binding memo-2c4c4 \
--member="serviceAccount:${SA_EMAIL}" \
--role="roles/artifactregistry.writer"
# 3. Create and download key
gcloud iam service-accounts keys create manadeck-sa-key.json \
--iam-account=${SA_EMAIL} \
--project=memo-2c4c4
# 4. Copy contents of manadeck-sa-key.json to GCP_SA_KEY_PROD secret
cat manadeck-sa-key.json
# 5. Delete local key file (security best practice)
rm manadeck-sa-key.json
```
#### Create GitHub Personal Access Token
1. Go to https://github.com/settings/tokens
2. Click "Generate new token (classic)"
3. Name: `Manadeck CI/CD`
4. Expiration: Choose appropriate timeframe
5. Scopes: Select `repo` (Full control of private repositories)
6. Click "Generate token"
7. Copy token and add to `GH_PERSONAL_TOKEN` secret
### 2. GCP Artifact Registry
```bash
# Create repository for Docker images
gcloud artifacts repositories create manadeck-backend \
--repository-format=docker \
--location=europe-west3 \
--project=memo-2c4c4 \
--description="Docker images for Manadeck Backend"
```
### 3. GCP Secrets (Required)
Run the interactive script:
```bash
cd backend
./create-secrets.sh
```
Or create manually:
```bash
# All secrets go to mana-core-453821 project
PROJECT_ID="mana-core-453821"
# Generate service key
SERVICE_KEY=$(openssl rand -base64 32)
# Create secrets
echo "your-app-id" | gcloud secrets create MANADECK_APP_ID --data-file=- --project=$PROJECT_ID
echo "$SERVICE_KEY" | gcloud secrets create MANADECK_SERVICE_KEY --data-file=- --project=$PROJECT_ID
echo "https://xxx.supabase.co" | gcloud secrets create MANADECK_SUPABASE_URL --data-file=- --project=$PROJECT_ID
echo "your-anon-key" | gcloud secrets create MANADECK_SUPABASE_ANON_KEY --data-file=- --project=$PROJECT_ID
echo "your-service-key" | gcloud secrets create MANADECK_SUPABASE_SERVICE_KEY --data-file=- --project=$PROJECT_ID
echo "https://app.com/welcome" | gcloud secrets create MANADECK_SIGNUP_REDIRECT_URL --data-file=- --project=$PROJECT_ID
# Grant access to service account
SA_EMAIL="manadeck-backend-sa@memo-2c4c4.iam.gserviceaccount.com"
for SECRET in MANA_SERVICE_URL MANADECK_APP_ID MANADECK_SERVICE_KEY MANADECK_SUPABASE_URL MANADECK_SUPABASE_ANON_KEY MANADECK_SUPABASE_SERVICE_KEY MANADECK_SIGNUP_REDIRECT_URL; do
gcloud secrets add-iam-policy-binding $SECRET \
--member="serviceAccount:${SA_EMAIL}" \
--role="roles/secretmanager.secretAccessor" \
--project=$PROJECT_ID
done
```
**IMPORTANT**: Add the generated `SERVICE_KEY` to mana-core-middleware's `APP_SERVICE_KEYS`:
```
APP_SERVICE_KEYS=existing-apps,YOUR_APP_ID:YOUR_SERVICE_KEY
```
## 🚀 Deployment Process
### Automatic Deployment (GitHub Actions)
1. Push to `main` branch:
```bash
git add .
git commit -m "feat: your changes"
git push origin main
```
2. GitHub Actions will automatically:
- ✅ Run tests and linting
- ✅ Build Docker image
- ✅ Push to Artifact Registry
- ✅ Deploy to Cloud Run
- ✅ Run health checks
- ✅ Rollback on failure
3. Monitor deployment:
- Go to https://github.com/Memo-2023/manadeck/actions
- View workflow run progress
### Manual Deployment (Cloud Build)
```bash
cd backend
# Update version in cloudbuild.yaml (e.g., v1.0.0 → v1.0.1)
# Build and push
gcloud builds submit --project=memo-2c4c4 --config=cloudbuild.yaml .
# Deploy
gcloud run deploy manadeck-backend \
--image=europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:v1.0.1 \
--project=memo-2c4c4 \
--region=europe-west3
```
## 🔍 Verification
### Check Deployment Status
```bash
# Get service URL
gcloud run services describe manadeck-backend \
--project=memo-2c4c4 \
--region=europe-west3 \
--format='value(status.url)'
# Test health endpoint
curl https://manadeck-backend-xxx.run.app/health
# Test liveness
curl https://manadeck-backend-xxx.run.app/health/live
```
### View Logs
```bash
# Recent logs
gcloud logging read "resource.type=cloud_run_revision AND resource.labels.service_name=manadeck-backend" \
--project=memo-2c4c4 \
--limit=50
# Error logs only
gcloud logging read "resource.type=cloud_run_revision AND resource.labels.service_name=manadeck-backend AND severity>=ERROR" \
--project=memo-2c4c4 \
--limit=20
```
## 🔧 Troubleshooting
### GitHub Actions Fails with "npm ci" Error
**Problem**: Private package `@mana-core/nestjs-integration` can't be installed
**Solution**: Verify `GH_PERSONAL_TOKEN` secret is set with `repo` scope
### Deployment Fails with "Permission Denied" on Secrets
**Problem**: Service account can't access secrets in `mana-core-453821`
**Solution**: Grant cross-project secret access:
```bash
SA_EMAIL="manadeck-backend-sa@memo-2c4c4.iam.gserviceaccount.com"
gcloud secrets add-iam-policy-binding MANADECK_APP_ID \
--member="serviceAccount:${SA_EMAIL}" \
--role="roles/secretmanager.secretAccessor" \
--project=mana-core-453821
```
### Health Check Fails After Deployment
**Problem**: Service starts but health endpoint returns 500
**Possible causes**:
1. Missing environment variables/secrets
2. Can't connect to Supabase
3. Can't connect to Mana Core
**Debug**:
```bash
# Check service logs
gcloud logging read "resource.type=cloud_run_revision AND resource.labels.service_name=manadeck-backend" \
--project=memo-2c4c4 \
--limit=20
# Check secret values (if you have permissions)
gcloud secrets versions access latest --secret=MANADECK_APP_ID --project=mana-core-453821
```
### Peer Dependency Warning
**Problem**: `@mana-core/nestjs-integration` has peer dependency on `@nestjs/common@^10.0.0` but project uses `^11.0.0`
**Solution**: Already handled with `--legacy-peer-deps` flag in workflow. If you see this locally:
```bash
npm install --legacy-peer-deps
```
## 📊 Project Structure
```
manadeck/
├── .github/
│ └── workflows/
│ └── deploy-backend.yml # GitHub Actions workflow
├── backend/
│ ├── src/ # Source code
│ ├── Dockerfile # Container definition
│ ├── cloudbuild.yaml # Manual deployment config
│ ├── create-secrets.sh # Interactive secrets setup
│ ├── verify-build.sh # Local build verification
│ ├── DEPLOY_MANUAL.md # Detailed deployment docs
│ └── package.json
├── apps/
│ ├── mobile/
│ ├── web/
│ └── landing/
└── DEPLOYMENT_CHECKLIST.md # This file
```
## 📝 Configuration Summary
| Component | Location | Value |
|-----------|----------|-------|
| **Deployment Project** | GCP | `memo-2c4c4` |
| **Secrets Project** | GCP | `mana-core-453821` |
| **Region** | GCP | `europe-west3` |
| **Service Name** | Cloud Run | `manadeck-backend` |
| **Image Registry** | Artifact Registry | `europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend` |
| **Port** | Container | `8080` |
| **Repository** | GitHub | `Memo-2023/manadeck` |
## 🎯 Quick Start
**First-time setup**:
```bash
# 1. Create GCP resources
./backend/create-secrets.sh
# 2. Add GitHub secrets (see "GitHub Secrets" section)
# 3. Push to trigger deployment
git push origin main
```
**After setup**:
```bash
# Just push to deploy
git add .
git commit -m "feat: your changes"
git push origin main
```
---
**Last Updated**: 2025-09-30
**Maintainer**: Development Team

View file

@ -0,0 +1,143 @@
# Edge Function Authentication Fix
## Problem
The Edge Function tries to validate Mana app tokens as Supabase JWTs using `supabase.auth.getUser()`, but Mana tokens are signed by a different Supabase project and need to be verified using the JWKS endpoint.
## Solution
Update the Edge Function to verify Mana tokens using the jose library and JWKS:
```typescript
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3';
import * as jose from "https://deno.land/x/jose@v5.9.6/index.ts";
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
};
Deno.serve(async (req) => {
// Handle CORS preflight requests
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
// Get the authorization header
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
throw new Error('No authorization header');
}
// Extract the Mana app token
const appToken = authHeader.replace('Bearer ', '');
// Get Mana Core JWKS URL from environment variable
// This should be: https://your-mana-project.supabase.co/auth/v1/.well-known/jwks.json
const manaJwksUrl = Deno.env.get('MANA_JWKS_URL');
if (!manaJwksUrl) {
throw new Error('MANA_JWKS_URL not configured');
}
// Verify the Mana token using JWKS
const JWKS = jose.createRemoteJWKSet(new URL(manaJwksUrl));
const { payload } = await jose.jwtVerify(appToken, JWKS);
const userId = payload.sub as string;
if (!userId) {
throw new Error('Invalid token: no user ID');
}
console.log(`Authenticated user: ${userId}`);
// Initialize Supabase client with service role
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
const supabase = createClient(supabaseUrl, supabaseServiceKey);
// Use the userId from the Mana token
const user = { id: userId };
// Parse request body
const requestData = await req.json();
const {
prompt: userPrompt,
deckTitle,
deckDescription = '',
cardCount = 10,
cardTypes = ['flashcard', 'quiz'],
difficulty = 'intermediate',
tags = []
} = requestData;
// Validate input
if (!userPrompt || !deckTitle) {
throw new Error('userPrompt and deckTitle are required');
}
if (cardCount < 1 || cardCount > 50) {
throw new Error('cardCount must be between 1 and 50');
}
// Continue with OpenAI call and rest of your existing function code...
} catch (error) {
console.error('Error in generate-deck function:', error);
return new Response(JSON.stringify({
success: false,
error: error.message || 'Ein unerwarteter Fehler ist aufgetreten'
}), {
headers: {
...corsHeaders,
'Content-Type': 'application/json'
},
status: error.message?.includes('authorization') ? 401 : 400
});
}
});
```
## Critical: Disable Gateway JWT Verification
The Supabase Edge Gateway tries to validate JWTs before passing requests to your function. Since you're using Mana Core JWTs (not Manadeck JWTs), you need to disable this.
Create `supabase/functions/generate-deck/config.toml`:
```toml
# Disable automatic JWT verification by Supabase Edge Gateway
# We'll manually verify the Mana Core JWT inside the function
verify_jwt = false
```
Do the same for `generate-deck-from-image`:
```toml
# supabase/functions/generate-deck-from-image/config.toml
verify_jwt = false
```
## Environment Variables Needed
Add this to your Supabase Edge Function secrets:
```bash
# Get your Mana Core project reference from the Supabase dashboard
supabase secrets set MANA_JWKS_URL=https://zvwwwwtcmhnxcjiswrln.supabase.co/auth/v1/.well-known/jwks.json
```
Replace `zvwwwwtcmhnxcjiswrln` with your actual Mana Core Supabase project reference.
## How It Works
1. ✅ Receives Mana app token from Authorization header
2. ✅ Verifies token signature using Mana Core JWKS endpoint
3. ✅ Validates token expiration automatically (jose handles this)
4. ✅ Extracts user ID from verified token payload
5. ✅ Uses Supabase service role for database operations
6. ✅ Associates created decks with the correct user ID
## Benefits
- Properly validates asymmetric ECC P-256 signed JWTs
- Handles key rotation automatically via JWKS
- More secure than manual JWT decoding
- Works with Supabase's new JWT signing keys

View file

@ -0,0 +1,591 @@
# Mana Core Architecture in Storyteller
This document explains the architecture and data flow of the Mana Core integration in the Storyteller project.
## System Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ Frontend Layer │
│ (React Native + Expo) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Auth Service │ │ API Client │ │Token Manager │ │
│ │ │ │ │ │ │ │
│ │ • Sign In │ │ • fetchWith │ │ • getValid │ │
│ │ • Sign Up │ │ Auth() │ │ Token() │ │
│ │ • Sign Out │ │ • Auto │ │ • refresh │ │
│ │ • Device │ │ Refresh │ │ Token() │ │
│ │ Info │ │ • Error │ │ • Token │ │
│ │ │ │ Handling │ │ Storage │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
└─────────┼──────────────────┼──────────────────┼────────────────┘
│ │ │
│ HTTP/HTTPS │ Bearer Token │ Refresh
│ Requests │ in Headers │ Flow
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Backend Layer │
│ (NestJS + TypeScript) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Mana Core NestJS Integration │ │
│ ├───────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ AuthGuard │ │@CurrentUser()│ │ Credit │ │ │
│ │ │ │ │ │ │ Client │ │ │
│ │ │ • Validate │ │ • Extract │ │ Service │ │ │
│ │ │ JWT │ │ User ID │ │ │ │ │
│ │ │ • Check │ │ • Extract │ │ • validate │ │ │
│ │ │ Expiry │ │ Email │ │ Credits() │ │ │
│ │ │ • Inject │ │ • Extract │ │ • consume │ │ │
│ │ │ User │ │ Role │ │ Credits() │ │ │
│ │ │ │ │ │ │ • get │ │ │
│ │ │ │ │ │ │ Balance() │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────┼──────────────────────────────────┐ │
│ │ Application Controllers & Services │ │
│ ├────────────────────────┼──────────────────────────────────┤ │
│ │ │ │ │
│ │ CharacterController │ StoryController │ │
│ │ • generateCharacter │ • createStory │ │
│ │ • getCharacters │ • getStories │ │
│ │ • updateCharacter │ • updateStory │ │
│ │ • deleteCharacter │ • deleteStory │ │
│ │ │ │ │
│ │ SettingsController │ CreatorsController │ │
│ │ • getUserSettings │ • getCreators │ │
│ │ • updateSettings │ • getLanguages │ │
│ │ │ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
└───────────────────────────┼─────────────────────────────────────┘
│ API Requests
│ (Auth Validation,
│ Credit Operations)
┌─────────────────────────────────────────────────────────────────┐
│ Mana Core Service │
│ (Authentication & Credits) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Authentication │ │ Credit Management │ │
│ │ │ │ │ │
│ │ • User Management │ │ • Balance Tracking │ │
│ │ • Token Generation │ │ • Transaction Log │ │
│ │ • Token Validation │ │ • Operation Types │ │
│ │ • Token Refresh │ │ • App-Level Track │ │
│ │ • Device Management │ │ • Space Credits │ │
│ │ • OAuth Providers │ │ • Billing History │ │
│ │ │ │ │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
---
## Authentication Flow
### Sign-In Flow
```
┌────────┐ ┌────────┐ ┌────────┐
│ Mobile │ │Backend │ │ Mana │
│ App │ │ (Nest) │ │ Core │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
│ 1. Sign In Request │ │
│ POST /auth/signin │ │
│ { email, password, │ │
│ deviceInfo } │ │
├────────────────────────────►│ │
│ │ │
│ │ 2. Forward to Mana │
│ │ POST /auth/signin │
│ ├────────────────────────────►│
│ │ │
│ │ │ 3. Validate
│ │ │ Credentials
│ │ │
│ │ 4. Return Tokens │
│ │ { appToken, refreshToken, │
│ │ user, device } │
│ │◄────────────────────────────┤
│ │ │
│ 5. Return to Client │ │
│ { appToken, refreshToken } │ │
│◄────────────────────────────┤ │
│ │ │
│ 6. Store Tokens │ │
│ in SecureStorage │ │
│ │ │
```
### Token Refresh Flow
```
┌────────┐ ┌────────┐ ┌────────┐
│ Mobile │ │Backend │ │ Mana │
│ App │ │ (Nest) │ │ Core │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
│ 1. API Request │ │
│ with Expired Token │ │
├────────────────────────────►│ │
│ │ │
│ │ 2. Validate Token │
│ │ (Expired!) │
│ │ │
│ 3. 401 Unauthorized │ │
│◄────────────────────────────┤ │
│ │ │
│ 4. Token Refresh Request │ │
│ POST /auth/refresh │ │
│ { refreshToken, deviceInfo }│ │
├────────────────────────────►│ │
│ │ │
│ │ 5. Forward to Mana │
│ ├────────────────────────────►│
│ │ │
│ │ │ 6. Validate
│ │ │ Refresh
│ │ │ Token
│ │ │
│ │ 7. New Tokens │
│ │◄────────────────────────────┤
│ │ │
│ 8. Return New Tokens │ │
│◄────────────────────────────┤ │
│ │ │
│ 9. Store New Tokens │ │
│ │ │
│ 10. Retry Original Request │ │
│ with New Token │ │
├────────────────────────────►│ │
│ │ │
│ 11. Success Response │ │
│◄────────────────────────────┤ │
```
---
## Protected Route Flow
### With AuthGuard
```
┌────────┐ ┌────────┐ ┌────────┐
│ Mobile │ │Backend │ │ Mana │
│ App │ │ (Nest) │ │ Core │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
│ 1. GET /character │ │
│ Authorization: Bearer │ │
│ eyJhbGc... │ │
├────────────────────────────►│ │
│ │ │
│ │ 2. AuthGuard │
│ │ intercepts │
│ │ │
│ │ 3. Validate Token │
│ ├────────────────────────────►│
│ │ │
│ │ 4. Token Valid │
│ │ + User Payload │
│ │◄────────────────────────────┤
│ │ │
│ │ 5. Inject user into │
│ │ request object │
│ │ │
│ │ 6. Execute Controller │
│ │ Method │
│ │ @CurrentUser() │
│ │ extracts user │
│ │ │
│ 7. Response with Data │ │
│◄────────────────────────────┤ │
│ │ │
```
---
## Credit Management Flow
### Character Creation (20 Credits)
```
┌────────┐ ┌────────┐ ┌────────┐
│ Mobile │ │Backend │ │ Mana │
│ App │ │ (Nest) │ │ Core │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
│ 1. Create Character Request │ │
│ POST /character/generate │ │
│ { name, description } │ │
├────────────────────────────►│ │
│ │ │
│ │ 2. Pre-flight Check │
│ │ validateCredits() │
│ │ userId, "character_ │
│ │ creation", 20 │
│ ├────────────────────────────►│
│ │ │
│ │ │ 3. Check
│ │ │ Balance
│ │ │
│ │ 4. Validation Result │
│ │ { hasCredits: true, │
│ │ availableCredits: 100 } │
│ │◄────────────────────────────┤
│ │ │
│ │ 5. If hasCredits = false │
│ │ Return error │
│◄────────────────────────────┤ │
│ │ │
│ 6. Show "Buy Credits" Modal │ │
│ │ │
│ │ 7. If hasCredits = true │
│ │ Proceed with creation │
│ │ • Generate images │
│ │ • Store in database │
│ │ │
│ │ 8. Consume Credits │
│ │ consumeCredits() │
│ │ userId, "character_ │
│ │ creation", 20, │
│ │ "Created: Cat" │
│ ├────────────────────────────►│
│ │ │
│ │ │ 9. Deduct
│ │ │ Credits
│ │ │
│ │ 10. Transaction Receipt │
│ │ { transactionId, │
│ │ remainingBalance: 80 } │
│ │◄────────────────────────────┤
│ │ │
│ 11. Success Response │ │
│ { data: { characterId, │ │
│ images, ... } } │ │
│◄────────────────────────────┤ │
│ │ │
```
### Story Creation (100 Credits)
```
Same flow as character creation, but with:
- operationType: "story_creation"
- amount: 100
- More expensive due to:
* Story text generation
* Multiple image generations (10 pages)
* Translation to German
```
---
## Credit Operation Types in Storyteller
```typescript
// Defined in Storyteller
type StorytellerOperations =
| 'character_creation' // 20 credits
| 'story_creation'; // 100 credits
// Credit Costs
const CREDIT_COSTS = {
character_creation: 20, // 3 image variants
story_creation: 100, // 10-page story with images + translation
};
```
---
## Data Flow: Complete Example
### Creating a Story with Credits
1. **Frontend**: User fills story form
```typescript
const createStory = async () => {
const response = await fetchWithAuth('/story', {
method: 'POST',
body: JSON.stringify({
characters: [characterId],
storyDescription: 'A magical forest adventure',
authorId: 'author-1',
illustratorId: 'illustrator-1',
}),
});
const data = await response.json();
if (data.error === 'insufficient_credits') {
navigation.navigate('PurchaseCredits', {
required: 100,
available: data.availableCredits,
});
}
};
```
2. **Backend Controller**: Story creation endpoint
```typescript
@Post()
@UseGuards(AuthGuard)
async createStory(
@Body() dto: CreateStoryDto,
@CurrentUser() user: JwtPayload,
) {
// Pre-flight check
const validation = await this.creditClient.validateCredits(
user.sub,
'story_creation',
100,
);
if (!validation.hasCredits) {
throw new BadRequestException({
error: 'insufficient_credits',
requiredCredits: 100,
availableCredits: validation.availableCredits,
});
}
// Create story
const result = await this.storyService.createStory({
userId: user.sub,
characterId: dto.characters[0],
storyDescription: dto.storyDescription,
});
// Consume credits
await this.creditClient.consumeCredits(
user.sub,
'story_creation',
100,
`Created story: ${result.storyData.title}`,
{ storyId: result.storyData.id },
);
return result;
}
```
3. **Mana Core**: Processes credit operations
- Validates user has sufficient credits
- Deducts credits from user's balance
- Records transaction in ledger
- Returns transaction ID and new balance
- Tracks operation by app ID
---
## Security Architecture
### Token Security
```
┌─────────────────────────────────────────────────────────┐
│ Token Structure │
├─────────────────────────────────────────────────────────┤
│ │
│ Access Token (appToken): │
│ • JWT format │
│ • Short-lived (15 minutes) │
│ • Contains: { sub, email, role, exp, iat } │
│ • Used for API authentication │
│ • Stored in SecureStorage (mobile) │
│ │
│ Refresh Token: │
│ • Long-lived (30 days) │
│ • Used to get new access token │
│ • Single-use (rotated on refresh) │
│ • Device-specific │
│ • Stored in SecureStorage (mobile) │
│ │
└─────────────────────────────────────────────────────────┘
```
### AuthGuard Protection
```typescript
// All routes under @UseGuards(AuthGuard) are protected:
✅ Validates JWT signature
✅ Checks token expiration
✅ Extracts user payload
✅ Injects user into request
✅ Returns 401 if invalid
// Usage:
@Controller('character')
@UseGuards(AuthGuard) // ← Protects all routes in controller
export class CharacterController {
@Get()
getCharacters(@CurrentUser() user: JwtPayload) {
// user is automatically available here
}
}
```
---
## Database Integration (Supabase RLS)
### Row Level Security with JWT
Storyteller uses Supabase with Row Level Security (RLS). The `@UserToken()` decorator extracts the raw JWT for RLS:
```typescript
@Get()
async getCharacters(
@CurrentUser() user: JwtPayload, // Validated user
@UserToken() token: string, // Raw JWT for RLS
) {
// Pass token to Supabase client
const characters = await this.supabase
.from('characters')
.select('*')
.eq('user_id', user.sub)
.setAuth(token); // RLS enforces user_id match
return { data: characters };
}
```
**Why Both Decorators?**
- `@CurrentUser()`: Validated user data for business logic
- `@UserToken()`: Raw JWT for database RLS enforcement
---
## Environment Configuration
### Backend `.env`
```env
# Mana Core (Required)
MANA_SERVICE_URL=https://mana-core-middleware-111768794939.europe-west3.run.app
APP_ID=8d2f5ddb-e251-4b3b-8802-84022a7ac77f
MANA_SUPABASE_SECRET_KEY=your-service-key
# Application
NODE_ENV=development
PORT=3002
# Database (Supabase)
MAERCHENZAUBER_SUPABASE_URL=your-supabase-url
MAERCHENZAUBER_SUPABASE_ANON_KEY=your-anon-key
```
### Frontend `.env`
```env
# Backend URL
EXPO_PUBLIC_STORYTELLER_BACKEND_URL=http://localhost:3002
# For production:
# EXPO_PUBLIC_STORYTELLER_BACKEND_URL=https://your-api.com
```
---
## Key Takeaways
### What Mana Core Provides
1. **Authentication System**
- ✅ Email/password sign-in/sign-up
- ✅ Google OAuth integration
- ✅ Apple Sign-in integration
- ✅ JWT token generation and validation
- ✅ Token refresh mechanism
- ✅ Multi-device management
2. **Credit Management**
- ✅ Balance tracking per user
- ✅ Pre-flight validation
- ✅ Transaction recording
- ✅ Operation type tracking
- ✅ App-level tracking
- ✅ Metadata support
3. **Developer Experience**
- ✅ Simple module configuration
- ✅ Guards for route protection
- ✅ Decorators for data extraction
- ✅ TypeScript support
- ✅ Error handling utilities
- ✅ Debug logging
### What You Need to Implement
1. **Application Logic**
- Define operation types
- Set credit costs
- Implement business logic
- Handle errors
2. **Frontend Integration**
- Token storage
- API client with auto-refresh
- Error handling UI
- Purchase flow (if needed)
3. **Testing**
- Unit tests with mocked services
- Integration tests
- End-to-end flows
---
## Performance Considerations
### Token Caching
- Frontend caches tokens in SecureStorage
- Token validation is fast (local check + remote if needed)
- Refresh only when needed (5-minute buffer)
### Credit Operations
- Pre-flight validation is lightweight
- Consumption happens after success
- No blocking during operations
### Best Practices
1. Always validate credits BEFORE expensive operations
2. Consume credits AFTER successful operations
3. Handle `InsufficientCreditsException` gracefully
4. Log all credit transactions for audit
---
## Next Steps
After understanding the architecture:
1. **Review the Integration Guide**: See `MANA_CORE_INTEGRATION_GUIDE.md`
2. **Follow the Checklist**: Use `MANA_CORE_INTEGRATION_CHECKLIST.md`
3. **Study the Code**: Examine Storyteller's implementation
4. **Test Thoroughly**: Ensure all flows work correctly
5. **Monitor in Production**: Track usage and errors
---
## Questions?
- **Full Guide**: `MANA_CORE_INTEGRATION_GUIDE.md`
- **Checklist**: `MANA_CORE_INTEGRATION_CHECKLIST.md`
- **Mana Docs**: https://docs.mana-core.com
- **GitHub**: https://github.com/Memo-2023/mana-core-nestjs-package

View file

@ -0,0 +1,445 @@
# Mana Core Integration Checklist
Use this checklist when integrating `@mana-core/nestjs-integration` into a new NestJS project.
## Prerequisites ✓
- [ ] NestJS v10+ application set up
- [ ] `@nestjs/config` installed
- [ ] Node.js v18+ and npm/yarn
- [ ] Mana Core credentials obtained:
- [ ] `MANA_SERVICE_URL`
- [ ] `APP_ID`
- [ ] `MANA_SUPABASE_SECRET_KEY` (service key)
---
## Backend Integration Steps
### 1. Installation
- [ ] Install the package:
```bash
npm install git+https://github.com/Memo-2023/mana-core-nestjs-package.git
```
- [ ] Verify in `package.json`:
```json
"@mana-core/nestjs-integration": "git+https://github.com/..."
```
### 2. Environment Configuration
- [ ] Create/update `.env` file:
```env
MANA_SERVICE_URL=https://your-mana-instance.com
APP_ID=your-app-id
MANA_SUPABASE_SECRET_KEY=your-service-key
NODE_ENV=development
```
- [ ] Add to `.env.example` (for team reference)
- [ ] Add `.env` to `.gitignore`
### 3. Module Configuration
- [ ] Import `ManaCoreModule` in `app.module.ts`
- [ ] Configure with `forRootAsync()`:
```typescript
ManaCoreModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
manaServiceUrl: 'your-mana-url',
appId: 'your-app-id',
serviceKey: configService.get('MANA_SUPABASE_SECRET_KEY'),
debug: configService.get('NODE_ENV') === 'development',
}),
inject: [ConfigService],
})
```
- [ ] Test backend starts without errors
### 4. Protect Routes with AuthGuard
- [ ] Import `AuthGuard` in controller:
```typescript
import { AuthGuard } from '@mana-core/nestjs-integration';
```
- [ ] Apply to controller or route:
```typescript
@Controller('protected')
@UseGuards(AuthGuard)
export class ProtectedController {}
```
- [ ] Test: Verify 401 without token
### 5. Extract User Information
- [ ] Import `@CurrentUser()` decorator:
```typescript
import { CurrentUser } from '@mana-core/nestjs-integration';
```
- [ ] Use in route handlers:
```typescript
@Get('profile')
async getProfile(@CurrentUser() user: JwtPayload) {
return { userId: user.sub, email: user.email };
}
```
- [ ] Test: Verify user data is extracted correctly
### 6. Integrate Credit System
- [ ] Inject `CreditClientService`:
```typescript
constructor(private creditClient: CreditClientService) {}
```
- [ ] Add pre-flight credit validation:
```typescript
const validation = await this.creditClient.validateCredits(
userId,
'operation_type',
creditCost,
);
```
- [ ] Add credit consumption after success:
```typescript
await this.creditClient.consumeCredits(
userId,
'operation_type',
creditCost,
'Description',
metadata,
);
```
- [ ] Handle `InsufficientCreditsException`:
```typescript
import { InsufficientCreditsException } from '@mana-core/nestjs-integration';
```
- [ ] Test: Verify credits are deducted
### 7. (Optional) Custom Token Decorator
- [ ] Create `@UserToken()` decorator for RLS:
```typescript
// decorators/user.decorator.ts
export const UserToken = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
return authHeader.substring(7);
}
return request.token;
},
);
```
- [ ] Use for database RLS:
```typescript
@Get()
async getData(
@CurrentUser() user: JwtPayload,
@UserToken() token: string,
) {
return await this.db.query(userId, token);
}
```
---
## Frontend Integration Steps
### 1. Configure API Base URL
- [ ] Create `.env` file:
```env
EXPO_PUBLIC_STORYTELLER_BACKEND_URL=http://localhost:3002
```
- [ ] Create API utility (`utils/api.ts`):
```typescript
export const API_BASE_URL = process.env.EXPO_PUBLIC_BACKEND_URL;
```
### 2. Create Auth Service
- [ ] Create `services/authService.ts`
- [ ] Implement sign-in:
```typescript
signIn: async (email: string, password: string) => {
const deviceInfo = await getDeviceInfo();
const response = await fetch(`${BACKEND_URL}/auth/signin`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, deviceInfo }),
});
const data = await response.json();
await storeTokens(data.appToken, data.refreshToken);
}
```
- [ ] Implement sign-up
- [ ] Implement sign-out
- [ ] Test: Verify tokens are stored securely
### 3. Create Token Manager
- [ ] Create `services/tokenManager.ts`
- [ ] Implement `getValidToken()`:
```typescript
getValidToken: async () => {
let token = await storage.getItem('appToken');
if (isExpiringSoon(token)) {
await this.refreshToken();
token = await storage.getItem('appToken');
}
return token;
}
```
- [ ] Implement `refreshToken()`:
```typescript
refreshToken: async () => {
const refreshToken = await storage.getItem('refreshToken');
const deviceInfo = await getDeviceInfo();
const response = await fetch(`${BACKEND_URL}/auth/refresh`, {
method: 'POST',
body: JSON.stringify({ refreshToken, deviceInfo }),
});
const data = await response.json();
await storeTokens(data.appToken, data.refreshToken);
}
```
- [ ] Test: Verify automatic refresh works
### 4. Create Authenticated API Client
- [ ] Create `fetchWithAuth()` function:
```typescript
export async function fetchWithAuth(endpoint: string, options = {}) {
const token = await tokenManager.getValidToken();
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
},
});
if (response.status === 401) {
await tokenManager.refreshToken();
// Retry request
}
return response;
}
```
- [ ] Test: Verify authenticated requests work
### 5. Handle Credit Errors
- [ ] Create error handling utility:
```typescript
export function isInsufficientCreditsError(error: any) {
return error?.error === 'insufficient_credits';
}
```
- [ ] Handle in UI:
```typescript
if (data.error === 'insufficient_credits') {
showPurchaseCreditsModal({
required: data.requiredCredits,
available: data.availableCredits,
});
}
```
- [ ] Test: Verify error is displayed correctly
### 6. Device Management
- [ ] Create `utils/deviceManager.ts`:
```typescript
export class DeviceManager {
static async getDeviceInfo() {
return {
deviceId: await getOrCreateDeviceId(),
deviceName: Platform.OS,
deviceType: Platform.OS as 'ios' | 'android' | 'web',
userAgent: getUserAgent(),
};
}
}
```
- [ ] Include in auth requests
- [ ] Test: Verify device info is sent
---
## Testing Steps
### Backend Tests
- [ ] Create unit tests with mocked services:
```typescript
{
provide: CreditClientService,
useValue: {
validateCredits: jest.fn().mockResolvedValue({
hasCredits: true,
}),
consumeCredits: jest.fn(),
},
}
```
- [ ] Create integration tests with real Mana Core module
- [ ] Test credit validation flow
- [ ] Test insufficient credits error
- [ ] Run tests: `npm run test`
### Frontend Tests
- [ ] Test authentication flow
- [ ] Test token refresh
- [ ] Test authenticated API calls
- [ ] Test credit error handling
- [ ] Run tests: `npm run test`
---
## Production Deployment
### Backend
- [ ] Set production environment variables:
```env
MANA_SERVICE_URL=https://production-mana.com
APP_ID=production-app-id
MANA_SUPABASE_SECRET_KEY=production-key
NODE_ENV=production
```
- [ ] Disable debug logging (`debug: false`)
- [ ] Test health endpoint
- [ ] Deploy and monitor logs
### Frontend
- [ ] Update `.env` for production:
```env
EXPO_PUBLIC_BACKEND_URL=https://your-api.com
```
- [ ] Build production bundle
- [ ] Test authentication flow
- [ ] Test API calls
- [ ] Deploy to stores (iOS/Android) or web
---
## Post-Integration Verification
- [ ] Sign-up flow works end-to-end
- [ ] Sign-in flow works end-to-end
- [ ] Token refresh works automatically
- [ ] Protected routes require authentication
- [ ] Credit validation prevents operations
- [ ] Credit consumption records transactions
- [ ] Insufficient credits error handled gracefully
- [ ] Sign-out clears tokens
- [ ] Multi-device support works
---
## Documentation
- [ ] Update README with Mana Core setup instructions
- [ ] Document custom operation types and credit costs
- [ ] Add environment variable documentation
- [ ] Create troubleshooting guide
- [ ] Document API endpoints
---
## Common Issues Checklist
If something doesn't work, check:
- [ ] Environment variables are set correctly
- [ ] Backend is running and accessible
- [ ] Service key is configured (for credit operations)
- [ ] Tokens are being stored and retrieved correctly
- [ ] Token expiration is being checked
- [ ] Device info is being sent with auth requests
- [ ] CORS is configured (if using web frontend)
- [ ] Network requests are not being blocked
- [ ] Debug logging is enabled for troubleshooting
---
## Support Resources
- **Full Integration Guide**: See `MANA_CORE_INTEGRATION_GUIDE.md`
- **Mana Core Docs**: https://docs.mana-core.com
- **GitHub Issues**: https://github.com/Memo-2023/mana-core-nestjs-package/issues
- **Example Code**: Check Storyteller project for working implementation
---
## Integration Complete! 🎉
Once all items are checked, your application is fully integrated with Mana Core.
**Estimated Time**: 2-4 hours for basic integration, 1-2 days for complete implementation with testing.
**Next Steps**:
1. Define your operation types and credit costs
2. Implement purchase flow for credits
3. Add analytics and monitoring
4. Set up role-based access control (if needed)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,590 @@
# Mana Core Integration Documentation
Complete documentation for integrating the `@mana-core/nestjs-integration` package into your NestJS application, based on the Storyteller project implementation.
## 📚 Documentation Overview
This documentation suite includes four comprehensive guides:
### 1. **This File** - Quick Start & Overview
Start here for a high-level understanding and quick reference.
### 2. **[Integration Guide](./MANA_CORE_INTEGRATION_GUIDE.md)** - Complete Implementation Guide
📖 **70+ pages** of detailed step-by-step instructions covering:
- Installation and setup
- Backend integration with code examples
- Frontend integration (React Native/Expo)
- Credit management implementation
- Error handling patterns
- Testing strategies
- Troubleshooting common issues
**Use this when**: You're implementing Mana Core for the first time or need detailed explanations.
### 3. **[Integration Checklist](./MANA_CORE_INTEGRATION_CHECKLIST.md)** - Step-by-Step Checklist
**Actionable checklist** with checkboxes covering:
- Prerequisites verification
- Backend integration steps
- Frontend integration steps
- Testing checklist
- Production deployment
- Post-integration verification
**Use this when**: You want a quick reference while implementing or to verify nothing was missed.
### 4. **[Architecture Guide](./MANA_CORE_ARCHITECTURE.md)** - Visual Architecture & Data Flow
🎨 **Visual diagrams and architecture** explaining:
- System architecture
- Authentication flows
- Credit management flows
- Data flow examples
- Security architecture
- Database integration
**Use this when**: You need to understand how everything fits together or explain it to your team.
---
## 🚀 Quick Start
### What is Mana Core?
Mana Core is a centralized authentication and credit management system that provides:
- **Authentication**: Email/password, Google OAuth, Apple Sign-in
- **JWT Token Management**: Automatic validation, refresh, and multi-device support
- **Credit System**: Pre-flight validation, consumption tracking, and billing
- **Guards & Decorators**: Easy-to-use NestJS integration
### How Storyteller Uses It
| Feature | Implementation | Credits |
|---------|---------------|---------|
| **User Authentication** | Email/password + OAuth | Free |
| **Character Creation** | AI image generation (3 variants) | 20 credits |
| **Story Creation** | 10-page illustrated story + translation | 100 credits |
| **Protected Routes** | All character/story endpoints | Via AuthGuard |
### Integration Time
- **Basic Setup**: 1-2 hours
- **Full Integration**: 4-8 hours
- **With Testing**: 1-2 days
---
## 📦 Installation
```bash
# Backend
cd backend
npm install git+https://github.com/Memo-2023/mana-core-nestjs-package.git
```
---
## ⚙️ Configuration
### 1. Environment Variables
**Backend `.env`**:
```env
MANA_SERVICE_URL=https://mana-core-middleware-111768794939.europe-west3.run.app
APP_ID=your-app-id
MANA_SUPABASE_SECRET_KEY=your-service-key
NODE_ENV=development
PORT=3002
```
**Frontend `.env`**:
```env
EXPO_PUBLIC_STORYTELLER_BACKEND_URL=http://localhost:3002
```
### 2. Module Setup
**`backend/src/app.module.ts`**:
```typescript
import { ManaCoreModule } from '@mana-core/nestjs-integration';
@Module({
imports: [
ManaCoreModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
manaServiceUrl: 'https://mana-core-middleware-111768794939.europe-west3.run.app',
appId: '8d2f5ddb-e251-4b3b-8802-84022a7ac77f',
serviceKey: configService.get('MANA_SUPABASE_SECRET_KEY'),
debug: configService.get('NODE_ENV') === 'development',
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
```
---
## 🔐 Authentication Example
### Backend Controller
```typescript
import {
AuthGuard,
CurrentUser,
CreditClientService,
} from '@mana-core/nestjs-integration';
@Controller('character')
@UseGuards(AuthGuard) // Protect all routes
export class CharacterController {
constructor(
private readonly creditClient: CreditClientService,
) {}
@Get()
async getCharacters(@CurrentUser() user: JwtPayload) {
// user.sub = user ID
// user.email = user email
return { data: await this.getCharactersForUser(user.sub) };
}
}
```
### Frontend API Client
```typescript
import { tokenManager } from './tokenManager';
export async function fetchWithAuth(endpoint: string, options = {}) {
const token = await tokenManager.getValidToken();
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
},
});
if (response.status === 401) {
await tokenManager.refreshToken();
// Retry...
}
return response;
}
// Usage
const response = await fetchWithAuth('/character');
const data = await response.json();
```
---
## 💳 Credit Management Example
### Backend: Validate & Consume Credits
```typescript
@Post('generate')
async generateCharacter(
@Body() dto: CreateCharacterDto,
@CurrentUser() user: JwtPayload,
) {
// 1. Pre-flight validation
const validation = await this.creditClient.validateCredits(
user.sub,
'character_creation',
20,
);
if (!validation.hasCredits) {
return {
error: 'insufficient_credits',
requiredCredits: 20,
availableCredits: validation.availableCredits,
};
}
// 2. Perform expensive operation
const character = await this.createCharacter(dto);
// 3. Consume credits after success
await this.creditClient.consumeCredits(
user.sub,
'character_creation',
20,
`Created character: ${character.name}`,
{ characterId: character.id },
);
return { data: character };
}
```
### Frontend: Handle Credit Errors
```typescript
const response = await fetchWithAuth('/character/generate', {
method: 'POST',
body: JSON.stringify({ name, description }),
});
const data = await response.json();
if (data.error === 'insufficient_credits') {
// Show purchase modal
navigation.navigate('PurchaseCredits', {
required: data.requiredCredits,
available: data.availableCredits,
});
return;
}
// Success
console.log('Character created:', data.data);
```
---
## 🔑 Key Features
### 1. AuthGuard
Protects routes automatically:
```typescript
@Controller('protected')
@UseGuards(AuthGuard) // All routes require authentication
export class ProtectedController {}
```
### 2. @CurrentUser() Decorator
Extracts authenticated user:
```typescript
// Get entire user
@Get('profile')
getProfile(@CurrentUser() user: JwtPayload) {
return { id: user.sub, email: user.email };
}
// Get specific field
@Get('id')
getUserId(@CurrentUser('sub') userId: string) {
return { userId };
}
```
### 3. Credit Client Service
Manages credits:
```typescript
// Validate before operation
const validation = await this.creditClient.validateCredits(
userId,
'operation_type',
amount,
);
// Consume after success
await this.creditClient.consumeCredits(
userId,
'operation_type',
amount,
'Description',
metadata,
);
// Check balance
const balance = await this.creditClient.getCreditBalance(userId);
```
---
## 📊 Storyteller Credit Costs
| Operation | Cost | What's Included |
|-----------|------|-----------------|
| **Character Creation** | 20 credits | 3 AI-generated image variants |
| **Story Creation** | 100 credits | 10-page illustrated story + German translation |
---
## 🎯 Authentication Flow
```
Mobile App (React Native)
│ 1. Sign In (email, password, deviceInfo)
Backend (NestJS)
│ 2. Forward to Mana Core
Mana Core Service
│ 3. Validate & Generate Tokens
Backend
│ 4. Return { appToken, refreshToken }
Mobile App
│ 5. Store in SecureStorage
│ 6. Use appToken for API calls
│ 7. Auto-refresh when expired
```
---
## 🛡️ Security Features
- ✅ **JWT-based authentication**
- ✅ **Token expiration** (15 minutes for access, 30 days for refresh)
- ✅ **Automatic token refresh** (5-minute buffer)
- ✅ **Multi-device support** with device tracking
- ✅ **Secure token storage** (SecureStorage on mobile)
- ✅ **Row Level Security** (RLS) support for Supabase
---
## 📝 Code Examples
### Sign In
**Backend** (Auto-provided by Mana Core):
```
POST /auth/signin
{
"email": "user@example.com",
"password": "password123",
"deviceInfo": { ... }
}
```
**Frontend**:
```typescript
const result = await authService.signIn(email, password);
if (result.success) {
navigation.navigate('Home');
} else {
showError(result.error);
}
```
### Protected Route
```typescript
@Get('stories')
@UseGuards(AuthGuard)
async getStories(@CurrentUser() user: JwtPayload) {
return await this.storyService.getStoriesForUser(user.sub);
}
```
### Credit Validation
```typescript
const validation = await this.creditClient.validateCredits(
user.sub,
'story_creation',
100,
);
if (!validation.hasCredits) {
throw new BadRequestException({
error: 'insufficient_credits',
requiredCredits: 100,
availableCredits: validation.availableCredits,
});
}
```
---
## 🧪 Testing
### Mock Credit Client
```typescript
{
provide: CreditClientService,
useValue: {
validateCredits: jest.fn().mockResolvedValue({
hasCredits: true,
availableCredits: 100,
}),
consumeCredits: jest.fn().mockResolvedValue({
success: true,
}),
},
}
```
### Test Protected Routes
```typescript
it('should require authentication', () => {
return request(app.getHttpServer())
.get('/character')
.expect(401);
});
it('should return data with valid token', () => {
return request(app.getHttpServer())
.get('/character')
.set('Authorization', `Bearer ${validToken}`)
.expect(200);
});
```
---
## 🚨 Common Issues
| Issue | Solution |
|-------|----------|
| **401 Unauthorized** | Check token expiration, verify service key |
| **Credit validation fails** | Verify service key, check user balance |
| **Token refresh not working** | Verify device info is sent, check backend URL |
| **Module not found** | Re-install package from GitHub |
See [Integration Guide](./MANA_CORE_INTEGRATION_GUIDE.md#troubleshooting) for detailed troubleshooting.
---
## 📖 Documentation Structure
```
MANA_CORE_README.md ← You are here (Quick start)
├── MANA_CORE_INTEGRATION_GUIDE.md ← Complete guide (read first)
│ ├── Installation
│ ├── Backend Integration
│ ├── Frontend Integration
│ ├── Credit Management
│ ├── Error Handling
│ ├── Testing
│ └── Troubleshooting
├── MANA_CORE_INTEGRATION_CHECKLIST.md ← Step-by-step checklist
│ ├── Prerequisites ✓
│ ├── Backend Steps ✓
│ ├── Frontend Steps ✓
│ ├── Testing ✓
│ └── Deployment ✓
└── MANA_CORE_ARCHITECTURE.md ← Visual architecture & flows
├── System Architecture
├── Authentication Flow
├── Credit Management Flow
├── Security Architecture
└── Data Flow Examples
```
---
## 🎓 Learning Path
### For New Integrations
1. **Read**: [Integration Guide](./MANA_CORE_INTEGRATION_GUIDE.md) (Overview + Installation)
2. **Understand**: [Architecture Guide](./MANA_CORE_ARCHITECTURE.md) (System design)
3. **Implement**: Follow [Checklist](./MANA_CORE_INTEGRATION_CHECKLIST.md)
4. **Study**: Review Storyteller code examples
5. **Test**: Verify all flows work
6. **Deploy**: Production configuration
### For Understanding Existing Implementation
1. **Review**: [Architecture Guide](./MANA_CORE_ARCHITECTURE.md)
2. **Trace**: Follow authentication and credit flows
3. **Explore**: Study Storyteller controllers
4. **Reference**: Use [Integration Guide](./MANA_CORE_INTEGRATION_GUIDE.md) for details
---
## 📊 Key Files in Storyteller
### Backend
| File | Purpose |
|------|---------|
| `backend/src/app.module.ts` | Mana Core module configuration |
| `backend/src/character/character.controller.ts` | AuthGuard + Credit usage example |
| `backend/src/story/story.controller.ts` | Credit validation + consumption |
| `backend/src/decorators/user.decorator.ts` | Custom @UserToken() for RLS |
### Frontend
| File | Purpose |
|------|---------|
| `mobile/src/utils/api.ts` | API client with auto-refresh |
| `mobile/src/services/authService.ts` | Sign-in, sign-up, sign-out |
| `mobile/src/services/tokenManager.ts` | Token management |
---
## 🔗 Resources
- **Mana Core Package**: https://github.com/Memo-2023/mana-core-nestjs-package
- **Mana Core Docs**: https://docs.mana-core.com
- **NestJS Docs**: https://docs.nestjs.com
- **Storyteller Source**: This repository
---
## 💡 Best Practices
1. ✅ **Always validate credits BEFORE expensive operations**
2. ✅ **Consume credits AFTER successful operations**
3. ✅ **Handle insufficient credits gracefully in UI**
4. ✅ **Use AuthGuard for all protected routes**
5. ✅ **Enable debug logging during development**
6. ✅ **Store tokens securely (SecureStorage)**
7. ✅ **Implement automatic token refresh**
8. ✅ **Test authentication and credit flows thoroughly**
---
## ❓ Support
### Getting Help
1. **Documentation**: Check all four documentation files
2. **Code Examples**: Study Storyteller implementation
3. **Mana Core Docs**: https://docs.mana-core.com
4. **GitHub Issues**: https://github.com/Memo-2023/mana-core-nestjs-package/issues
### Contributing
If you find issues or improvements in this documentation:
1. Create a pull request
2. Open an issue
3. Contact the team
---
## ✨ Summary
This documentation provides everything you need to integrate Mana Core into your NestJS application:
- **Complete integration guide** with step-by-step instructions
- **Actionable checklist** to track progress
- **Visual architecture diagrams** to understand the system
- **Real code examples** from Storyteller
- **Troubleshooting guides** for common issues
**Start with**: [Integration Guide](./MANA_CORE_INTEGRATION_GUIDE.md)
**Use while implementing**: [Checklist](./MANA_CORE_INTEGRATION_CHECKLIST.md)
**Reference for architecture**: [Architecture Guide](./MANA_CORE_ARCHITECTURE.md)
Good luck with your integration! 🚀

442
apps/manadeck/README.md Normal file
View file

@ -0,0 +1,442 @@
# Manadeck
A deck management system with Mana Core authentication and credit system integration.
## Features
- 🔐 **Mana Core Authentication** - Complete auth system with JWT tokens, device tracking, and automatic token refresh
- ⚡ **Credit System** - Mana-based billing for operations (10 mana to create a deck, 5 for AI features, etc.)
- 📱 **React Native/Expo** - Cross-platform mobile app (iOS, Android, Web)
- 🚀 **NestJS Backend** - Type-safe API with AuthGuard protection
- 💾 **Supabase** - Database and real-time features
- 🎨 **NativeWind** - Tailwind CSS for React Native styling
## Quick Start
### Prerequisites
- Node.js 18+
- npm or yarn
- Expo CLI (`npm install -g expo-cli`)
- Supabase account
- Mana Core credentials (APP_ID, SERVICE_KEY)
### Backend Setup
1. **Navigate to backend directory**:
```bash
cd backend
```
2. **Install dependencies**:
```bash
npm install
```
3. **Configure environment variables**:
```bash
cp .env.example .env
```
Edit `.env` and add your credentials:
```env
# Mana Core
MANA_SERVICE_URL=https://mana-core-middleware-111768794939.europe-west3.run.app
APP_ID=your-app-id-from-mana-core
SERVICE_KEY=your-service-key-from-mana-core # Required for credits
# Supabase
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_KEY=your-service-key
# Server
NODE_ENV=development
PORT=8080
```
4. **Start the backend**:
```bash
npm run start:dev
```
Backend will be available at `http://localhost:8080`
### Frontend Setup
1. **Navigate to mobile app directory**:
```bash
cd apps/mobile
```
2. **Install dependencies**:
```bash
npm install
```
3. **Configure environment**:
```bash
cp .env.example .env
```
Edit `.env`:
```env
# Supabase
EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
# Backend API
EXPO_PUBLIC_API_URL=http://localhost:8080 # For local development
# EXPO_PUBLIC_API_URL=https://your-production-backend.com # For production
```
4. **Start Expo**:
```bash
npm start
```
5. **Run on platform**:
- Press `i` for iOS simulator
- Press `a` for Android emulator
- Press `w` for web browser
## Architecture
```
┌─────────────────────┐
│ Mobile App │ React Native + Expo
│ (Frontend) │ - Authentication UI
│ │ - Credit balance display
│ │ - Deck management
└──────────┬──────────┘
│ HTTPS/JSON
│ Bearer Token Auth
┌─────────────────────┐
│ Backend API │ NestJS
│ │ - AuthGuard protection
│ │ - Credit validation
│ │ - Business logic
└──────────┬──────────┘
├─────────────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ Mana Core │ │ Supabase │
│ - Auth │ │ - Database │
│ - Credits │ │ - Storage │
│ - Transactions │ │ - Real-time │
└─────────────────┘ └──────────────────┘
```
## Credit System
Manadeck uses **Mana** as its credit currency. Operations cost credits:
| Operation | Cost |
|-----------|------|
| Create Deck | 10 mana |
| Create Card | 2 mana |
| AI Card Generation | 5 mana |
| Export Deck | 3 mana |
### How it Works
1. **Pre-flight Validation**: Backend checks if user has enough credits
2. **Operation**: Performs the requested operation (create deck, etc.)
3. **Consumption**: Deducts credits only if operation succeeds
4. **Response**: Returns success + credits used
### Frontend Integration
The frontend automatically handles insufficient credits with a modal:
```typescript
import { useInsufficientCredits } from '../hooks/useInsufficientCredits';
import { InsufficientCreditsModal } from '../components/InsufficientCreditsModal';
function MyScreen() {
const insufficientCredits = useInsufficientCredits();
const handleAction = async () => {
try {
await post('/api/decks', data);
} catch (error) {
// Automatically shows modal if insufficient credits
insufficientCredits.handleCreditError(error);
}
};
return (
<>
<Button onPress={handleAction} />
<InsufficientCreditsModal {...insufficientCredits} />
</>
);
}
```
**Full documentation**: See [CREDIT_SYSTEM.md](./CREDIT_SYSTEM.md)
## API Endpoints
### Authentication (via Mana Core)
- `POST /v1/auth/signin` - Sign in
- `POST /v1/auth/signup` - Sign up
- `POST /v1/auth/refresh` - Refresh token
- `POST /v1/auth/logout` - Sign out
### Protected Endpoints (require Bearer token)
- `GET /api/profile` - Get user profile + credit balance
- `GET /api/credits/balance` - Get credit balance
- `GET /api/decks` - List user's decks
- `POST /api/decks` - Create deck (costs 10 mana)
- `PUT /api/decks/:id` - Update deck
- `DELETE /api/decks/:id` - Delete deck
- `GET /api/cards` - List cards
- `POST /api/cards` - Create card (costs 2 mana)
### Public Endpoints
- `GET /public/health` - Health check
- `GET /public/version` - API version
- `GET /public/featured-decks` - Featured decks (optional auth)
## Project Structure
```
manadeck/
├── backend/ # NestJS backend
│ ├── src/
│ │ ├── config/
│ │ │ ├── credit-operations.ts # Credit costs & operation types
│ │ │ └── validation.schema.ts # Environment validation
│ │ ├── controllers/
│ │ │ ├── api.controller.ts # Protected endpoints
│ │ │ ├── public.controller.ts # Public endpoints
│ │ │ └── health.controller.ts # Health checks
│ │ ├── services/
│ │ │ └── supabase.service.ts # Supabase integration
│ │ ├── app.module.ts # Main module (Mana Core config)
│ │ └── main.ts # Entry point
│ ├── .env # Environment variables
│ └── package.json
├── apps/
│ ├── mobile/ # React Native/Expo app
│ ├── web/ # Web app
│ └── landing/ # Landing page
│ ├── app/ # Expo Router screens
│ │ ├── (tabs)/ # Tab navigation
│ │ └── _layout.tsx # Root layout
│ ├── components/
│ │ └── InsufficientCreditsModal.tsx # Credit error modal
│ ├── services/
│ │ ├── authService.ts # Authentication
│ │ ├── tokenManager.ts # Token refresh & management
│ │ └── creditService.ts # Credit operations
│ ├── hooks/
│ │ └── useInsufficientCredits.ts # Credit error handling
│ ├── types/
│ │ ├── auth.ts # Auth types
│ │ └── credits.ts # Credit types
│ ├── utils/
│ │ ├── apiClient.ts # Authenticated API client
│ │ └── deviceManager.ts # Device info
│ ├── examples/
│ │ └── DeckCreationExample.tsx # Usage example
│ ├── .env # Environment variables
│ └── package.json
├── CREDIT_SYSTEM.md # Credit system documentation
├── MANA_CORE_INTEGRATION_GUIDE.md
├── MANA_CORE_INTEGRATION_CHECKLIST.md
├── MANA_CORE_ARCHITECTURE.md
├── MANA_CORE_README.md
└── README.md # This file
```
## Development
### Backend
```bash
cd backend
# Development with hot reload
npm run start:dev
# Build for production
npm run build
# Run production build
npm run start:prod
# Linting
npm run lint
# Tests
npm run test
```
### Frontend
```bash
cd apps/mobile
# Start Expo dev server
npm start
# Run on iOS
npm run ios
# Run on Android
npm run android
# Run on web
npm run web
# Linting & formatting
npm run lint
npm run format
# Build with EAS
npm run build:dev # Development build
npm run build:preview # Preview build
npm run build:prod # Production build
```
## Environment Variables
### Backend Required Variables
| Variable | Description | Example |
|----------|-------------|---------|
| `MANA_SERVICE_URL` | Mana Core service URL | `https://mana-core-middleware-*.run.app` |
| `APP_ID` | Your app ID from Mana Core | `cea4bfc6-a4de-4e17-91e2-54275940156e` |
| `SERVICE_KEY` | Service key for credit operations | Get from Mana Core |
| `SUPABASE_URL` | Supabase project URL | `https://abc.supabase.co` |
| `SUPABASE_ANON_KEY` | Supabase anonymous key | Your anon key |
| `PORT` | Backend port | `8080` |
### Frontend Required Variables
| Variable | Description | Example |
|----------|-------------|---------|
| `EXPO_PUBLIC_SUPABASE_URL` | Supabase project URL | `https://abc.supabase.co` |
| `EXPO_PUBLIC_SUPABASE_ANON_KEY` | Supabase anonymous key | Your anon key |
| `EXPO_PUBLIC_API_URL` | Backend API URL | `http://localhost:8080` |
## Testing
### Backend Testing
The backend includes example tests in `backend/src/app.controller.spec.ts`. To add credit system tests:
```typescript
import { CreditClientService } from '@mana-core/nestjs-integration/services';
// Mock in test setup
{
provide: CreditClientService,
useValue: {
validateCredits: jest.fn().mockResolvedValue({
hasCredits: true,
availableCredits: 100,
}),
consumeCredits: jest.fn(),
},
}
```
### Manual Testing
1. **Test authentication**:
```bash
# Sign up
curl -X POST http://localhost:8080/v1/auth/signup \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"password123","username":"testuser"}'
# Sign in
curl -X POST http://localhost:8080/v1/auth/signin \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"password123"}'
```
2. **Test protected endpoint**:
```bash
export TOKEN="your-jwt-token-from-signin"
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/api/profile
```
3. **Test credit system**:
```bash
# Check balance
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/api/credits/balance
# Create deck (costs 10 mana)
curl -X POST http://localhost:8080/api/decks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"My Deck","description":"Test deck"}'
```
## Troubleshooting
### Backend won't start
- Check all environment variables are set
- Verify Mana Core credentials are correct
- Check if port 8080 is available
### Credits not working
- Ensure `SERVICE_KEY` is set in backend `.env`
- Check backend logs for credit validation errors
- Verify user has credits in Mana Core dashboard
### Frontend can't connect to backend
- Check `EXPO_PUBLIC_API_URL` is correct
- For Android emulator, use `http://10.0.2.2:8080` instead of `localhost`
- Verify backend is running
### Token refresh fails
- Check device info is being sent with refresh requests
- Verify refresh token is stored correctly
- Check network connectivity
## Documentation
- **[Credit System](./CREDIT_SYSTEM.md)** - Complete credit system documentation
- **[Mana Core Integration Guide](./MANA_CORE_INTEGRATION_GUIDE.md)** - Step-by-step integration
- **[Integration Checklist](./MANA_CORE_INTEGRATION_CHECKLIST.md)** - Checkboxes for tracking
- **[Architecture Guide](./MANA_CORE_ARCHITECTURE.md)** - System architecture and flows
- **[Mana Core README](./MANA_CORE_README.md)** - Quick reference
- **[Example Implementation](./apps/mobile/examples/DeckCreationExample.tsx)** - Working code example
## Resources
- [Mana Core Documentation](https://docs.mana-core.com)
- [Mana Core NestJS Package](https://github.com/Memo-2023/mana-core-nestjs-package)
- [Expo Documentation](https://docs.expo.dev)
- [NestJS Documentation](https://docs.nestjs.com)
- [Supabase Documentation](https://supabase.com/docs)
## License
Private project - All rights reserved
## Support
For issues related to:
- **Mana Core**: https://github.com/Memo-2023/mana-core-nestjs-package/issues
- **This project**: Contact the development team

View file

@ -0,0 +1,339 @@
# Manadeck Setup Guide
## What's Been Completed
Your Mana Core integration is now **100% complete** with a fully functional credit system! 🎉
### ✅ Backend (Complete)
- [x] ManaCoreModule configured with environment variables
- [x] Service key configuration for credit operations
- [x] Credit operation types defined (deck creation = 10 mana)
- [x] Credit validation before deck creation
- [x] Credit consumption after successful operations
- [x] Credit balance endpoint (`GET /api/credits/balance`)
- [x] AuthGuard protecting all API routes
- [x] User profile endpoint with credit balance
### ✅ Frontend (Complete)
- [x] Authentication service with sign in/up/out
- [x] Token manager with automatic refresh
- [x] API client with auth token injection
- [x] Device manager for multi-device support
- [x] InsufficientCreditsModal component
- [x] useInsufficientCredits hook
- [x] Credit service for balance checking
- [x] Type definitions for credit errors
- [x] Example deck creation implementation
### ✅ Documentation (Complete)
- [x] README.md with quick start
- [x] CREDIT_SYSTEM.md with full documentation
- [x] Integration guides (4 files)
- [x] Working code example
## Next Steps to Go Live
### 1. Add Your Service Key (5 minutes) ⚠️ REQUIRED
The backend needs a service key from Mana Core to validate and consume credits.
**backend/.env**:
```env
SERVICE_KEY=your-actual-service-key-here
```
**Where to get it**: Contact Mana Core admin or check your Mana Core dashboard.
Without this key, credit operations will fail with authentication errors.
### 2. Test the Credit Flow (15 minutes)
#### A. Start the backend
```bash
cd backend
npm run start:dev
```
#### B. Test authentication
```bash
# Sign up a test user
curl -X POST http://localhost:8080/v1/auth/signup \
-H "Content-Type: application/json" \
-d '{"email":"test@manadeck.com","password":"test123","username":"testuser"}'
# Sign in to get token
curl -X POST http://localhost:8080/v1/auth/signin \
-H "Content-Type: application/json" \
-d '{"email":"test@manadeck.com","password":"test123"}'
# Copy the appToken from response
export TOKEN="paste-your-token-here"
```
#### C. Test credit balance
```bash
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/api/credits/balance
```
Expected response:
```json
{
"userId": "...",
"balance": 100,
"currency": "mana",
"timestamp": "2025-..."
}
```
#### D. Test deck creation (costs 10 mana)
```bash
curl -X POST http://localhost:8080/api/decks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Test Deck","description":"My first deck"}'
```
Expected success response:
```json
{
"success": true,
"userId": "...",
"deck": {
"id": "deck_...",
"name": "Test Deck",
"description": "My first deck",
"userId": "...",
"createdAt": "2025-..."
},
"creditsUsed": 10,
"message": "Deck created successfully"
}
```
#### E. Check updated balance
```bash
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/api/credits/balance
```
Should now show `90` instead of `100`.
#### F. Test insufficient credits error
Create decks until balance < 10, then try one more time:
```bash
curl -X POST http://localhost:8080/api/decks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Another Deck"}'
```
Expected insufficient credits response (400):
```json
{
"error": "insufficient_credits",
"message": "Insufficient mana. Required: 10, Available: 5",
"requiredCredits": 10,
"availableCredits": 5,
"operation": "Create a new deck"
}
```
### 3. Test Frontend Integration (15 minutes)
#### A. Start the mobile app
```bash
cd apps/mobile
npm start
```
Press `i` for iOS, `a` for Android, or `w` for web.
#### B. Test authentication
1. Sign up with a new account
2. Verify tokens are stored
3. Sign out and sign in again
#### C. Test credit display
1. Navigate to profile or deck creation screen
2. Verify credit balance is displayed
3. Create a deck and see balance update
#### D. Test insufficient credits modal
1. Create decks until balance < 10
2. Try creating another deck
3. Verify modal appears with:
- Required: 10 mana
- Available: [your balance]
- Needed: [shortfall]
### 4. Integrate into Your Actual Screens (1-2 hours)
Copy the pattern from `apps/mobile/examples/DeckCreationExample.tsx` into your real deck creation screens.
#### Quick integration checklist:
- [ ] Import `useInsufficientCredits` hook
- [ ] Import `InsufficientCreditsModal` component
- [ ] Import `creditService`
- [ ] Load and display credit balance on screen
- [ ] Wrap API calls in try/catch
- [ ] Call `insufficientCredits.handleCreditError(error)` in catch
- [ ] Render `<InsufficientCreditsModal {...insufficientCredits} />`
### 5. Add Credit Purchase Flow (Optional)
If users can purchase credits:
1. Create a purchase screen in `apps/mobile/app/purchase-credits.tsx`
2. Implement payment integration (Stripe, in-app purchases, etc.)
3. Update `onPurchase` handler in InsufficientCreditsModal:
```typescript
onPurchase={() => {
insufficientCredits.hideInsufficientCredits();
navigation.navigate('PurchaseCredits');
}}
```
## Credit Costs Reference
These are defined in `backend/src/config/credit-operations.ts`:
| Operation | Cost | Location |
|-----------|------|----------|
| Deck Creation | 10 mana | `POST /api/decks` |
| Card Creation | 2 mana | `POST /api/cards` |
| AI Card Generation | 5 mana | `POST /api/cards/generate` |
| Deck Export | 3 mana | `POST /api/decks/:id/export` |
To change costs, edit the `CREDIT_COSTS` object in that file.
## Adding More Billable Operations
Example: Make card updates cost 1 mana:
### 1. Add to credit-operations.ts
```typescript
export enum CreditOperationType {
// ... existing
CARD_UPDATE = 'card_update',
}
export const CREDIT_COSTS = {
// ... existing
[CreditOperationType.CARD_UPDATE]: 1,
};
```
### 2. Update the endpoint
```typescript
@Put('cards/:id')
async updateCard(@CurrentUser() user: any, @Param('id') cardId: string, @Body() data: any) {
const operationType = CreditOperationType.CARD_UPDATE;
const cost = getCreditCost(operationType);
// Validate credits
const validation = await this.creditClient.validateCredits(user.id, operationType, cost);
if (!validation.hasCredits) {
throw new BadRequestException({
error: 'insufficient_credits',
requiredCredits: cost,
availableCredits: validation.availableCredits,
});
}
// Update card
const updated = await this.updateCardInDatabase(cardId, data);
// Consume credits
await this.creditClient.consumeCredits(
user.id,
operationType,
cost,
`Updated card ${cardId}`,
{ cardId }
);
return { success: true, card: updated, creditsUsed: cost };
}
```
That's it! The frontend automatically handles the error.
## Production Checklist
Before deploying to production:
### Backend
- [ ] Set `NODE_ENV=production` in production environment
- [ ] Add real `SERVICE_KEY` from Mana Core
- [ ] Configure proper CORS for your frontend domain
- [ ] Set up proper logging/monitoring
- [ ] Add rate limiting
- [ ] Set up database backups
### Frontend
- [ ] Update `EXPO_PUBLIC_API_URL` to production backend URL
- [ ] Test on real devices (iOS + Android)
- [ ] Test insufficient credits flow
- [ ] Test token refresh
- [ ] Test offline behavior
- [ ] Build production bundle
- [ ] Submit to app stores
## Common Issues & Solutions
### "Service key not configured"
**Problem**: Credit operations fail.
**Solution**: Add `SERVICE_KEY` to `backend/.env`
### Credits not deducting
**Problem**: Balance doesn't change after operations.
**Solution**: Check that `consumeCredits()` is called AFTER operation succeeds.
### Modal not showing
**Problem**: Error occurs but modal doesn't appear.
**Solution**: Make sure:
1. `useInsufficientCredits` hook is used
2. `handleCreditError()` is called in catch block
3. `InsufficientCreditsModal` is rendered
### Android can't connect to backend
**Problem**: Frontend can't reach localhost:8080.
**Solution**: Use `http://10.0.2.2:8080` for Android emulator.
## Files You Can Customize
### Credit Costs
`backend/src/config/credit-operations.ts` - Change costs here
### Modal Appearance
`apps/mobile/components/InsufficientCreditsModal.tsx` - Customize UI
### Error Messages
`backend/src/controllers/api.controller.ts` - Customize error responses
### Purchase Flow
Add `onPurchase` handler to modal in your screens
## Need Help?
1. **Credit System**: See [CREDIT_SYSTEM.md](./CREDIT_SYSTEM.md)
2. **Integration**: See [MANA_CORE_INTEGRATION_GUIDE.md](./MANA_CORE_INTEGRATION_GUIDE.md)
3. **Architecture**: See [MANA_CORE_ARCHITECTURE.md](./MANA_CORE_ARCHITECTURE.md)
4. **Example Code**: See [apps/mobile/examples/DeckCreationExample.tsx](./apps/mobile/examples/DeckCreationExample.tsx)
5. **Mana Core**: https://github.com/Memo-2023/mana-core-nestjs-package
## Summary
You now have:
- ✅ Complete authentication system
- ✅ Credit validation before operations
- ✅ Credit consumption after success
- ✅ Insufficient credits modal
- ✅ Balance checking
- ✅ Full documentation
- ✅ Working example
**Next action**: Add your `SERVICE_KEY` to `backend/.env` and run the test flow above!

View file

@ -0,0 +1,57 @@
# Dependencies
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment files - these should come from Cloud Run secrets
.env
.env.*
.env.local
.env.*.local
env.example
.env.example
# Test files
*.spec.ts
*.spec.js
test
coverage
.nyc_output
jest.config.js
# Development files
.git
.gitignore
.claude
README.md
README_*.md
*.md
DEPLOY_MANUAL.md
verify-build.sh
# Build artifacts (optional - include if building inside Docker)
dist
# IDE files
.vscode
.idea
*.swp
*.swo
# OS files
.DS_Store
Thumbs.db
.AppleDouble
.LSOverride
# Temporary files
*.tmp
*.temp
*.log
# Documentation
docs
# GitHub workflows
.github

View file

@ -0,0 +1,21 @@
# Server Configuration
NODE_ENV=development
PORT=8080
# Mana Core Configuration
MANA_SERVICE_URL=https://mana-core-middleware-111768794939.europe-west3.run.app
APP_ID=your-app-id-from-mana
MANA_SUPABASE_SECRET_KEY=your-service-key-from-mana-core # REQUIRED for credit operations
SIGNUP_REDIRECT_URL=https://manadeck.com/welcome
# PostgreSQL Database (Drizzle ORM)
DATABASE_URL=postgresql://postgres:postgres@localhost:5433/manadeck
# AI Services (Google Gemini)
GOOGLE_GENAI_API_KEY=your-google-genai-api-key # Get from https://aistudio.google.com/apikey
# JWT Configuration
JWT_SECRET=your-jwt-secret
# CORS Configuration
FRONTEND_URL=http://localhost:8081

View file

@ -0,0 +1,20 @@
# Server Configuration
NODE_ENV=production
PORT=8080
# Mana Core Configuration
MANA_SERVICE_URL=https://mana-core-middleware-dev-55965480161.europe-west3.run.app
APP_ID=cea4bfc6-a4de-4e17-91e2-54275940156e
SERVICE_KEY=your-production-service-key
SIGNUP_REDIRECT_URL=https://manadeck.com/welcome
# Supabase Configuration (Your app's database)
SUPABASE_URL=https://vksoodohrbjwyloitvsz.supabase.co
SUPABASE_ANON_KEY=sb_publishable_2ndX-kBHFpbDlL_ZeeOnfQ_ZlLI8ONk
SUPABASE_SERVICE_KEY=your-production-supabase-service-key
# JWT Configuration
JWT_SECRET=your-production-jwt-secret
# CORS Configuration
FRONTEND_URL=https://manadeck.com

1
apps/manadeck/apps/backend/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules

View file

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

View file

@ -0,0 +1,487 @@
# Manadeck Backend Deployment Manual
## Overview
This guide covers deploying the Manadeck backend service to Google Cloud Run. The service can be deployed either:
- **Automatically** via GitHub Actions (recommended)
- **Manually** via Cloud Build and gcloud CLI
## Prerequisites
1. **Google Cloud SDK** installed and authenticated:
```bash
gcloud auth login
gcloud config set project memo-2c4c4
```
2. **Docker** installed (for local testing)
3. **Access to `memo-2c4c4` project** with:
- Cloud Build API enabled
- Cloud Run API enabled
- Artifact Registry API enabled
- Secret Manager API enabled
4. **Required permissions**:
- Cloud Run Admin
- Service Account User
- Artifact Registry Writer
- Secret Manager Secret Accessor
## Initial Setup (One-Time)
### 1. Create Artifact Registry Repository
```bash
gcloud artifacts repositories create manadeck-backend \
--repository-format=docker \
--location=europe-west3 \
--project=memo-2c4c4 \
--description="Docker images for Manadeck Backend"
```
### 2. Create Service Account
```bash
# Create service account for Cloud Run
gcloud iam service-accounts create manadeck-backend-sa \
--display-name="Manadeck Backend Service Account" \
--project=memo-2c4c4
# Get service account email
SA_EMAIL="manadeck-backend-sa@memo-2c4c4.iam.gserviceaccount.com"
# Grant necessary permissions
gcloud projects add-iam-policy-binding memo-2c4c4 \
--member="serviceAccount:${SA_EMAIL}" \
--role="roles/run.admin"
gcloud projects add-iam-policy-binding memo-2c4c4 \
--member="serviceAccount:${SA_EMAIL}" \
--role="roles/iam.serviceAccountUser"
gcloud projects add-iam-policy-binding memo-2c4c4 \
--member="serviceAccount:${SA_EMAIL}" \
--role="roles/artifactregistry.writer"
```
### 3. Create Secrets in Secret Manager
**IMPORTANT**: All secrets are stored in the `mana-core-453821` project (not `memo-2c4c4`).
```bash
# Verify MANA_SERVICE_URL exists (global secret)
gcloud secrets describe MANA_SERVICE_URL --project=mana-core-453821
# Create Manadeck-specific secrets in mana-core-453821
echo "your-app-id" | gcloud secrets create MANADECK_APP_ID --data-file=- --project=mana-core-453821
echo "your-service-key" | gcloud secrets create MANADECK_SERVICE_KEY --data-file=- --project=mana-core-453821
echo "https://your-project.supabase.co" | gcloud secrets create MANADECK_SUPABASE_URL --data-file=- --project=mana-core-453821
echo "your-supabase-anon-key" | gcloud secrets create MANADECK_SUPABASE_ANON_KEY --data-file=- --project=mana-core-453821
echo "your-supabase-service-key" | gcloud secrets create MANADECK_SUPABASE_SERVICE_KEY --data-file=- --project=mana-core-453821
echo "https://yourapp.com/welcome" | gcloud secrets create MANADECK_SIGNUP_REDIRECT_URL --data-file=- --project=mana-core-453821
# Grant service account (from memo-2c4c4) access to ALL secrets in mana-core-453821
for SECRET in MANA_SERVICE_URL MANADECK_APP_ID MANADECK_SERVICE_KEY MANADECK_SUPABASE_URL MANADECK_SUPABASE_ANON_KEY MANADECK_SUPABASE_SERVICE_KEY MANADECK_SIGNUP_REDIRECT_URL; do
gcloud secrets add-iam-policy-binding $SECRET \
--member="serviceAccount:${SA_EMAIL}" \
--role="roles/secretmanager.secretAccessor" \
--project=mana-core-453821
done
```
**Why mana-core-453821?** All Mana-related secrets are centralized in this project for easier management across multiple services.
### 4. Setup GitHub Secrets (for Automated Deployment)
Go to your repository → Settings → Secrets and variables → Actions
Add these secrets:
- **GCP_SA_KEY_PROD**: Create and download a service account key:
```bash
gcloud iam service-accounts keys create key.json \
--iam-account=manadeck-backend-sa@memo-2c4c4.iam.gserviceaccount.com \
--project=memo-2c4c4
# Copy contents of key.json to GitHub secret
cat key.json
```
- **CLOUD_RUN_SERVICE_ACCOUNT**: Set to `manadeck-backend-sa@memo-2c4c4.iam.gserviceaccount.com`
## Automatic Deployment (GitHub Actions)
### Trigger Deployment
The GitHub Actions workflow automatically deploys when:
- Code is pushed to `main` branch
- Changes are made to `manadeck/backend/**` directory
- Workflow file is modified
**Manual trigger:**
1. Go to GitHub → Actions tab
2. Select "Deploy Manadeck Backend to Cloud Run"
3. Click "Run workflow"
4. Choose environment and click "Run workflow"
### Workflow Steps
1. ✅ **Test & Build Verification**
- Runs linter
- Type checks and builds
- Runs tests
2. ✅ **Build & Deploy**
- Builds Docker image
- Pushes to Artifact Registry
- Deploys to Cloud Run
- Runs health checks
3. ✅ **Rollback** (if deployment fails)
- Automatically rolls back to previous revision
- Verifies rollback health
### Monitoring Deployment
- View progress in GitHub Actions tab
- Check deployment summary in workflow run
- View logs in Cloud Logging:
```bash
gcloud logging read "resource.type=cloud_run_revision AND resource.labels.service_name=manadeck-backend" \
--project=memo-2c4c4 \
--limit=50
```
## Manual Deployment
### Option 1: Cloud Build (Build Only)
```bash
# Navigate to backend directory
cd manadeck/backend
# Submit build
gcloud builds submit --project=memo-2c4c4 --config=cloudbuild.yaml .
```
This builds and pushes the Docker image to Artifact Registry but does NOT deploy to Cloud Run.
### Option 2: Full Manual Deployment
```bash
# 1. Navigate to backend directory
cd manadeck/backend
# 2. Update version in cloudbuild.yaml
# Edit line 12 and 26: change v1.0.0 to v1.0.1 (or next version)
# 3. Build and push image
gcloud builds submit --project=memo-2c4c4 --config=cloudbuild.yaml .
# 4. Deploy to Cloud Run
gcloud run deploy manadeck-backend \
--image=europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:v1.0.0 \
--project=memo-2c4c4 \
--region=europe-west3 \
--platform=managed \
--allow-unauthenticated \
--min-instances=0 \
--max-instances=10 \
--memory=512Mi \
--cpu=1 \
--timeout=300 \
--port=8080 \
--service-account=manadeck-backend-sa@memo-2c4c4.iam.gserviceaccount.com \
--set-env-vars="NODE_ENV=production" \
--update-secrets="MANA_SERVICE_URL=projects/mana-core-453821/secrets/MANA_SERVICE_URL:latest,APP_ID=projects/mana-core-453821/secrets/MANADECK_APP_ID:latest,SERVICE_KEY=projects/mana-core-453821/secrets/MANADECK_SERVICE_KEY:latest,SUPABASE_URL=projects/mana-core-453821/secrets/MANADECK_SUPABASE_URL:latest,SUPABASE_ANON_KEY=projects/mana-core-453821/secrets/MANADECK_SUPABASE_ANON_KEY:latest,SUPABASE_SERVICE_KEY=projects/mana-core-453821/secrets/MANADECK_SUPABASE_SERVICE_KEY:latest,SIGNUP_REDIRECT_URL=projects/mana-core-453821/secrets/MANADECK_SIGNUP_REDIRECT_URL:latest"
```
## Testing Deployment
### Get Service URL
```bash
SERVICE_URL=$(gcloud run services describe manadeck-backend \
--project=memo-2c4c4 \
--region=europe-west3 \
--format='value(status.url)')
echo "Service URL: $SERVICE_URL"
```
### Test Health Endpoints
```bash
# Basic health check
curl $SERVICE_URL/health
# Liveness check
curl $SERVICE_URL/health/live
# Readiness check
curl $SERVICE_URL/health/ready
```
### Test Authenticated Endpoints
```bash
# Example: Test with JWT token
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
$SERVICE_URL/api/your-endpoint
```
## Version Management
### Updating Version
To deploy a new version:
1. **Edit `cloudbuild.yaml`**:
```yaml
# Change v1.0.0 to v1.1.0
- 'europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:v1.1.0'
```
2. **Build and deploy**:
```bash
gcloud builds submit --project=memo-2c4c4 --config=cloudbuild.yaml .
gcloud run deploy manadeck-backend \
--image=europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:v1.1.0 \
--project=memo-2c4c4 \
--region=europe-west3
```
### List Deployed Revisions
```bash
gcloud run revisions list \
--service=manadeck-backend \
--project=memo-2c4c4 \
--region=europe-west3
```
## Rollback
### Manual Rollback to Previous Revision
```bash
# List revisions
gcloud run revisions list \
--service=manadeck-backend \
--project=memo-2c4c4 \
--region=europe-west3
# Rollback to specific revision
gcloud run services update-traffic manadeck-backend \
--to-revisions=manadeck-backend-00001-abc=100 \
--project=memo-2c4c4 \
--region=europe-west3
```
## Troubleshooting
### Build Fails
**Authentication errors:**
```bash
gcloud auth login
gcloud auth configure-docker europe-west3-docker.pkg.dev
```
**Permission denied:**
- Verify you have Cloud Build Editor role
- Check service account has Artifact Registry Writer role
### Deployment Fails
**Secret not found:**
```bash
# List secrets
gcloud secrets list --project=memo-2c4c4
# Create missing secret
echo "value" | gcloud secrets create SECRET_NAME --data-file=- --project=memo-2c4c4
```
**Service account permissions:**
```bash
# Check service account IAM policy
gcloud projects get-iam-policy memo-2c4c4 \
--flatten="bindings[].members" \
--filter="bindings.members:serviceAccount:manadeck-backend-sa@memo-2c4c4.iam.gserviceaccount.com"
```
### Health Check Fails
**Check logs:**
```bash
gcloud logging read "resource.type=cloud_run_revision AND resource.labels.service_name=manadeck-backend" \
--project=memo-2c4c4 \
--limit=50 \
--format=json
```
**Common issues:**
- Port mismatch (must be 8080)
- Missing environment variables
- Database connection issues
- Mana Core service unreachable
**Test locally:**
```bash
cd manadeck/backend
# Build Docker image
docker build -t manadeck-backend:local .
# Run container
docker run -p 8080:8080 \
-e NODE_ENV=production \
-e MANA_SERVICE_URL=your-mana-url \
-e APP_ID=your-app-id \
manadeck-backend:local
# Test health
curl http://localhost:8080/health
```
### Update Environment Variables
```bash
# Update a Manadeck-specific secret
echo "new-value" | gcloud secrets versions add MANADECK_APP_ID --data-file=- --project=memo-2c4c4
# Redeploy to pick up new secret version
gcloud run services update manadeck-backend \
--project=memo-2c4c4 \
--region=europe-west3
# Note: MANA_SERVICE_URL is a global secret - updating it affects all services
```
### View Service Configuration
```bash
gcloud run services describe manadeck-backend \
--project=memo-2c4c4 \
--region=europe-west3 \
--format=yaml
```
## Monitoring and Logs
### View Logs
```bash
# Recent logs
gcloud logging read "resource.type=cloud_run_revision AND resource.labels.service_name=manadeck-backend" \
--project=memo-2c4c4 \
--limit=50
# Tail logs
gcloud logging tail "resource.type=cloud_run_revision AND resource.labels.service_name=manadeck-backend" \
--project=memo-2c4c4
# Filter error logs
gcloud logging read "resource.type=cloud_run_revision AND resource.labels.service_name=manadeck-backend AND severity>=ERROR" \
--project=memo-2c4c4 \
--limit=20
```
### Cloud Console Links
- **Service**: https://console.cloud.google.com/run/detail/europe-west3/manadeck-backend/metrics?project=memo-2c4c4
- **Logs**: https://console.cloud.google.com/logs/query?project=memo-2c4c4
- **Artifact Registry**: https://console.cloud.google.com/artifacts/docker/memo-2c4c4/europe-west3/manadeck-backend?project=memo-2c4c4
## Configuration Reference
### Environment Variables (via Secrets)
| Secret Name | Description | Example | Note |
|------------|-------------|---------|------|
| MANA_SERVICE_URL | Mana Core service URL | https://mana-core.example.com | Global secret |
| MANADECK_APP_ID | Application ID from Mana | app-12345 | |
| MANADECK_SERVICE_KEY | Service authentication key | sk_live_... | |
| MANADECK_SUPABASE_URL | Supabase project URL | https://abc.supabase.co | |
| MANADECK_SUPABASE_ANON_KEY | Supabase anonymous key | eyJhb... | |
| MANADECK_SUPABASE_SERVICE_KEY | Supabase service role key | eyJhb... | |
| MANADECK_SIGNUP_REDIRECT_URL | Post-signup redirect URL | https://app.example.com/welcome | |
### Cloud Run Configuration
- **Project**: memo-2c4c4
- **Region**: europe-west3
- **Service Name**: manadeck-backend
- **Port**: 8080
- **Memory**: 512Mi
- **CPU**: 1
- **Timeout**: 300s
- **Min Instances**: 0 (scales to zero)
- **Max Instances**: 10
- **Concurrency**: 80
### Docker Image
- **Registry**: europe-west3-docker.pkg.dev
- **Repository**: manadeck-backend
- **Image**: europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend
- **Tags**: v1.0.0, latest
## Quick Reference Commands
```bash
# Build only
cd manadeck/backend
gcloud builds submit --project=memo-2c4c4 --config=cloudbuild.yaml .
# Deploy latest version
gcloud run deploy manadeck-backend \
--image=europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:latest \
--project=memo-2c4c4 \
--region=europe-west3
# Get service URL
gcloud run services describe manadeck-backend \
--project=memo-2c4c4 \
--region=europe-west3 \
--format='value(status.url)'
# View logs
gcloud logging read "resource.type=cloud_run_revision AND resource.labels.service_name=manadeck-backend" \
--project=memo-2c4c4 \
--limit=20
# List revisions
gcloud run revisions list \
--service=manadeck-backend \
--project=memo-2c4c4 \
--region=europe-west3
# Update secret (example with Manadeck-specific secret)
echo "new-value" | gcloud secrets versions add MANADECK_APP_ID \
--data-file=- \
--project=memo-2c4c4
# For global secrets like MANA_SERVICE_URL, update carefully as it affects all services
echo "new-value" | gcloud secrets versions add MANA_SERVICE_URL \
--data-file=- \
--project=memo-2c4c4
```
## Support
For issues or questions:
1. Check Cloud Run logs for error messages
2. Verify all secrets are configured correctly
3. Test health endpoints
4. Review GitHub Actions workflow logs (for automated deployments)
5. Consult the troubleshooting section above
---
**Last Updated**: 2025-09-30
**Current Version**: v1.0.0
**Deployment Region**: europe-west3
**Project**: memo-2c4c4

View file

@ -0,0 +1,110 @@
# syntax=docker/dockerfile:1
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
# Install build dependencies
RUN apk add --no-cache python3 make g++ git openssh-client
# Configure git to use HTTPS with token
RUN git config --global url."https://github.com/".insteadOf "git@github.com:" && \
git config --global url."https://".insteadOf "git://"
# Clone, build and package mana-core as a tarball
RUN --mount=type=secret,id=github_token \
if [ -f /run/secrets/github_token ]; then \
export GITHUB_TOKEN=$(cat /run/secrets/github_token) && \
echo "Using GitHub token for private repo access" && \
git clone https://${GITHUB_TOKEN}@github.com/Memo-2023/mana-core-nestjs-package.git /tmp/mana-core; \
else \
echo "No GitHub token provided, attempting public clone" && \
git clone https://github.com/Memo-2023/mana-core-nestjs-package.git /tmp/mana-core; \
fi && \
cd /tmp/mana-core && \
npm install --force && \
npm run build && \
npm pack && \
mv *.tgz /app/mana-core.tgz && \
echo "Mana-core packaged as tarball at /app/mana-core.tgz"
# Copy package.json
COPY package.json ./
# Replace GitHub URL with the tarball
RUN sed -i 's|"git+https://github.com/Memo-2023/mana-core-nestjs-package.git"|"file:mana-core.tgz"|g' package.json || \
sed -i 's|"github:Memo-2023/mana-core-nestjs-package"|"file:mana-core.tgz"|g' package.json
# Debug: Verify the replacement and file existence
RUN echo "=== Verifying tarball and package.json ===" && \
ls -la mana-core.tgz && \
echo "Tarball exists at /app/mana-core.tgz" && \
echo "Checking package.json replacement:" && \
grep -n "mana-core" package.json && \
echo "=== End verification ==="
# Install dependencies
RUN npm install --legacy-peer-deps && \
echo "Dependencies installed with mana-core from tarball"
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Debug: List the contents to verify build output
RUN echo "=== Listing dist contents ===" && \
ls -la dist/ || echo "No dist folder found" && \
echo "=== Build complete ==="
# Production stage
FROM node:18-alpine
# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
# Copy the dist folder
COPY --from=builder /app/dist ./dist
# Copy package.json for metadata
COPY --from=builder /app/package.json ./
# Copy node_modules
COPY --from=builder /app/node_modules ./node_modules
# Debug in production to verify file structure
RUN echo "=== Production stage file check ===" && \
ls -la /app/ && \
echo "=== Checking dist folder ===" && \
ls -la /app/dist/ || echo "No dist folder" && \
echo "=== Looking for main.js ===" && \
find /app -name "main.js" -type f 2>/dev/null || echo "main.js not found"
# Change ownership to nodejs user
RUN chown -R nodejs:nodejs /app
# Switch to non-root user
USER nodejs
# Expose the port (Cloud Run will set PORT env var)
EXPOSE 8080
# Set environment to production
ENV NODE_ENV=production
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:' + (process.env.PORT || 8080) + '/health', (r) => {r.statusCode === 200 ? process.exit(0) : process.exit(1)})" || exit 1
# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]
# Start the application
CMD ["node", "dist/main"]

View file

@ -0,0 +1,98 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ npm install
```
## Compile and run the project
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Run tests
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ npm install -g @nestjs/mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).

View file

@ -0,0 +1,300 @@
# ManaDeck Backend with Mana Core Integration
## Overview
This is the NestJS backend for ManaDeck, integrated with the Mana Core authentication system. It provides a secure API for the ManaDeck mobile application with centralized authentication, user management, and database operations.
## Architecture
```
Mobile App (React Native/Expo)
↓ API requests
Backend Service (NestJS + @mana-core/nestjs-integration)
↓ Proxy auth requests
Mana Core Middleware (Central Auth Service)
↓ User management
Supabase (Database & Storage)
```
## Features
- ✅ Mana Core authentication integration
- ✅ Protected and public API endpoints
- ✅ Supabase database integration with RLS
- ✅ Health check endpoints
- ✅ CORS configuration for mobile app
- ✅ Docker containerization
- ✅ Google Cloud Run deployment ready
## Project Structure
```
backend/
├── src/
│ ├── config/
│ │ └── validation.schema.ts # Environment variable validation
│ ├── controllers/
│ │ ├── api.controller.ts # Protected API endpoints
│ │ ├── public.controller.ts # Public endpoints
│ │ └── health.controller.ts # Health checks
│ ├── services/
│ │ └── supabase.service.ts # Database operations
│ ├── app.controller.ts # Default app controller
│ ├── app.module.ts # Main application module
│ ├── app.service.ts # Default app service
│ └── main.ts # Application entry point
├── .env # Environment variables
├── .env.example # Example environment file
├── Dockerfile # Docker configuration
├── cloudbuild.yaml # Google Cloud Build config
└── package.json # Dependencies and scripts
```
## Prerequisites
- Node.js 18+
- NPM or Yarn
- Mana Core Middleware running (locally or deployed)
- Supabase project with configured database
## Installation
```bash
# Install dependencies
npm install
# Copy environment variables
cp .env.example .env
# Edit .env with your configuration
```
## Configuration
### Required Environment Variables
```bash
# Server Configuration
NODE_ENV=development
PORT=8080
# Mana Core Configuration
MANA_SERVICE_URL=http://localhost:3000 # URL to Mana Core Middleware
APP_ID=your-app-id # Your app's UUID
SERVICE_KEY=your-service-key # Optional service key
SIGNUP_REDIRECT_URL=https://yourapp.com/welcome
# Supabase Configuration
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_KEY=your-service-key # Optional for admin operations
# JWT Configuration
JWT_SECRET=your-jwt-secret
# CORS Configuration
FRONTEND_URL=http://localhost:8081
```
## Available Endpoints
### Authentication (Proxied by ManaCoreModule)
- `POST /auth/signin` - Email/password sign-in
- `POST /auth/signup` - User registration
- `POST /auth/google-signin` - Google OAuth
- `POST /auth/apple-signin` - Apple OAuth
- `POST /auth/refresh` - Token refresh
- `POST /auth/logout` - User logout
- `POST /auth/forgot-password` - Password reset
- `POST /auth/validate` - Token validation
- `GET /auth/credits` - Get user credits
- `GET /auth/devices` - Get user devices
### Protected API Endpoints (Requires Authentication)
- `GET /v1/api/profile` - Get user profile
- `GET /v1/api/decks` - Get user's decks
- `POST /v1/api/decks` - Create a new deck
- `PUT /v1/api/decks/:id` - Update a deck
- `DELETE /v1/api/decks/:id` - Delete a deck
- `GET /v1/api/cards` - Get user's cards
- `POST /v1/api/cards` - Create a new card
- `GET /v1/api/stats` - Get user statistics
### Public Endpoints
- `GET /v1/public/featured-decks` - Get featured decks (personalized if authenticated)
- `GET /v1/public/leaderboard` - Get leaderboard
- `GET /v1/public/deck-templates` - Get deck templates
- `GET /v1/public/announcements` - Get announcements
- `GET /v1/public/health` - Basic health check
- `GET /v1/public/version` - API version info
### Health Checks
- `GET /health` - Comprehensive health check
- `GET /health/ready` - Readiness probe
- `GET /health/live` - Liveness probe
## Development
```bash
# Start development server with watch mode
npm run start:dev
# Start production server
npm run start:prod
# Run tests
npm test
# Run linting
npm run lint
# Build the application
npm run build
```
## Testing the Integration
1. **Start Mana Core Middleware** (if running locally):
```bash
cd ../mana-core-middleware
npm run start:dev
```
2. **Start the Backend**:
```bash
npm run start:dev
```
3. **Test Health Check**:
```bash
curl http://localhost:8080/health
```
4. **Test Public Endpoint**:
```bash
curl http://localhost:8080/v1/public/version
```
5. **Test Authentication**:
```bash
# Sign up
curl -X POST http://localhost:8080/auth/signup \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"password123"}'
# Sign in
curl -X POST http://localhost:8080/auth/signin \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"password123"}'
```
6. **Test Protected Endpoint**:
```bash
# Use the token from signin response
curl http://localhost:8080/v1/api/profile \
-H "Authorization: Bearer YOUR_TOKEN"
```
## Docker
### Build Docker Image
```bash
docker build -t manadeck-backend .
```
### Run Docker Container
```bash
docker run -p 8080:8080 --env-file .env manadeck-backend
```
## Deployment
### Google Cloud Run
1. **Build and push to Container Registry**:
```bash
gcloud builds submit --config cloudbuild.yaml
```
2. **Or deploy directly**:
```bash
gcloud run deploy manadeck-backend \
--source . \
--region us-central1 \
--allow-unauthenticated
```
## Mobile App Integration
Update your React Native app to use the backend:
1. **Update API configuration**:
```javascript
// utils/api.js
const API_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:8080';
```
2. **Use auth endpoints**:
```javascript
// Sign in
const response = await fetch(`${API_URL}/auth/signin`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
```
3. **Use protected endpoints**:
```javascript
// Get profile
const response = await fetch(`${API_URL}/v1/api/profile`, {
headers: {
'Authorization': `Bearer ${token}`
},
});
```
## Security Considerations
- Always use HTTPS in production
- Keep SERVICE_KEY secure and never expose it
- Regularly rotate JWT_SECRET
- Use environment-specific configurations
- Enable rate limiting on auth endpoints
- Implement proper CORS policies
- Use Supabase RLS for data security
## Troubleshooting
### "App verification failed"
- Verify APP_ID matches the one registered with Mana Core
- Check MANA_SERVICE_URL is correct and accessible
### "Token expired" errors
- Ensure token refresh is implemented in the mobile app
- Check token expiration settings in Mana Core
### "CORS errors" in mobile app
- Update FRONTEND_URL in .env
- Verify CORS configuration in main.ts
### "Cannot connect to Mana Core"
- Ensure Mana Core Middleware is running
- Check network connectivity
- Verify MANA_SERVICE_URL is correct
## Next Steps
1. **Database Setup**: Create tables in Supabase for decks, cards, etc.
2. **RLS Policies**: Implement Row-Level Security in Supabase
3. **Testing**: Add unit and integration tests
4. **Monitoring**: Set up logging and monitoring
5. **CI/CD**: Configure automated deployment pipeline
6. **Documentation**: Generate API documentation with Swagger
## Support
For issues or questions:
- Check the Mana Core documentation
- Review the integration guides
- Contact the development team

View file

@ -0,0 +1,176 @@
# SSH URLs in package-lock.json - The Complete Solution
## TL;DR
**Two-layer approach:**
1. **CI test stage**: Patch lockfile SSH → HTTPS for `npm ci` (for tests)
2. **Docker build**: Clone private repo, build tarball, replace with `file:` (for production image)
## The Problem
Your local machine converts HTTPS → SSH during `npm install`, baking SSH URLs into `package-lock.json`. CI/CD fails because it can't authenticate via SSH.
## Why Fighting It Locally Doesn't Work
**Approach 1**: "Fix package.json to use HTTPS"
- Doesn't work if git config rewrites it during install
**Approach 2**: "Remove SSH rewrites from git config"
- Inconvenient for developers
- Easy to forget
- Breaks other workflows
**Approach 3**: "Temporarily disable git config during install"
- Doesn't persist
- Every developer needs to remember
## ✅ The Complete Solution (Two-Layer Approach)
**Accept that the lockfile has SSH URLs and handle them at two stages:**
### Layer 1: CI Test Stage (For Running Tests)
The GitHub Actions workflow uses the proven pattern to handle both SSH and HTTPS URLs:
```yaml
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false # Don't let default GITHUB_TOKEN interfere
- name: Configure git for private packages
env:
GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
run: |
git config --global url."https://${GH_TOKEN}@github.com/".insteadOf ssh://git@github.com/
git config --global url."https://${GH_TOKEN}@github.com/".insteadOf git@github.com:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Patch package-lock.json with authenticated URLs
env:
GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
run: |
# Handle both SSH and HTTPS URLs
if grep -q "git+ssh://git@github.com" package-lock.json; then
echo "⚠️ SSH URLs found - patching to HTTPS with token..."
sed -i "s|git+ssh://git@github.com/Memo-2023/|git+https://${GH_TOKEN}@github.com/Memo-2023/|g" package-lock.json
echo "✓ Lockfile patched successfully"
else
echo "⚠️ HTTPS URLs found - injecting token..."
sed -i "s|git+https://github.com/Memo-2023/|git+https://${GH_TOKEN}@github.com/Memo-2023/|g" package-lock.json
echo "✓ Token injected successfully"
fi
- name: Install dependencies
run: npm ci --legacy-peer-deps
```
### Layer 2: Docker Build (For Production Image)
The Dockerfile clones the private repo, builds it as a tarball, and installs from `file:`:
```dockerfile
# Clone, build and package mana-core as a tarball
RUN --mount=type=secret,id=github_token \
if [ -f /run/secrets/github_token ]; then \
export GITHUB_TOKEN=$(cat /run/secrets/github_token) && \
git clone https://${GITHUB_TOKEN}@github.com/Memo-2023/mana-core-nestjs-package.git /tmp/mana-core; \
fi && \
cd /tmp/mana-core && \
npm install --force && \
npm run build && \
npm pack && \
mv *.tgz /app/mana-core.tgz
# Copy package.json and replace GitHub URL with the tarball
COPY package.json ./
RUN sed -i 's|"git+https://github.com/Memo-2023/mana-core-nestjs-package.git"|"file:mana-core.tgz"|g' package.json
# Install dependencies from tarball
RUN npm install --legacy-peer-deps
```
The GitHub Actions workflow passes the token as a Docker secret:
```yaml
- name: Build and Push Docker Image
env:
GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
run: |
docker build \
--secret id=github_token,env=GH_TOKEN \
.
```
## Why This Works
1. **Developers**: Use SSH locally (convenient, no config changes needed)
2. **package-lock.json**: Contains SSH URLs (fine, we handle it in CI)
3. **CI Test Stage**: Patches SSH → HTTPS for `npm ci` to run tests
4. **Docker Build**: Clones repo, builds tarball, installs from `file:` (no git involved in final image)
5. **Production**: Docker image has mana-core built into it, no runtime git dependency
6. **Everyone's happy**: No git config changes, no local workflow disruption
## Key Implementation Details
### For CI Test Stage (sed patching)
```bash
sed -i "s|git+ssh://git@github.com/Memo-2023/|git+https://${GH_TOKEN}@github.com/Memo-2023/|g" package-lock.json
```
- Happens at runtime in CI (never committed)
- Allows `npm ci` to install dependencies for testing
- Works reliably every time
### For Docker Build (tarball approach)
```bash
# Docker secret gives access to GitHub token
--mount=type=secret,id=github_token
# Clone and build the private package
git clone https://${GITHUB_TOKEN}@github.com/Memo-2023/mana-core-nestjs-package.git
npm pack # Creates .tgz file
# Replace git URL with local file reference
sed -i 's|"git+https://..."|"file:mana-core.tgz"|g' package.json
```
- Private package is baked into the Docker image
- No git dependency at runtime
- Production image is fully self-contained
## Why This is Better Than Alternatives
| Approach | Developer Impact | Reliability | Production Quality | Maintenance |
|----------|-----------------|-------------|-------------------|-------------|
| Fix git config locally | 😡 High | 🔴 Low | ⚠️ Medium | 😱 High |
| Require HTTPS in lockfile | 😡 High | 🔴 Low | ⚠️ Medium | 😱 High |
| **Two-layer (sed + tarball)** | 😊 None | 🟢 100% | ✅ Excellent | 😌 None |
## Lessons Learned
1. **npm ci reads package-lock.json directly** - It doesn't care about git config
2. **Fighting developer workflows is futile** - Accept SSH URLs locally
3. **Two layers solve different problems**:
- CI test stage needs quick install for testing → sed patch
- Production image needs reliability and security → tarball bake-in
4. **Docker secrets are the right tool** - Pass credentials without committing them
5. **Self-contained images are better** - No runtime git dependencies
## References
This solution combines proven approaches:
- **sed patching**: Used in `storyteller-project` for CI/CD
- **tarball approach**: Used in memoro-service for production Docker images
- Battle-tested across multiple projects
---
**Bottom line**: Commit the SSH lockfile, handle it in two layers (CI + Docker). Done. 🎯

View file

@ -0,0 +1,30 @@
# Cloud Build configuration for Manadeck Backend
# Project: memo-2c4c4
# Region: europe-west3
# Manual deployment: gcloud builds submit --project=memo-2c4c4 --config=cloudbuild.yaml .
steps:
# Build the Docker image
- name: 'gcr.io/cloud-builders/docker'
args:
- 'build'
- '-t'
- 'europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:v1.0.0'
- '-t'
- 'europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:latest'
- '.'
# Push the Docker images to Artifact Registry
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:v1.0.0']
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:latest']
# Specify the images to be stored in Artifact Registry
images:
- 'europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:v1.0.0'
- 'europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:latest'
# Build timeout
timeout: '1200s'

View file

@ -0,0 +1,257 @@
#!/bin/bash
# Create GCP secrets for Manadeck Backend
# Usage: ./create-secrets.sh
set -e # Exit on error
# Colors for output
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo "=========================================="
echo "Manadeck Backend - GCP Secrets Setup"
echo "=========================================="
echo ""
# Configuration
DEPLOY_PROJECT_ID="memo-2c4c4" # Project where Cloud Run service is deployed
SECRETS_PROJECT_ID="mana-core-453821" # Project where all secrets are stored
REGION="europe-west3"
# Check if gcloud is installed
if ! command -v gcloud &> /dev/null; then
echo -e "${RED}${NC} gcloud CLI not found. Please install Google Cloud SDK."
exit 1
fi
echo -e "${GREEN}${NC} gcloud CLI found"
echo ""
# Check authentication
echo "Checking GCP authentication..."
if ! gcloud auth list --filter=status:ACTIVE --format="value(account)" &> /dev/null; then
echo -e "${RED}${NC} Not authenticated with GCP. Running 'gcloud auth login'..."
gcloud auth login
fi
ACTIVE_ACCOUNT=$(gcloud auth list --filter=status:ACTIVE --format="value(account)")
echo -e "${GREEN}${NC} Authenticated as: $ACTIVE_ACCOUNT"
echo ""
# Set project for deployment
echo "Deployment project: $DEPLOY_PROJECT_ID"
echo "Secrets project: $SECRETS_PROJECT_ID"
gcloud config set project $SECRETS_PROJECT_ID
echo ""
# Prompt for secret values
echo "=========================================="
echo "Enter Secret Values"
echo "=========================================="
echo ""
echo -e "${YELLOW}Note: All secrets will be stored in project ${SECRETS_PROJECT_ID}${NC}"
echo ""
# Check if MANA_SERVICE_URL exists
echo "Checking for global MANA_SERVICE_URL secret..."
if gcloud secrets describe MANA_SERVICE_URL --project=$SECRETS_PROJECT_ID &> /dev/null; then
echo -e "${GREEN}${NC} MANA_SERVICE_URL secret exists"
MANA_URL=$(gcloud secrets versions access latest --secret=MANA_SERVICE_URL --project=$SECRETS_PROJECT_ID 2>/dev/null || echo "")
if [ -n "$MANA_URL" ]; then
echo " Current value: $MANA_URL"
else
echo -e "${YELLOW}${NC} Could not read value (may need permissions)"
fi
else
echo -e "${YELLOW}${NC} MANA_SERVICE_URL secret not found!"
read -p "Enter MANA_SERVICE_URL (e.g., https://mana-core.run.app): " MANA_URL
if [ -n "$MANA_URL" ]; then
echo "Creating MANA_SERVICE_URL secret..."
echo "$MANA_URL" | gcloud secrets create MANA_SERVICE_URL \
--data-file=- \
--project=$SECRETS_PROJECT_ID \
--labels=service=global
echo -e "${GREEN}${NC} MANA_SERVICE_URL secret created"
else
echo -e "${RED}${NC} MANA_SERVICE_URL is required"
exit 1
fi
fi
echo ""
# Manadeck-specific secrets
echo "Enter Manadeck-specific secrets:"
echo ""
# APP_ID
read -p "MANADECK_APP_ID (your app ID from Mana Core): " APP_ID
if [ -z "$APP_ID" ]; then
echo -e "${RED}${NC} APP_ID is required"
exit 1
fi
# SERVICE_KEY
echo ""
echo "SERVICE_KEY (for service-to-service authentication)"
echo -e "${YELLOW}Press Enter to generate a random key, or paste your own:${NC}"
read -p "" SERVICE_KEY
if [ -z "$SERVICE_KEY" ]; then
SERVICE_KEY=$(openssl rand -base64 32)
echo -e "${GREEN}Generated SERVICE_KEY:${NC} $SERVICE_KEY"
echo -e "${YELLOW}⚠ IMPORTANT: Add this to APP_SERVICE_KEYS in mana-core-middleware:${NC}"
echo -e "${YELLOW} Format: ${APP_ID}:${SERVICE_KEY}${NC}"
fi
# SUPABASE_URL
echo ""
read -p "MANADECK_SUPABASE_URL (e.g., https://xxx.supabase.co): " SUPABASE_URL
if [ -z "$SUPABASE_URL" ]; then
echo -e "${RED}${NC} SUPABASE_URL is required"
exit 1
fi
# SUPABASE_ANON_KEY
echo ""
read -p "MANADECK_SUPABASE_ANON_KEY: " SUPABASE_ANON_KEY
if [ -z "$SUPABASE_ANON_KEY" ]; then
echo -e "${RED}${NC} SUPABASE_ANON_KEY is required"
exit 1
fi
# SUPABASE_SERVICE_KEY
echo ""
read -p "MANADECK_SUPABASE_SERVICE_KEY (service role key): " SUPABASE_SERVICE_KEY
if [ -z "$SUPABASE_SERVICE_KEY" ]; then
echo -e "${RED}${NC} SUPABASE_SERVICE_KEY is required"
exit 1
fi
# SIGNUP_REDIRECT_URL
echo ""
read -p "MANADECK_SIGNUP_REDIRECT_URL (e.g., https://yourapp.com/welcome): " SIGNUP_REDIRECT_URL
if [ -z "$SIGNUP_REDIRECT_URL" ]; then
echo -e "${YELLOW}${NC} SIGNUP_REDIRECT_URL is empty (optional)"
fi
echo ""
echo "=========================================="
echo "Creating Secrets"
echo "=========================================="
echo ""
# Function to create or update secret
create_or_update_secret() {
local SECRET_NAME=$1
local SECRET_VALUE=$2
if [ -z "$SECRET_VALUE" ]; then
echo -e "${YELLOW}${NC} Skipping $SECRET_NAME (empty value)"
return
fi
if gcloud secrets describe $SECRET_NAME --project=$SECRETS_PROJECT_ID &> /dev/null; then
echo -e "${YELLOW}${NC} $SECRET_NAME already exists"
read -p " Update with new value? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "$SECRET_VALUE" | gcloud secrets versions add $SECRET_NAME \
--data-file=- \
--project=$SECRETS_PROJECT_ID
echo -e "${GREEN}${NC} $SECRET_NAME updated"
else
echo " Skipped $SECRET_NAME"
fi
else
echo "Creating $SECRET_NAME..."
echo "$SECRET_VALUE" | gcloud secrets create $SECRET_NAME \
--data-file=- \
--project=$SECRETS_PROJECT_ID \
--labels=service=manadeck,environment=production
echo -e "${GREEN}${NC} $SECRET_NAME created"
fi
}
# Create all secrets
create_or_update_secret "MANADECK_APP_ID" "$APP_ID"
create_or_update_secret "MANADECK_SERVICE_KEY" "$SERVICE_KEY"
create_or_update_secret "MANADECK_SUPABASE_URL" "$SUPABASE_URL"
create_or_update_secret "MANADECK_SUPABASE_ANON_KEY" "$SUPABASE_ANON_KEY"
create_or_update_secret "MANADECK_SUPABASE_SERVICE_KEY" "$SUPABASE_SERVICE_KEY"
create_or_update_secret "MANADECK_SIGNUP_REDIRECT_URL" "$SIGNUP_REDIRECT_URL"
echo ""
echo "=========================================="
echo "Grant Service Account Access"
echo "=========================================="
echo ""
SERVICE_ACCOUNT="manadeck-backend-sa@${DEPLOY_PROJECT_ID}.iam.gserviceaccount.com"
echo "Checking if service account exists..."
if gcloud iam service-accounts describe $SERVICE_ACCOUNT --project=$DEPLOY_PROJECT_ID &> /dev/null; then
echo -e "${GREEN}${NC} Service account exists: $SERVICE_ACCOUNT"
echo ""
read -p "Grant service account access to secrets? (Y/n): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
echo "Granting access to all secrets in ${SECRETS_PROJECT_ID}..."
# Grant access to all secrets
for SECRET in MANA_SERVICE_URL MANADECK_APP_ID MANADECK_SERVICE_KEY MANADECK_SUPABASE_URL MANADECK_SUPABASE_ANON_KEY MANADECK_SUPABASE_SERVICE_KEY MANADECK_SIGNUP_REDIRECT_URL; do
if gcloud secrets describe $SECRET --project=$SECRETS_PROJECT_ID &> /dev/null; then
gcloud secrets add-iam-policy-binding $SECRET \
--member="serviceAccount:${SERVICE_ACCOUNT}" \
--role="roles/secretmanager.secretAccessor" \
--project=$SECRETS_PROJECT_ID \
--condition=None \
2>/dev/null || echo " Access already granted for $SECRET"
echo -e "${GREEN}${NC} $SECRET access granted"
fi
done
fi
else
echo -e "${YELLOW}${NC} Service account not found: $SERVICE_ACCOUNT"
echo " Create it first with:"
echo " gcloud iam service-accounts create manadeck-backend-sa \\"
echo " --display-name=\"Manadeck Backend Service Account\" \\"
echo " --project=$DEPLOY_PROJECT_ID"
fi
echo ""
echo "=========================================="
echo "Summary"
echo "=========================================="
echo ""
echo -e "${GREEN}${NC} Secrets created/updated in project: $SECRETS_PROJECT_ID"
echo ""
echo "All secrets in mana-core-453821:"
echo " - MANA_SERVICE_URL (global)"
echo " - MANADECK_APP_ID"
echo " - MANADECK_SERVICE_KEY"
echo " - MANADECK_SUPABASE_URL"
echo " - MANADECK_SUPABASE_ANON_KEY"
echo " - MANADECK_SUPABASE_SERVICE_KEY"
echo " - MANADECK_SIGNUP_REDIRECT_URL"
echo ""
echo -e "${YELLOW}⚠ IMPORTANT NEXT STEPS:${NC}"
echo ""
echo "1. Add SERVICE_KEY to mana-core-middleware APP_SERVICE_KEYS:"
echo " Format: ${APP_ID}:${SERVICE_KEY}"
echo ""
echo "2. Verify secrets:"
echo " gcloud secrets list --project=$SECRETS_PROJECT_ID --filter=\"labels.service=manadeck\""
echo ""
echo "3. Deploy manadeck-backend:"
echo " git add ."
echo " git commit -m \"feat: configure secrets\""
echo " git push origin main"
echo ""
echo "4. View secret values (if needed):"
echo " gcloud secrets versions access latest --secret=MANADECK_APP_ID --project=$SECRETS_PROJECT_ID"
echo ""
echo -e "${GREEN}${NC} Setup complete!"
echo ""

View file

@ -0,0 +1,34 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
sourceType: 'commonjs',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn'
},
},
);

View file

@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "memo-2c4c4",
"private_key_id": "891033d18bb7a58f26590d7289aa10f171709511",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDDlzSIuERbKHd4\nrTOgW3A3AiQ4oaluDWFrxXvMKOXeKsqMx2LZr3LjaG1VohR11/m3yNCrENwWwMil\nKlTzJridVZgxskpsnftj5LZiMIWTkGrbEKrR7t4EFGX7B1PlDByoHkg70UWSv4Cp\nLEqrKNc9GDa+HJuCpAu5Y8s5Vr1jcn4pNYcbe8Qz98M05u5SjsRi3Wo4sApQtMO+\n1I43+BBkQgP53rvbTGmvDkJPX+X30+pHrWIGq1RBbY99T39EfxgVfYbxKIkGTiAU\nT97I2L8A+AcTOZuYpeLVUYkpWyb/PhXRaBzZZpX4DuVl7rex3B903SqXEAmvXGUF\n08YtW3gHAgMBAAECggEAKHjMRRIkCtzXw9/LE/53vNozlohVH9Gi/AL0N+2qiws9\nuF21pae5VTcRkKumIjBbmtzx2MP3VrnzisG0rE+x2dSRVo82+TzDfQshdjXmHf+/\nLK9JOYF87UkvRP9iK74S2/LQLZiDFfeMbvUcSkh8KUcuZK5Uu3aoemcc8TNyovX8\na/rB1LqhMMh6e/5dm/na1iTAigonhlZl9xja4vinsa4k9wMwJrIW+eOpe5lA5Kd1\n9Y2iz4PaORNczUUeozJ820Y27S7WfvvbEGwXJgMO6fkkB+FGgVWl+BxY/BzATk6t\naeimdBWUNmRoqTAGND13hmVytEP0aBhM9bd/FSMOOQKBgQDgmAhyeWn6GkKMNDec\nB9bIWxW7P6ArdAHEnJQBWVStXYI01rS4T+Tet+D8w4Id4Z0d23UfVgMcF6E36xBG\nTpVYHlj30CfFXnkhiLxP2wx/i3ogNj8jrGU2yYKaE/72GzPx1YFHXXbTBdqrQRR+\nSY2ZgLyEpH0k6+H1bNIb7p1CswKBgQDe8OuUJHyQI2Elxv9eyE2TG73RA2TUCTH/\n9H7/ddtiYNFQv5m1u6a2tJomqFa9iTD0KPKJ1k43M63WjKRLlj0c1EFrVFEh7xuA\nYWtuuL9pl+LIskChI+HYBEn+4BejZs41kSDgOwQC+sztc52x77QbwB69XHvjTx8y\n+y49LUdPXQKBgH2JfW8/HlWFyBzGMy8r0oo7H3KCpeyj8zkKrE/0trgQxG70dB9x\nyOQUs9WVEm3RZhkCSf0x/pr9OkgfRAGIe0G99MzPtOtFWz+7Rkb8kYfZVnIa4D/4\nZLqy+CVXd/PoUKikRpa1T220n/2JawML6fIbW0RpDOpOTwb3xJmITq6xAoGAQnas\npMOj5wzFH5AN/eHVTrAMjM1oN9Zt85KeceZe+xtGC76zQnypHPsGodO52FOYJpI2\njGU5eDeN8dKIAIjPu1KEFk1Syr0QA9wHPUESv5YqZA7QOboK0G2m6bdBFNPHUizs\nEi1F6uV+RqQbNTrzBPrnryhtPr0N3hMIG/Dtu3kCgYEA1e2BtMIGnnM7Cm7fXfVp\nMeP5zBiqPcsUXVAoJ2WaYPj8n/BNhTd79STWZCUlTfcZ1IywIP9yMhleIXfKW35v\nNVFNMcduCXiqExWqmyjnVZJPFawO+KQ4G+JO8FzH6S/O82PUNbTRjJDs87cJs8t3\nSnd0TEt4+T9vAcdei3Kor20=\n-----END PRIVATE KEY-----\n",
"client_email": "manadeck-backend-sa@memo-2c4c4.iam.gserviceaccount.com",
"client_id": "101484948835153964567",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/manadeck-backend-sa%40memo-2c4c4.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

View file

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

View file

@ -0,0 +1,83 @@
{
"name": "@manadeck/backend",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"dev": "nest start --watch",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@manacore/shared-errors": "workspace:*",
"@google/genai": "^1.14.0",
"@manacore/manadeck-database": "workspace:*",
"@nestjs/axios": "^4.0.1",
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/terminus": "^11.0.0",
"axios": "^1.7.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"joi": "^18.0.1",
"nestjs-cls": "^6.0.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^30.0.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View file

@ -0,0 +1,173 @@
#!/bin/bash
# Script to generate values for GitHub Secrets
# Run this script and copy the outputs to GitHub repository secrets
set -e
# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
echo "=========================================="
echo "GitHub Secrets Setup for Manadeck Backend"
echo "=========================================="
echo ""
PROJECT_ID="memo-2c4c4"
SA_NAME="manadeck-backend-sa"
SA_EMAIL="${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
# Check if service account exists
echo "Checking if service account exists..."
if gcloud iam service-accounts describe $SA_EMAIL --project=$PROJECT_ID &> /dev/null; then
echo -e "${GREEN}${NC} Service account exists: $SA_EMAIL"
else
echo -e "${YELLOW}${NC} Service account not found. Creating..."
# Create service account
gcloud iam service-accounts create $SA_NAME \
--display-name="Manadeck Backend Service Account" \
--project=$PROJECT_ID
echo -e "${GREEN}${NC} Service account created"
# Grant required roles
echo "Granting required roles..."
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:${SA_EMAIL}" \
--role="roles/run.admin" \
--condition=None
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:${SA_EMAIL}" \
--role="roles/iam.serviceAccountUser" \
--condition=None
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:${SA_EMAIL}" \
--role="roles/artifactregistry.writer" \
--condition=None
echo -e "${GREEN}${NC} Roles granted"
fi
echo ""
echo "=========================================="
echo "GitHub Secret #1: CLOUD_RUN_SERVICE_ACCOUNT"
echo "=========================================="
echo ""
echo -e "${BLUE}Copy this value:${NC}"
echo ""
echo "$SA_EMAIL"
echo ""
echo "Add to GitHub: Settings → Secrets → Actions → New repository secret"
echo "Name: CLOUD_RUN_SERVICE_ACCOUNT"
echo "Value: $SA_EMAIL"
echo ""
read -p "Press Enter when you've added CLOUD_RUN_SERVICE_ACCOUNT to GitHub..."
echo ""
echo "=========================================="
echo "GitHub Secret #2: GCP_SA_KEY_PROD"
echo "=========================================="
echo ""
# Check if key file already exists
KEY_FILE="manadeck-sa-key.json"
if [ -f "$KEY_FILE" ]; then
echo -e "${YELLOW}${NC} Key file already exists: $KEY_FILE"
read -p "Do you want to create a new key? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Using existing key file..."
else
rm "$KEY_FILE"
echo "Creating new service account key..."
gcloud iam service-accounts keys create $KEY_FILE \
--iam-account=$SA_EMAIL \
--project=$PROJECT_ID
echo -e "${GREEN}${NC} New key created"
fi
else
echo "Creating service account key..."
gcloud iam service-accounts keys create $KEY_FILE \
--iam-account=$SA_EMAIL \
--project=$PROJECT_ID
echo -e "${GREEN}${NC} Key created"
fi
echo ""
echo -e "${BLUE}Copy the ENTIRE JSON content below:${NC}"
echo ""
echo "=========================================="
cat $KEY_FILE
echo "=========================================="
echo ""
echo "Add to GitHub: Settings → Secrets → Actions → New repository secret"
echo "Name: GCP_SA_KEY_PROD"
echo "Value: [paste the entire JSON above]"
echo ""
read -p "Press Enter when you've added GCP_SA_KEY_PROD to GitHub..."
echo ""
echo "=========================================="
echo "GitHub Secret #3: GH_PERSONAL_TOKEN"
echo "=========================================="
echo ""
echo -e "${YELLOW}This token is needed to access private GitHub packages${NC}"
echo ""
echo "Steps to create:"
echo "1. Go to: https://github.com/settings/tokens"
echo "2. Click 'Generate new token (classic)'"
echo "3. Name: 'Manadeck CI/CD'"
echo "4. Select scope: 'repo' (Full control of private repositories)"
echo "5. Click 'Generate token'"
echo "6. Copy the token"
echo ""
echo "Add to GitHub: Settings → Secrets → Actions → New repository secret"
echo "Name: GH_PERSONAL_TOKEN"
echo "Value: [paste your GitHub token]"
echo ""
read -p "Press Enter when you've added GH_PERSONAL_TOKEN to GitHub..."
# Cleanup
echo ""
echo "=========================================="
echo "Cleanup"
echo "=========================================="
echo ""
echo -e "${YELLOW}${NC} Security best practice: Delete the local key file"
echo "The key file contains sensitive credentials and should not be kept locally."
echo ""
read -p "Delete $KEY_FILE now? (Y/n): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
rm -f $KEY_FILE
echo -e "${GREEN}${NC} Key file deleted"
else
echo -e "${YELLOW}${NC} Key file kept: $KEY_FILE"
echo " Remember to delete it manually when done!"
fi
echo ""
echo "=========================================="
echo "✅ Setup Complete!"
echo "=========================================="
echo ""
echo "Summary of GitHub Secrets:"
echo "1. ✓ CLOUD_RUN_SERVICE_ACCOUNT = $SA_EMAIL"
echo "2. ✓ GCP_SA_KEY_PROD = [JSON key from service account]"
echo "3. ✓ GH_PERSONAL_TOKEN = [Your GitHub Personal Access Token]"
echo ""
echo "Next steps:"
echo "1. Verify all 3 secrets are in GitHub: https://github.com/Memo-2023/manadeck/settings/secrets/actions"
echo "2. Run: ./create-secrets.sh (to set up GCP secrets)"
echo "3. Push to main branch to trigger deployment"
echo ""
echo "Documentation:"
echo "- DEPLOYMENT_CHECKLIST.md - Complete setup guide"
echo "- DEPLOY_MANUAL.md - Detailed deployment docs"
echo ""

View file

@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View file

@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

View file

@ -0,0 +1,81 @@
import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ClsModule } from 'nestjs-cls';
import { TerminusModule } from '@nestjs/terminus';
import { HttpModule } from '@nestjs/axios';
import { ManaCoreModule } from '@mana-core/nestjs-integration';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ApiController } from './controllers/api.controller';
import { PublicController } from './controllers/public.controller';
import { HealthController } from './controllers/health.controller';
import { validationSchema } from './config/validation.schema';
import { AiService } from './services/ai.service';
import {
DatabaseModule,
DeckRepository,
CardRepository,
UserStatsRepository,
DeckTemplateRepository,
} from './database';
@Module({
imports: [
// Context storage for request-scoped data (must be first)
ClsModule.forRoot({
global: true,
middleware: {
mount: true,
generateId: true,
},
}),
// Configuration management
ConfigModule.forRoot({
isGlobal: true,
validationSchema,
ignoreEnvFile: process.env.NODE_ENV === 'production',
}),
// Mana Core authentication
ManaCoreModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
manaServiceUrl: configService.get<string>('MANA_SERVICE_URL', 'https://mana-core-middleware-111768794939.europe-west3.run.app'),
appId: configService.get<string>('APP_ID', 'cea4bfc6-a4de-4e17-91e2-54275940156e'),
serviceKey: configService.get<string>('MANA_SUPABASE_SECRET_KEY', ''), // Required for service-to-service communication
signupRedirectUrl: configService.get<string>('SIGNUP_REDIRECT_URL', 'https://manadeck.com/welcome'),
debug: configService.get('NODE_ENV') === 'development',
}),
inject: [ConfigService],
}) as any,
// Health checks
TerminusModule,
HttpModule,
// Database (Drizzle/PostgreSQL)
DatabaseModule,
],
controllers: [
AppController,
ApiController,
PublicController,
HealthController,
],
providers: [
AppService,
// AI Service
AiService,
// Database repositories
DeckRepository,
CardRepository,
UserStatsRepository,
DeckTemplateRepository,
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
// Add any custom middleware here
}
}

View file

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View file

@ -0,0 +1,51 @@
/**
* Credit Operation Types and Costs for Manadeck
*
* Define all billable operations and their credit costs here.
* This ensures consistent pricing across the application.
*/
export enum CreditOperationType {
DECK_CREATION = 'deck_creation',
CARD_CREATION = 'card_creation',
AI_CARD_GENERATION = 'ai_card_generation',
AI_DECK_GENERATION = 'ai_deck_generation',
DECK_EXPORT = 'deck_export',
// Add more operation types as needed
}
/**
* Credit costs for each operation type
*/
export const CREDIT_COSTS: Record<CreditOperationType, number> = {
[CreditOperationType.DECK_CREATION]: 10, // 10 mana to create a deck
[CreditOperationType.CARD_CREATION]: 2, // 2 mana to create a card
[CreditOperationType.AI_CARD_GENERATION]: 5, // 5 mana for AI-generated card
[CreditOperationType.AI_DECK_GENERATION]: 30, // 30 mana for AI-generated deck
[CreditOperationType.DECK_EXPORT]: 3, // 3 mana to export deck
};
/**
* Operation descriptions for user-facing messages
*/
export const OPERATION_DESCRIPTIONS: Record<CreditOperationType, string> = {
[CreditOperationType.DECK_CREATION]: 'Create a new deck',
[CreditOperationType.CARD_CREATION]: 'Add a card to your deck',
[CreditOperationType.AI_CARD_GENERATION]: 'Generate card with AI',
[CreditOperationType.AI_DECK_GENERATION]: 'Generate deck with AI',
[CreditOperationType.DECK_EXPORT]: 'Export deck',
};
/**
* Get credit cost for an operation
*/
export function getCreditCost(operationType: CreditOperationType): number {
return CREDIT_COSTS[operationType] || 0;
}
/**
* Get operation description
*/
export function getOperationDescription(operationType: CreditOperationType): string {
return OPERATION_DESCRIPTIONS[operationType] || operationType;
}

View file

@ -0,0 +1,26 @@
import * as Joi from 'joi';
export const validationSchema = Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
PORT: Joi.number().default(8080),
// Mana Core Required
MANA_SERVICE_URL: Joi.string().required(),
APP_ID: Joi.string().uuid().required(),
MANA_SUPABASE_SECRET_KEY: Joi.string().optional(),
SIGNUP_REDIRECT_URL: Joi.string().uri().optional(),
// PostgreSQL Database
DATABASE_URL: Joi.string().required(),
// JWT
JWT_SECRET: Joi.string().optional(),
// CORS
FRONTEND_URL: Joi.string().uri().optional(),
// AI Services
GOOGLE_GENAI_API_KEY: Joi.string().optional(),
});

View file

@ -0,0 +1,426 @@
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
UseGuards,
Logger,
BadRequestException,
} from '@nestjs/common';
import { AuthGuard } from '@mana-core/nestjs-integration/guards';
import { CurrentUser } from '@mana-core/nestjs-integration/decorators';
import { CreditClientService } from '@mana-core/nestjs-integration';
import { isOk, CreditError, ServiceError } from '@manacore/shared-errors';
import {
CreditOperationType,
getCreditCost,
getOperationDescription,
} from '../config/credit-operations';
import { DeckRepository, CardRepository, UserStatsRepository } from '../database';
import { AiService, CardType } from '../services/ai.service';
@Controller('api')
@UseGuards(AuthGuard)
export class ApiController {
private readonly logger = new Logger(ApiController.name);
constructor(
private readonly creditClient: CreditClientService,
private readonly deckRepository: DeckRepository,
private readonly cardRepository: CardRepository,
private readonly userStatsRepository: UserStatsRepository,
private readonly aiService: AiService,
) {}
@Get('profile')
async getProfile(@CurrentUser() user: any) {
this.logger.log(`Getting profile for user: ${user.sub}`);
// Include credit balance in profile
let creditBalance = 0;
try {
const balance = await this.creditClient.getCreditBalance(user.sub);
creditBalance = balance.balance || 0;
} catch (error) {
this.logger.warn(`Failed to fetch credit balance for user ${user.sub}:`, error);
}
return {
user,
credits: creditBalance,
timestamp: new Date(),
};
}
@Get('credits/balance')
async getCreditBalance(@CurrentUser() user: any) {
this.logger.log(`Getting credit balance for user: ${user.sub}`);
try {
const balance = await this.creditClient.getCreditBalance(user.sub);
return {
userId: user.sub,
balance: balance.balance || 0,
currency: 'mana',
timestamp: new Date(),
};
} catch (error) {
this.logger.error(`Error fetching credit balance for user ${user.sub}:`, error);
throw new BadRequestException({
error: 'credit_balance_fetch_failed',
message: 'Failed to retrieve credit balance',
});
}
}
@Get('decks')
async getUserDecks(@CurrentUser() user: any) {
this.logger.log(`Getting decks for user: ${user.sub}`);
const decks = await this.deckRepository.findByUserId(user.sub);
return {
userId: user.sub,
decks,
count: decks.length,
};
}
@Post('decks')
async createDeck(@CurrentUser() user: any, @Body() deckData: any) {
this.logger.log(`Creating deck for user: ${user.sub}`);
const operationType = CreditOperationType.DECK_CREATION;
const creditCost = getCreditCost(operationType);
try {
// 1. Pre-flight credit validation
const validation = await this.creditClient.validateCredits(
user.sub,
operationType,
creditCost,
);
if (!validation.hasCredits) {
this.logger.warn(
`User ${user.sub} has insufficient credits. Required: ${creditCost}, Available: ${validation.availableCredits}`,
);
throw new BadRequestException({
error: 'insufficient_credits',
message: `Insufficient mana. Required: ${creditCost}, Available: ${validation.availableCredits}`,
requiredCredits: creditCost,
availableCredits: validation.availableCredits,
operation: getOperationDescription(operationType),
});
}
// 2. Perform the operation (create deck in PostgreSQL)
const newDeck = await this.deckRepository.create({
userId: user.sub,
title: deckData.name || deckData.title || 'Untitled Deck',
description: deckData.description,
coverImageUrl: deckData.coverImageUrl,
isPublic: deckData.isPublic ?? false,
settings: deckData.settings || {},
tags: deckData.tags || [],
metadata: deckData.metadata || {},
});
// 3. Success - Consume credits
await this.creditClient.consumeCredits(
user.sub,
operationType,
creditCost,
`Created deck: ${newDeck.title}`,
{
deckId: newDeck.id,
deckName: newDeck.title,
},
);
this.logger.log(`Deck created successfully for user ${user.sub}. ${creditCost} credits consumed.`);
return {
success: true,
userId: user.sub,
deck: newDeck,
creditsUsed: creditCost,
message: 'Deck created successfully',
};
} catch (error) {
// If it's already a BadRequestException (insufficient credits), rethrow it
if (error instanceof BadRequestException) {
throw error;
}
// Log other errors
this.logger.error(`Error creating deck for user ${user.sub}:`, error);
throw new BadRequestException({
error: 'deck_creation_failed',
message: error.message || 'Failed to create deck',
});
}
}
@Post('decks/generate')
async generateDeckWithAI(@CurrentUser() user: any, @Body() requestData: any) {
this.logger.log(`AI deck generation requested by user: ${user.sub}`);
// Check if AI service is available
if (!this.aiService.isAvailable()) {
throw ServiceError.unavailable('AI');
}
// Validate request
const {
prompt,
deckTitle,
deckDescription,
cardCount = 10,
cardTypes,
difficulty,
tags,
language,
} = requestData;
if (!prompt || !deckTitle) {
throw new BadRequestException({
error: 'validation_failed',
message: 'prompt and deckTitle are required',
});
}
if (cardCount < 1 || cardCount > 50) {
throw new BadRequestException({
error: 'validation_failed',
message: 'cardCount must be between 1 and 50',
});
}
// Validate card types
const validCardTypes: CardType[] = ['text', 'flashcard', 'quiz', 'mixed'];
const requestedTypes: CardType[] = cardTypes || ['flashcard', 'quiz'];
const invalidTypes = requestedTypes.filter((t) => !validCardTypes.includes(t));
if (invalidTypes.length > 0) {
throw new BadRequestException({
error: 'validation_failed',
message: `Invalid card types: ${invalidTypes.join(', ')}. Valid types: ${validCardTypes.join(', ')}`,
});
}
const operationType = CreditOperationType.AI_DECK_GENERATION;
const creditCost = getCreditCost(operationType);
// 1. Pre-flight credit validation
const validation = await this.creditClient.validateCredits(
user.sub,
operationType,
creditCost,
);
if (!validation.hasCredits) {
this.logger.warn(
`User ${user.sub} has insufficient credits for AI deck generation. Required: ${creditCost}, Available: ${validation.availableCredits}`,
);
throw new CreditError(
creditCost,
validation.availableCredits || 0,
getOperationDescription(operationType),
);
}
// 2. Generate cards with AI
this.logger.log(`Generating ${cardCount} cards with AI for user ${user.sub}...`);
const aiResult = await this.aiService.generateDeck({
prompt,
deckTitle,
deckDescription,
cardCount,
cardTypes: requestedTypes,
difficulty: difficulty || 'intermediate',
language: language || 'en',
});
if (!isOk(aiResult)) {
throw aiResult.error; // Caught by AppExceptionFilter
}
const { cards, metadata } = aiResult.value;
// 3. Create deck in database
const newDeck = await this.deckRepository.create({
userId: user.sub,
title: deckTitle,
description: deckDescription,
isPublic: false,
settings: { aiGenerated: true, difficulty },
tags: tags || [],
metadata: {
aiModel: metadata.model,
generationTime: metadata.generationTime,
prompt,
},
});
// 4. Create cards in database
const cardsToCreate = cards.map((card, index) => ({
deckId: newDeck.id,
title: card.title || `Card ${index + 1}`,
content: card.content,
cardType: card.cardType,
position: index,
aiModel: metadata.model,
aiPrompt: prompt,
}));
await this.cardRepository.createMany(cardsToCreate);
// 5. Consume credits
await this.creditClient.consumeCredits(
user.sub,
operationType,
creditCost,
`Generated AI deck: ${deckTitle}`,
{
deckId: newDeck.id,
deckTitle,
cardCount: cards.length,
prompt,
},
);
this.logger.log(
`AI deck generated successfully for user ${user.sub}. ` +
`${cards.length} cards created in ${metadata.generationTime}ms. ` +
`${creditCost} credits consumed.`,
);
return {
success: true,
userId: user.sub,
deck: newDeck,
cards,
cardCount: cards.length,
creditsUsed: creditCost,
metadata,
message: 'Deck generated successfully with AI',
};
}
@Put('decks/:id')
async updateDeck(
@CurrentUser() user: any,
@Param('id') deckId: string,
@Body() deckData: any,
) {
this.logger.log(`Updating deck ${deckId} for user: ${user.sub}`);
const updatedDeck = await this.deckRepository.update(deckId, user.sub, {
title: deckData.title,
description: deckData.description,
coverImageUrl: deckData.coverImageUrl,
isPublic: deckData.isPublic,
settings: deckData.settings,
tags: deckData.tags,
metadata: deckData.metadata,
});
if (!updatedDeck) {
throw new BadRequestException({
error: 'deck_not_found',
message: 'Deck not found or you do not have permission to update it',
});
}
return {
success: true,
userId: user.sub,
deck: updatedDeck,
};
}
@Delete('decks/:id')
async deleteDeck(@CurrentUser() user: any, @Param('id') deckId: string) {
this.logger.log(`Deleting deck ${deckId} for user: ${user.sub}`);
const deleted = await this.deckRepository.delete(deckId, user.sub);
if (!deleted) {
throw new BadRequestException({
error: 'deck_not_found',
message: 'Deck not found or you do not have permission to delete it',
});
}
return {
success: true,
userId: user.sub,
deckId,
};
}
@Get('cards')
async getUserCards(@CurrentUser() user: any) {
this.logger.log(`Getting cards for user: ${user.sub}`);
const cards = await this.cardRepository.findByUserDecks(user.sub);
return {
userId: user.sub,
cards,
count: cards.length,
};
}
@Post('cards')
async createCard(@CurrentUser() user: any, @Body() cardData: any) {
this.logger.log(`Creating card for user: ${user.sub}`);
// Verify the deck belongs to the user
const deck = await this.deckRepository.findByIdAndUserId(cardData.deckId, user.sub);
if (!deck) {
throw new BadRequestException({
error: 'deck_not_found',
message: 'Deck not found or you do not have permission to add cards to it',
});
}
const card = await this.cardRepository.create({
deckId: cardData.deckId,
title: cardData.title,
content: cardData.content,
cardType: cardData.cardType || 'flashcard',
position: cardData.position ?? 0,
aiModel: cardData.aiModel,
aiPrompt: cardData.aiPrompt,
});
return {
success: true,
userId: user.sub,
card,
};
}
@Get('stats')
async getUserStats(@CurrentUser() user: any) {
this.logger.log(`Getting stats for user: ${user.sub}`);
const [stats, totalDecks, totalCards] = await Promise.all([
this.userStatsRepository.findOrCreate(user.sub),
this.deckRepository.countByUserId(user.sub),
this.cardRepository.countByUserId(user.sub),
]);
return {
userId: user.sub,
stats: {
...stats,
totalDecks,
totalCards,
},
};
}
}

View file

@ -0,0 +1,54 @@
import { Controller, Get } from '@nestjs/common';
import {
HealthCheckService,
HttpHealthIndicator,
HealthCheck,
} from '@nestjs/terminus';
import { ConfigService } from '@nestjs/config';
import { Public } from '@mana-core/nestjs-integration/decorators';
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private http: HttpHealthIndicator,
private configService: ConfigService,
) {}
@Public()
@Get()
@HealthCheck()
check() {
const manaServiceUrl = this.configService.get<string>('MANA_SERVICE_URL')!;
return this.health.check([
() => this.http.pingCheck('mana-core', manaServiceUrl),
// PostgreSQL health is checked via database module initialization
]);
}
@Public()
@Get('ready')
@HealthCheck()
readiness() {
return this.health.check([
() => ({
ready: {
status: 'up',
message: 'Service is ready to receive requests',
},
}),
]);
}
@Public()
@Get('live')
liveness() {
return {
status: 'ok',
timestamp: new Date(),
uptime: process.uptime(),
environment: this.configService.get<string>('NODE_ENV'),
};
}
}

View file

@ -0,0 +1,106 @@
import { Controller, Get, UseGuards, Query, Logger } from '@nestjs/common';
import { OptionalAuthGuard } from '@mana-core/nestjs-integration/guards';
import { CurrentUser, Public } from '@mana-core/nestjs-integration/decorators';
import { DeckRepository, UserStatsRepository, DeckTemplateRepository } from '../database';
@Controller('public')
export class PublicController {
private readonly logger = new Logger(PublicController.name);
constructor(
private readonly deckRepository: DeckRepository,
private readonly userStatsRepository: UserStatsRepository,
private readonly deckTemplateRepository: DeckTemplateRepository,
) {}
@Public()
@Get('health')
health() {
return { status: 'ok', timestamp: new Date() };
}
@Public()
@Get('version')
version() {
return {
version: '1.0.0',
service: 'manadeck-backend',
environment: process.env.NODE_ENV,
};
}
@UseGuards(OptionalAuthGuard)
@Get('featured-decks')
async getFeaturedDecks(@CurrentUser() user?: any) {
const decks = await this.deckRepository.findFeatured(10);
if (user) {
this.logger.log(`Getting personalized featured decks for user: ${user.sub}`);
return {
type: 'personalized',
userId: user.sub,
decks,
count: decks.length,
};
}
this.logger.log('Getting generic featured decks');
return {
type: 'generic',
decks,
count: decks.length,
};
}
@UseGuards(OptionalAuthGuard)
@Get('leaderboard')
async getLeaderboard(@CurrentUser() user?: any, @Query('limit') limit = '10') {
const limitNum = Math.min(parseInt(limit, 10) || 10, 100);
const leaderboard = await this.userStatsRepository.getLeaderboard(limitNum);
if (user) {
this.logger.log(`Getting leaderboard with user ${user.sub} position`);
const userPosition = await this.userStatsRepository.getUserPosition(user.sub);
return {
leaderboard,
userPosition,
userId: user.sub,
limit: limitNum,
};
}
return {
leaderboard,
limit: limitNum,
};
}
@Public()
@Get('deck-templates')
async getDeckTemplates(@Query('category') category?: string) {
const templates = category
? await this.deckTemplateRepository.findByCategory(category)
: await this.deckTemplateRepository.findPublic();
return {
category: category || 'all',
templates,
count: templates.length,
};
}
@Public()
@Get('announcements')
getAnnouncements() {
return {
announcements: [
{
id: '1',
title: 'Welcome to ManaDeck!',
content: 'Your deck management system is now powered by Mana Core authentication.',
date: new Date(),
},
],
};
}
}

View file

@ -0,0 +1,29 @@
import { Module, Global, OnModuleDestroy, Logger } from '@nestjs/common';
import { getDb, closeDb, type Database } from '@manacore/manadeck-database/client';
export const DATABASE_TOKEN = 'DATABASE';
@Global()
@Module({
providers: [
{
provide: DATABASE_TOKEN,
useFactory: () => {
const logger = new Logger('DatabaseModule');
logger.log('Initializing database connection');
return getDb();
},
},
],
exports: [DATABASE_TOKEN],
})
export class DatabaseModule implements OnModuleDestroy {
private readonly logger = new Logger(DatabaseModule.name);
async onModuleDestroy() {
this.logger.log('Closing database connection');
await closeDb();
}
}
export type { Database };

View file

@ -0,0 +1,2 @@
export { DatabaseModule, DATABASE_TOKEN, type Database } from './database.module';
export * from './repositories';

View file

@ -0,0 +1,125 @@
import { Injectable, Inject, Logger } from '@nestjs/common';
import { DATABASE_TOKEN, type Database } from '../database.module';
import {
cards,
decks,
type Card,
type NewCard,
eq,
and,
asc,
sql,
} from '@manacore/manadeck-database';
@Injectable()
export class CardRepository {
private readonly logger = new Logger(CardRepository.name);
constructor(@Inject(DATABASE_TOKEN) private readonly db: Database) {}
async findByDeckId(deckId: string): Promise<Card[]> {
this.logger.debug(`Finding cards for deck: ${deckId}`);
return this.db
.select()
.from(cards)
.where(eq(cards.deckId, deckId))
.orderBy(asc(cards.position));
}
async findByDeckIdAndUserId(deckId: string, userId: string): Promise<Card[]> {
// Join with decks to verify ownership
const result = await this.db
.select({
card: cards,
})
.from(cards)
.innerJoin(decks, eq(cards.deckId, decks.id))
.where(and(eq(cards.deckId, deckId), eq(decks.userId, userId)))
.orderBy(asc(cards.position));
return result.map((r) => r.card);
}
async findById(id: string): Promise<Card | null> {
const result = await this.db
.select()
.from(cards)
.where(eq(cards.id, id))
.limit(1);
return result[0] || null;
}
async findByUserDecks(userId: string): Promise<Card[]> {
// Get all cards from decks owned by the user
const result = await this.db
.select({
card: cards,
})
.from(cards)
.innerJoin(decks, eq(cards.deckId, decks.id))
.where(eq(decks.userId, userId))
.orderBy(asc(cards.deckId), asc(cards.position));
return result.map((r) => r.card);
}
async create(data: NewCard): Promise<Card> {
this.logger.debug(`Creating card in deck: ${data.deckId}`);
const result = await this.db.insert(cards).values(data).returning();
return result[0];
}
async createMany(data: NewCard[]): Promise<Card[]> {
if (data.length === 0) return [];
this.logger.debug(`Creating ${data.length} cards`);
return this.db.insert(cards).values(data).returning();
}
async update(
id: string,
data: Partial<Omit<NewCard, 'id' | 'deckId' | 'createdAt'>>
): Promise<Card | null> {
this.logger.debug(`Updating card: ${id}`);
const result = await this.db
.update(cards)
.set({
...data,
updatedAt: new Date(),
})
.where(eq(cards.id, id))
.returning();
return result[0] || null;
}
async delete(id: string): Promise<boolean> {
this.logger.debug(`Deleting card: ${id}`);
const result = await this.db
.delete(cards)
.where(eq(cards.id, id))
.returning({ id: cards.id });
return result.length > 0;
}
async deleteByDeckId(deckId: string): Promise<number> {
const result = await this.db
.delete(cards)
.where(eq(cards.deckId, deckId))
.returning({ id: cards.id });
return result.length;
}
async countByDeckId(deckId: string): Promise<number> {
const result = await this.db
.select({ count: sql<number>`count(*)::int` })
.from(cards)
.where(eq(cards.deckId, deckId));
return result[0]?.count || 0;
}
async countByUserId(userId: string): Promise<number> {
const result = await this.db
.select({ count: sql<number>`count(*)::int` })
.from(cards)
.innerJoin(decks, eq(cards.deckId, decks.id))
.where(eq(decks.userId, userId));
return result[0]?.count || 0;
}
}

View file

@ -0,0 +1,121 @@
import { Injectable, Inject, Logger } from '@nestjs/common';
import { DATABASE_TOKEN, type Database } from '../database.module';
import {
deckTemplates,
type DeckTemplate,
type NewDeckTemplate,
eq,
and,
desc,
sql,
} from '@manacore/manadeck-database';
@Injectable()
export class DeckTemplateRepository {
private readonly logger = new Logger(DeckTemplateRepository.name);
constructor(@Inject(DATABASE_TOKEN) private readonly db: Database) {}
async findAll(includeInactive = false): Promise<DeckTemplate[]> {
if (includeInactive) {
return this.db
.select()
.from(deckTemplates)
.orderBy(desc(deckTemplates.popularity));
}
return this.db
.select()
.from(deckTemplates)
.where(eq(deckTemplates.isActive, true))
.orderBy(desc(deckTemplates.popularity));
}
async findById(id: string): Promise<DeckTemplate | null> {
const result = await this.db
.select()
.from(deckTemplates)
.where(eq(deckTemplates.id, id))
.limit(1);
return result[0] || null;
}
async findByCategory(category: string): Promise<DeckTemplate[]> {
return this.db
.select()
.from(deckTemplates)
.where(
and(
eq(deckTemplates.category, category),
eq(deckTemplates.isActive, true)
)
)
.orderBy(desc(deckTemplates.popularity));
}
async findPublic(): Promise<DeckTemplate[]> {
return this.db
.select()
.from(deckTemplates)
.where(
and(eq(deckTemplates.isPublic, true), eq(deckTemplates.isActive, true))
)
.orderBy(desc(deckTemplates.popularity));
}
async create(data: NewDeckTemplate): Promise<DeckTemplate> {
this.logger.debug(`Creating deck template: ${data.title}`);
const result = await this.db
.insert(deckTemplates)
.values(data)
.returning();
return result[0];
}
async update(
id: string,
data: Partial<Omit<NewDeckTemplate, 'id' | 'createdAt'>>
): Promise<DeckTemplate | null> {
this.logger.debug(`Updating deck template: ${id}`);
const result = await this.db
.update(deckTemplates)
.set({
...data,
updatedAt: new Date(),
})
.where(eq(deckTemplates.id, id))
.returning();
return result[0] || null;
}
async delete(id: string): Promise<boolean> {
this.logger.debug(`Deleting deck template: ${id}`);
const result = await this.db
.delete(deckTemplates)
.where(eq(deckTemplates.id, id))
.returning({ id: deckTemplates.id });
return result.length > 0;
}
async incrementPopularity(id: string): Promise<DeckTemplate | null> {
const result = await this.db
.update(deckTemplates)
.set({
popularity: sql`${deckTemplates.popularity} + 1`,
updatedAt: new Date(),
})
.where(eq(deckTemplates.id, id))
.returning();
return result[0] || null;
}
async getCategories(): Promise<string[]> {
const result = await this.db
.selectDistinct({ category: deckTemplates.category })
.from(deckTemplates)
.where(eq(deckTemplates.isActive, true));
return result
.map((r) => r.category)
.filter((c): c is string => c !== null);
}
}

View file

@ -0,0 +1,103 @@
import { Injectable, Inject, Logger } from '@nestjs/common';
import { DATABASE_TOKEN, type Database } from '../database.module';
import {
decks,
type Deck,
type NewDeck,
eq,
and,
desc,
sql,
} from '@manacore/manadeck-database';
@Injectable()
export class DeckRepository {
private readonly logger = new Logger(DeckRepository.name);
constructor(@Inject(DATABASE_TOKEN) private readonly db: Database) {}
async findByUserId(userId: string): Promise<Deck[]> {
this.logger.debug(`Finding decks for user: ${userId}`);
return this.db
.select()
.from(decks)
.where(eq(decks.userId, userId))
.orderBy(desc(decks.createdAt));
}
async findById(id: string): Promise<Deck | null> {
const result = await this.db
.select()
.from(decks)
.where(eq(decks.id, id))
.limit(1);
return result[0] || null;
}
async findByIdAndUserId(id: string, userId: string): Promise<Deck | null> {
const result = await this.db
.select()
.from(decks)
.where(and(eq(decks.id, id), eq(decks.userId, userId)))
.limit(1);
return result[0] || null;
}
async create(data: NewDeck): Promise<Deck> {
this.logger.debug(`Creating deck: ${data.title}`);
const result = await this.db.insert(decks).values(data).returning();
return result[0];
}
async update(
id: string,
userId: string,
data: Partial<Omit<NewDeck, 'id' | 'userId' | 'createdAt'>>
): Promise<Deck | null> {
this.logger.debug(`Updating deck: ${id}`);
const result = await this.db
.update(decks)
.set({
...data,
updatedAt: new Date(),
})
.where(and(eq(decks.id, id), eq(decks.userId, userId)))
.returning();
return result[0] || null;
}
async delete(id: string, userId: string): Promise<boolean> {
this.logger.debug(`Deleting deck: ${id}`);
const result = await this.db
.delete(decks)
.where(and(eq(decks.id, id), eq(decks.userId, userId)))
.returning({ id: decks.id });
return result.length > 0;
}
async findFeatured(limit = 10): Promise<Deck[]> {
return this.db
.select()
.from(decks)
.where(and(eq(decks.isFeatured, true), eq(decks.isPublic, true)))
.orderBy(desc(decks.featuredAt))
.limit(limit);
}
async findPublic(limit = 10): Promise<Deck[]> {
return this.db
.select()
.from(decks)
.where(eq(decks.isPublic, true))
.orderBy(desc(decks.createdAt))
.limit(limit);
}
async countByUserId(userId: string): Promise<number> {
const result = await this.db
.select({ count: sql<number>`count(*)::int` })
.from(decks)
.where(eq(decks.userId, userId));
return result[0]?.count || 0;
}
}

View file

@ -0,0 +1,4 @@
export { DeckRepository } from './deck.repository';
export { CardRepository } from './card.repository';
export { UserStatsRepository } from './user-stats.repository';
export { DeckTemplateRepository } from './deck-template.repository';

View file

@ -0,0 +1,146 @@
import { Injectable, Inject, Logger } from '@nestjs/common';
import { DATABASE_TOKEN, type Database } from '../database.module';
import {
userStats,
type UserStats,
type NewUserStats,
eq,
desc,
sql,
} from '@manacore/manadeck-database';
@Injectable()
export class UserStatsRepository {
private readonly logger = new Logger(UserStatsRepository.name);
constructor(@Inject(DATABASE_TOKEN) private readonly db: Database) {}
async findByUserId(userId: string): Promise<UserStats | null> {
this.logger.debug(`Finding stats for user: ${userId}`);
const result = await this.db
.select()
.from(userStats)
.where(eq(userStats.userId, userId))
.limit(1);
return result[0] || null;
}
async findOrCreate(userId: string): Promise<UserStats> {
const existing = await this.findByUserId(userId);
if (existing) return existing;
this.logger.debug(`Creating stats for user: ${userId}`);
const result = await this.db
.insert(userStats)
.values({ userId })
.returning();
return result[0];
}
async update(
userId: string,
data: Partial<Omit<NewUserStats, 'userId' | 'createdAt'>>
): Promise<UserStats | null> {
this.logger.debug(`Updating stats for user: ${userId}`);
const result = await this.db
.update(userStats)
.set({
...data,
updatedAt: new Date(),
})
.where(eq(userStats.userId, userId))
.returning();
return result[0] || null;
}
async getLeaderboard(limit = 10): Promise<UserStats[]> {
return this.db
.select()
.from(userStats)
.orderBy(desc(userStats.totalWins))
.limit(limit);
}
async getLeaderboardByStreak(limit = 10): Promise<UserStats[]> {
return this.db
.select()
.from(userStats)
.orderBy(desc(userStats.streakDays))
.limit(limit);
}
async getUserPosition(userId: string): Promise<number | null> {
// Get user's total wins
const user = await this.findByUserId(userId);
if (!user) return null;
// Count users with higher wins
const result = await this.db
.select({ count: sql<number>`count(*)::int` })
.from(userStats)
.where(sql`${userStats.totalWins} > ${user.totalWins}`);
return (result[0]?.count || 0) + 1;
}
async incrementWins(userId: string, count = 1): Promise<UserStats | null> {
const result = await this.db
.update(userStats)
.set({
totalWins: sql`${userStats.totalWins} + ${count}`,
updatedAt: new Date(),
})
.where(eq(userStats.userId, userId))
.returning();
return result[0] || null;
}
async incrementSessions(userId: string): Promise<UserStats | null> {
const result = await this.db
.update(userStats)
.set({
totalSessions: sql`${userStats.totalSessions} + 1`,
updatedAt: new Date(),
})
.where(eq(userStats.userId, userId))
.returning();
return result[0] || null;
}
async updateStudyProgress(
userId: string,
cardsStudied: number,
timeSeconds: number,
accuracy: number
): Promise<UserStats | null> {
const today = new Date().toISOString().split('T')[0];
const existing = await this.findByUserId(userId);
if (!existing) {
await this.findOrCreate(userId);
}
// Calculate new average accuracy
const currentAvg = existing ? parseFloat(existing.averageAccuracy) : 0;
const currentTotal = existing?.totalCardsStudied || 0;
const newTotal = currentTotal + cardsStudied;
const newAvg =
newTotal > 0
? (currentAvg * currentTotal + accuracy * cardsStudied) / newTotal
: accuracy;
const result = await this.db
.update(userStats)
.set({
totalCardsStudied: sql`${userStats.totalCardsStudied} + ${cardsStudied}`,
totalTimeSeconds: sql`${userStats.totalTimeSeconds} + ${timeSeconds}`,
averageAccuracy: newAvg.toFixed(2),
lastStudyDate: today,
updatedAt: new Date(),
})
.where(eq(userStats.userId, userId))
.returning();
return result[0] || null;
}
}

View file

@ -0,0 +1,51 @@
import { NestFactory } from '@nestjs/core';
import { Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AppExceptionFilter } from '@manacore/shared-errors/nestjs';
import { AppModule } from './app.module';
async function bootstrap() {
const logger = new Logger('Bootstrap');
logger.log('Starting ManaDeck Backend...');
// Debug: Log environment variables before validation
logger.log('=== Environment Variables Debug ===');
logger.log(`APP_ID: ${process.env.APP_ID ? process.env.APP_ID.substring(0, 20) + '...' : 'NOT SET'}`);
logger.log(`DATABASE_URL: ${process.env.DATABASE_URL ? '[SET]' : 'NOT SET'}`);
logger.log(`MANA_SERVICE_URL: ${process.env.MANA_SERVICE_URL || 'NOT SET'}`);
logger.log('===================================');
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn', 'log', 'debug', 'verbose'],
});
const configService = app.get(ConfigService);
// Global exception filter for standardized error responses
app.useGlobalFilters(new AppExceptionFilter());
// Enable CORS
app.enableCors({
origin: configService.get('FRONTEND_URL') || true,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
});
// Global prefix
app.setGlobalPrefix('v1', {
exclude: ['health', 'health/ready', 'health/live'],
});
const port = configService.get<number>('PORT') || 8080;
await app.listen(port);
logger.log(`Environment: ${configService.get('NODE_ENV')}`);
logger.log(`Mana Service URL: ${configService.get('MANA_SERVICE_URL')}`);
logger.log(`App ID: ${configService.get('APP_ID')}`);
logger.log(`Application running on port ${port}`);
logger.log(`Health check available at http://localhost:${port}/health`);
}
bootstrap();

View file

@ -0,0 +1,314 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { GoogleGenAI, Type } from '@google/genai';
import {
type AsyncResult,
ok,
err,
ServiceError,
} from '@manacore/shared-errors';
export type CardType = 'text' | 'flashcard' | 'quiz' | 'mixed';
export interface TextContent {
text: string;
}
export interface FlashcardContent {
front: string;
back: string;
hint?: string;
}
export interface QuizContent {
question: string;
options: string[];
correctAnswer: number;
explanation?: string;
}
export interface GeneratedCard {
cardType: CardType;
title?: string;
content: TextContent | FlashcardContent | QuizContent;
}
export interface DeckGenerationRequest {
prompt: string;
deckTitle: string;
deckDescription?: string;
cardCount?: number;
cardTypes?: CardType[];
difficulty?: 'beginner' | 'intermediate' | 'advanced';
language?: string;
}
export interface DeckGenerationData {
cards: GeneratedCard[];
metadata: {
model: string;
tokensUsed?: number;
generationTime: number;
};
}
@Injectable()
export class AiService {
private readonly logger = new Logger(AiService.name);
private readonly ai: GoogleGenAI | null;
private readonly model = 'gemini-2.0-flash';
constructor(private readonly configService: ConfigService) {
const apiKey = this.configService.get<string>('GOOGLE_GENAI_API_KEY');
if (apiKey) {
this.ai = new GoogleGenAI({ apiKey });
this.logger.log('Google Gemini AI initialized successfully');
} else {
this.ai = null;
this.logger.warn('Google Gemini API key not configured - AI features disabled');
}
}
isAvailable(): boolean {
return this.ai !== null;
}
async generateDeck(request: DeckGenerationRequest): AsyncResult<DeckGenerationData> {
const startTime = Date.now();
if (!this.ai) {
return err(
ServiceError.unavailable('AI (Google Gemini not configured)'),
);
}
const {
prompt,
deckTitle,
deckDescription,
cardCount = 10,
cardTypes = ['flashcard', 'quiz'],
difficulty = 'intermediate',
language = 'en',
} = request;
try {
const systemPrompt = this.buildSystemPrompt(cardTypes, difficulty, language);
const userPrompt = this.buildUserPrompt(
prompt,
deckTitle,
deckDescription,
cardCount,
cardTypes,
);
const response = await this.ai.models.generateContent({
model: this.model,
contents: userPrompt,
config: {
systemInstruction: systemPrompt,
responseMimeType: 'application/json',
responseSchema: this.buildResponseSchema(cardTypes),
},
});
const generationTime = Date.now() - startTime;
const responseText = response.text?.trim();
if (!responseText) {
return err(
ServiceError.generationFailed('Google Gemini', 'Empty response from AI'),
);
}
const parsed = JSON.parse(responseText);
const cards: GeneratedCard[] = parsed.cards || [];
if (cards.length === 0) {
return err(
ServiceError.generationFailed('Google Gemini', 'No cards generated'),
);
}
this.logger.log(`Generated ${cards.length} cards in ${generationTime}ms`);
return ok({
cards,
metadata: {
model: this.model,
tokensUsed: response.usageMetadata?.totalTokenCount,
generationTime,
},
});
} catch (error) {
this.logger.error('AI deck generation failed:', error);
return err(
ServiceError.generationFailed(
'Google Gemini',
error instanceof Error ? error.message : 'Unknown error occurred',
error instanceof Error ? error : undefined,
),
);
}
}
private buildSystemPrompt(cardTypes: CardType[], difficulty: string, language: string): string {
const cardTypeDescriptions = {
text: 'Text cards contain informational content or explanations.',
flashcard: 'Flashcards have a front (question/term) and back (answer/definition), optionally with a hint.',
quiz: 'Quiz cards have a question, 4 options (A-D), correct answer index (0-3), and an explanation.',
mixed: 'Mixed cards combine multiple content types.',
};
const enabledTypes = cardTypes.map(t => `- ${t}: ${cardTypeDescriptions[t]}`).join('\n');
return `You are an expert educational content creator specializing in flashcards and study materials.
Your task is to generate high-quality learning cards for a deck based on the user's topic.
CARD TYPES YOU CAN CREATE:
${enabledTypes}
DIFFICULTY LEVEL: ${difficulty}
- beginner: Simple concepts, basic vocabulary, straightforward questions
- intermediate: More complex topics, requires some prior knowledge
- advanced: Deep understanding, nuanced questions, expert-level content
LANGUAGE: ${language === 'de' ? 'German' : language === 'en' ? 'English' : language}
Generate all content in this language.
QUALITY GUIDELINES:
1. Make content educational and accurate
2. Vary question styles to keep learning engaging
3. For flashcards: front should be concise, back should be complete but not verbose
4. For quiz: all 4 options should be plausible, avoid obviously wrong answers
5. Include helpful hints for difficult flashcards
6. Add explanations for quiz questions to reinforce learning
7. Progress from easier to harder cards when possible`;
}
private buildUserPrompt(
prompt: string,
deckTitle: string,
deckDescription?: string,
cardCount: number = 10,
cardTypes: CardType[] = ['flashcard', 'quiz'],
): string {
const typeDistribution = this.suggestTypeDistribution(cardCount, cardTypes);
return `Create a deck of ${cardCount} learning cards about:
DECK TITLE: ${deckTitle}
${deckDescription ? `DESCRIPTION: ${deckDescription}` : ''}
USER'S REQUEST:
${prompt}
CARD DISTRIBUTION:
${typeDistribution}
Generate exactly ${cardCount} cards that cover the topic comprehensively.
Ensure variety in the questions and good coverage of the subject matter.`;
}
private suggestTypeDistribution(cardCount: number, cardTypes: CardType[]): string {
if (cardTypes.length === 1) {
return `- All ${cardCount} cards should be ${cardTypes[0]} type`;
}
const hasFlashcard = cardTypes.includes('flashcard');
const hasQuiz = cardTypes.includes('quiz');
const hasText = cardTypes.includes('text');
if (hasFlashcard && hasQuiz && !hasText) {
const flashcardCount = Math.ceil(cardCount * 0.6);
const quizCount = cardCount - flashcardCount;
return `- ${flashcardCount} flashcards for core concepts\n- ${quizCount} quiz cards to test understanding`;
}
if (hasFlashcard && hasQuiz && hasText) {
const textCount = Math.ceil(cardCount * 0.2);
const flashcardCount = Math.ceil((cardCount - textCount) * 0.6);
const quizCount = cardCount - textCount - flashcardCount;
return `- ${textCount} text cards for introductions/explanations\n- ${flashcardCount} flashcards for key terms\n- ${quizCount} quiz cards for testing`;
}
return `- Mix of ${cardTypes.join(', ')} cards as appropriate for the content`;
}
private buildResponseSchema(cardTypes: CardType[]): any {
const cardSchemas: any[] = [];
if (cardTypes.includes('flashcard')) {
cardSchemas.push({
type: Type.OBJECT,
properties: {
cardType: { type: Type.STRING, enum: ['flashcard'] },
title: { type: Type.STRING },
content: {
type: Type.OBJECT,
properties: {
front: { type: Type.STRING },
back: { type: Type.STRING },
hint: { type: Type.STRING },
},
required: ['front', 'back'],
},
},
required: ['cardType', 'content'],
});
}
if (cardTypes.includes('quiz')) {
cardSchemas.push({
type: Type.OBJECT,
properties: {
cardType: { type: Type.STRING, enum: ['quiz'] },
title: { type: Type.STRING },
content: {
type: Type.OBJECT,
properties: {
question: { type: Type.STRING },
options: { type: Type.ARRAY, items: { type: Type.STRING } },
correctAnswer: { type: Type.NUMBER },
explanation: { type: Type.STRING },
},
required: ['question', 'options', 'correctAnswer'],
},
},
required: ['cardType', 'content'],
});
}
if (cardTypes.includes('text')) {
cardSchemas.push({
type: Type.OBJECT,
properties: {
cardType: { type: Type.STRING, enum: ['text'] },
title: { type: Type.STRING },
content: {
type: Type.OBJECT,
properties: {
text: { type: Type.STRING },
},
required: ['text'],
},
},
required: ['cardType', 'content'],
});
}
return {
type: Type.OBJECT,
properties: {
cards: {
type: Type.ARRAY,
items: cardSchemas.length === 1 ? cardSchemas[0] : { anyOf: cardSchemas },
},
},
required: ['cards'],
};
}
}

View file

@ -0,0 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication<App>;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

View file

@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View file

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View file

@ -0,0 +1,25 @@
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"resolvePackageJsonExports": true,
"esModuleInterop": true,
"isolatedModules": true,
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false
}
}

View file

@ -0,0 +1,220 @@
#!/bin/bash
# Script to verify the build and local Docker testing for Manadeck Backend
set -e # Exit on error
echo "=========================================="
echo "Manadeck Backend Build Verification"
echo "=========================================="
echo ""
# Colors for output
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Function to print success
success() {
echo -e "${GREEN}${NC} $1"
}
# Function to print error
error() {
echo -e "${RED}${NC} $1"
}
# Function to print warning
warning() {
echo -e "${YELLOW}${NC} $1"
}
# Check current directory
echo "1. Checking directory..."
if [ ! -f "package.json" ]; then
error "package.json not found. Please run this script from manadeck/backend directory"
exit 1
fi
success "Running from correct directory: $(pwd)"
echo ""
# Check for required files
echo "2. Checking required files..."
REQUIRED_FILES=("Dockerfile" "cloudbuild.yaml" "tsconfig.json" "nest-cli.json" "src/main.ts")
for file in "${REQUIRED_FILES[@]}"; do
if [ -f "$file" ]; then
success "$file exists"
else
error "$file not found"
exit 1
fi
done
echo ""
# Check Node.js version
echo "3. Checking Node.js version..."
if command -v node &> /dev/null; then
NODE_VERSION=$(node -v)
success "Node.js version: $NODE_VERSION"
else
error "Node.js not installed"
exit 1
fi
echo ""
# Install dependencies
echo "4. Installing dependencies..."
if [ ! -d "node_modules" ]; then
warning "node_modules not found. Running npm ci..."
npm ci
else
success "node_modules exists"
fi
echo ""
# Run linter
echo "5. Running linter..."
if npm run lint; then
success "Linting passed"
else
warning "Linting failed (non-blocking)"
fi
echo ""
# Build the project
echo "6. Building the project..."
if npm run build; then
success "Build successful"
else
error "Build failed"
exit 1
fi
echo ""
# Check dist directory
echo "7. Checking build output..."
if [ -d "dist" ]; then
success "dist directory exists"
if [ -f "dist/main.js" ]; then
success "dist/main.js exists"
FILE_SIZE=$(du -h dist/main.js | cut -f1)
echo " File size: $FILE_SIZE"
else
error "dist/main.js not found"
exit 1
fi
else
error "dist directory not found"
exit 1
fi
echo ""
# Check for .env file
echo "8. Checking environment configuration..."
if [ -f ".env" ]; then
success ".env file exists"
else
warning ".env file not found (required for local testing)"
echo " Create .env from .env.example for local development"
fi
echo ""
# Docker build test (optional)
echo "9. Docker build test..."
read -p "Do you want to test Docker build? (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Building Docker image..."
if command -v docker &> /dev/null; then
if docker build -t manadeck-backend:test .; then
success "Docker build successful"
# Optional: Run container for health check
read -p "Do you want to test the Docker container? (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Starting Docker container..."
# Check if .env exists for env vars
if [ -f ".env" ]; then
docker run -d --name manadeck-test -p 8080:8080 --env-file .env manadeck-backend:test
else
warning "No .env file found. Starting container without environment variables..."
docker run -d --name manadeck-test -p 8080:8080 manadeck-backend:test
fi
echo "Waiting for service to start..."
sleep 5
# Test health endpoint
echo "Testing health endpoint..."
if curl -s http://localhost:8080/health > /dev/null; then
success "Health check passed"
curl http://localhost:8080/health | jq . || cat
else
warning "Health check failed (may need environment variables)"
fi
# Cleanup
echo ""
echo "Stopping and removing test container..."
docker stop manadeck-test > /dev/null 2>&1
docker rm manadeck-test > /dev/null 2>&1
success "Cleanup complete"
fi
# Optional: Remove test image
read -p "Do you want to remove the test Docker image? (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
docker rmi manadeck-backend:test > /dev/null 2>&1
success "Test image removed"
fi
else
error "Docker build failed"
exit 1
fi
else
warning "Docker not installed. Skipping Docker build test."
fi
else
echo "Skipping Docker build test"
fi
echo ""
# Check cloudbuild.yaml version
echo "10. Checking cloudbuild.yaml version..."
if [ -f "cloudbuild.yaml" ]; then
CURRENT_VERSION=$(grep -o "manadeck-backend:v[0-9.]*" cloudbuild.yaml | head -1 | sed 's/manadeck-backend://')
success "Current version in cloudbuild.yaml: $CURRENT_VERSION"
warning "Before deploying, consider incrementing the version in cloudbuild.yaml"
else
error "cloudbuild.yaml not found"
fi
echo ""
# Summary
echo "=========================================="
echo "Build Verification Summary"
echo "=========================================="
success "All required files present"
success "Dependencies installed"
success "Build completed successfully"
success "Build artifacts verified"
echo ""
echo "Next steps:"
echo "1. Review and update version in cloudbuild.yaml if needed"
echo "2. Commit your changes to git"
echo "3. Push to main branch to trigger automatic deployment"
echo " OR"
echo " Deploy manually:"
echo " cd $(pwd)"
echo " gcloud builds submit --project=memo-2c4c4 --config=cloudbuild.yaml ."
echo ""
echo "For deployment help, see DEPLOY_MANUAL.md"
echo ""

View file

@ -0,0 +1,17 @@
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
import sitemap from '@astrojs/sitemap';
// https://astro.build/config
export default defineConfig({
site: 'https://manadeck.app',
integrations: [
tailwind(),
sitemap()
],
vite: {
ssr: {
noExternal: ['@manacore/shared-landing-ui']
}
}
});

View file

@ -0,0 +1,27 @@
{
"name": "@manadeck/landing",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro",
"type-check": "astro check"
},
"dependencies": {
"@astrojs/check": "^0.9.0",
"@astrojs/sitemap": "^3.2.1",
"@manacore/shared-landing-ui": "workspace:*",
"astro": "^5.16.0",
"astro-icon": "^1.1.5",
"typescript": "^5.0.0"
},
"devDependencies": {
"@astrojs/tailwind": "^6.0.0",
"@tailwindcss/typography": "^0.5.16",
"tailwindcss": "^3.4.17"
}
}

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" rx="20" fill="#7C3AED"/>
<text x="50" y="70" font-size="60" text-anchor="middle" fill="white">🃏</text>
</svg>

After

Width:  |  Height:  |  Size: 211 B

View file

@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://manadeck.app/sitemap-index.xml

View file

@ -0,0 +1,78 @@
---
const footerLinks = {
product: [
{ href: '#features', label: 'Features' },
{ href: '#pricing', label: 'Preise' },
{ href: '#faq', label: 'FAQ' }
],
legal: [
{ href: '/privacy', label: 'Datenschutz' },
{ href: '/terms', label: 'AGB' },
{ href: '/imprint', label: 'Impressum' }
]
};
const currentYear = new Date().getFullYear();
---
<footer class="bg-background-card border-t border-border">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<!-- Brand -->
<div class="col-span-1 md:col-span-2">
<a href="/" class="flex items-center gap-2 mb-4">
<span class="text-2xl">🃏</span>
<span class="font-bold text-xl text-text-primary">ManaDeck</span>
</a>
<p class="text-text-secondary text-sm max-w-md">
Dein KI-gestützter Lernpartner. Erstelle intelligente Karteikarten aus deinen Notizen
und lerne effizienter mit Spaced Repetition.
</p>
</div>
<!-- Product Links -->
<div>
<h3 class="font-semibold text-text-primary mb-4">Produkt</h3>
<ul class="space-y-2">
{footerLinks.product.map(link => (
<li>
<a
href={link.href}
class="text-text-secondary hover:text-text-primary transition-colors text-sm"
>
{link.label}
</a>
</li>
))}
</ul>
</div>
<!-- Legal Links -->
<div>
<h3 class="font-semibold text-text-primary mb-4">Rechtliches</h3>
<ul class="space-y-2">
{footerLinks.legal.map(link => (
<li>
<a
href={link.href}
class="text-text-secondary hover:text-text-primary transition-colors text-sm"
>
{link.label}
</a>
</li>
))}
</ul>
</div>
</div>
<!-- Bottom -->
<div class="mt-12 pt-8 border-t border-border flex flex-col sm:flex-row justify-between items-center gap-4">
<p class="text-text-muted text-sm">
&copy; {currentYear} ManaDeck. Alle Rechte vorbehalten.
</p>
<p class="text-text-muted text-sm">
Made with 💜 in Germany
</p>
</div>
</div>
</footer>

View file

@ -0,0 +1,84 @@
---
const navLinks = [
{ href: '#features', label: 'Features' },
{ href: '#how-it-works', label: 'So funktioniert\'s' },
{ href: '#pricing', label: 'Preise' },
{ href: '#faq', label: 'FAQ' }
];
---
<nav class="fixed top-0 left-0 right-0 z-50 bg-background-page/80 backdrop-blur-lg border-b border-border">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<!-- Logo -->
<a href="/" class="flex items-center gap-2">
<span class="text-2xl">🃏</span>
<span class="font-bold text-xl text-text-primary">ManaDeck</span>
</a>
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center gap-8">
{navLinks.map(link => (
<a
href={link.href}
class="text-text-secondary hover:text-text-primary transition-colors text-sm font-medium"
>
{link.label}
</a>
))}
</div>
<!-- CTA Button -->
<div class="flex items-center gap-4">
<a
href="#download"
class="btn-primary text-sm px-4 py-2"
>
App herunterladen
</a>
<!-- Mobile Menu Button -->
<button
type="button"
class="md:hidden p-2 text-text-secondary hover:text-text-primary"
aria-label="Menu"
id="mobile-menu-button"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</div>
</div>
</div>
<!-- Mobile Menu -->
<div class="hidden md:hidden" id="mobile-menu">
<div class="px-4 py-4 space-y-2 bg-background-card border-t border-border">
{navLinks.map(link => (
<a
href={link.href}
class="block px-4 py-2 text-text-secondary hover:text-text-primary hover:bg-background-card-hover rounded-lg transition-colors"
>
{link.label}
</a>
))}
</div>
</div>
</nav>
<script>
const mobileMenuButton = document.getElementById('mobile-menu-button');
const mobileMenu = document.getElementById('mobile-menu');
mobileMenuButton?.addEventListener('click', () => {
mobileMenu?.classList.toggle('hidden');
});
// Close menu when clicking a link
mobileMenu?.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu?.classList.add('hidden');
});
});
</script>

View file

@ -0,0 +1 @@
/// <reference types="astro/client" />

View file

@ -0,0 +1,47 @@
---
import '../styles/global.css';
interface Props {
title: string;
description?: string;
}
const {
title,
description = 'ManaDeck - Dein KI-gestützter Lernpartner für Karteikarten und effektives Lernen'
} = Astro.props;
---
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<meta name="generator" content={Astro.generator} />
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:locale" content="de_DE" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<title>{title}</title>
</head>
<body class="min-h-screen bg-background-page text-text-primary antialiased">
<slot />
</body>
</html>

View file

@ -0,0 +1,287 @@
---
import Layout from '../layouts/Layout.astro';
import Navigation from '../components/Navigation.astro';
import Footer from '../components/Footer.astro';
// Shared components
import HeroSection from '@manacore/shared-landing-ui/sections/HeroSection.astro';
import FeatureSection from '@manacore/shared-landing-ui/sections/FeatureSection.astro';
import StepsSection from '@manacore/shared-landing-ui/sections/StepsSection.astro';
import FAQSection from '@manacore/shared-landing-ui/sections/FAQSection.astro';
import CTASection from '@manacore/shared-landing-ui/sections/CTASection.astro';
import PricingSection from '@manacore/shared-landing-ui/sections/PricingSection.astro';
import Card from '@manacore/shared-landing-ui/atoms/Card.astro';
// Feature data
const features = [
{
icon: '🧠',
title: 'KI-generierte Karteikarten',
description: 'Lade deine Notizen, PDFs oder Texte hoch und lass die KI automatisch perfekte Karteikarten erstellen.'
},
{
icon: '🔄',
title: 'Spaced Repetition',
description: 'Unser intelligenter Algorithmus zeigt dir Karten genau dann, wenn du sie wiederholen solltest.'
},
{
icon: '📊',
title: 'Lernstatistiken',
description: 'Verfolge deinen Fortschritt mit detaillierten Statistiken und optimiere dein Lernen.'
},
{
icon: '📱',
title: 'Überall lernen',
description: 'Synchronisiere deine Decks auf allen Geräten und lerne, wo immer du bist.'
},
{
icon: '🎯',
title: 'Personalisiertes Lernen',
description: 'Die KI passt sich deinem Lernstil an und erstellt maßgeschneiderte Wiederholungspläne.'
},
{
icon: '🤝',
title: 'Decks teilen',
description: 'Teile deine Karteikarten-Decks mit Freunden oder entdecke öffentliche Sammlungen.'
}
];
// Steps data
const steps = [
{
number: '1',
title: 'Inhalte hochladen',
description: 'Lade deine Notizen, PDFs oder Texte hoch. ManaDeck unterstützt verschiedene Formate.',
image: '/screenshots/upload.png'
},
{
number: '2',
title: 'KI erstellt Karten',
description: 'Unsere KI analysiert deine Inhalte und erstellt automatisch strukturierte Karteikarten.',
image: '/screenshots/generate.png'
},
{
number: '3',
title: 'Effizient lernen',
description: 'Lerne mit Spaced Repetition und behalte das Gelernte langfristig im Gedächtnis.',
image: '/screenshots/learn.png'
}
];
// Pricing data
const pricingPlans = [
{
name: 'Free',
price: '0',
period: '/Monat',
description: 'Perfekt zum Ausprobieren',
features: [
{ text: '50 Karteikarten', included: true },
{ text: '3 Decks', included: true },
{ text: 'Basis-KI-Generierung', included: true },
{ text: 'Spaced Repetition', included: true },
{ text: 'Unbegrenzte Karten', included: false },
{ text: 'Deck-Sharing', included: false }
],
cta: {
text: 'Kostenlos starten',
href: '#download'
}
},
{
name: 'Pro',
price: '9,99',
period: '/Monat',
description: 'Für ambitionierte Lerner',
features: [
{ text: 'Unbegrenzte Karteikarten', included: true },
{ text: 'Unbegrenzte Decks', included: true },
{ text: 'Erweiterte KI-Funktionen', included: true },
{ text: 'Spaced Repetition', included: true },
{ text: 'Deck-Sharing', included: true },
{ text: 'Prioritäts-Support', included: true }
],
cta: {
text: 'Pro werden',
href: '#download'
},
highlighted: true,
badge: 'Beliebt'
},
{
name: 'Team',
price: '24,99',
period: '/Monat',
description: 'Für Gruppen und Klassen',
features: [
{ text: 'Alles aus Pro', included: true },
{ text: 'Team-Verwaltung', included: true },
{ text: 'Gemeinsame Decks', included: true },
{ text: 'Fortschritts-Tracking', included: true },
{ text: 'Admin-Dashboard', included: true },
{ text: 'Dedizierter Support', included: true }
],
cta: {
text: 'Team starten',
href: '#download'
}
}
];
// FAQ data
const faqs = [
{
question: 'Wie funktioniert die KI-Karteikarten-Generierung?',
answer: 'ManaDeck verwendet fortschrittliche KI-Modelle, um deine Texte zu analysieren und die wichtigsten Konzepte zu extrahieren. Daraus werden automatisch Frage-Antwort-Paare erstellt, die du als Karteikarten lernen kannst.'
},
{
question: 'Was ist Spaced Repetition?',
answer: 'Spaced Repetition ist eine wissenschaftlich bewährte Lernmethode, bei der Karten in optimalen Zeitabständen wiederholt werden. Karten, die du gut kennst, siehst du seltener, während schwierige Karten häufiger erscheinen.'
},
{
question: 'Kann ich meine eigenen Karteikarten erstellen?',
answer: 'Ja! Du kannst sowohl KI-generierte Karten nutzen als auch komplett eigene Karteikarten erstellen. Beide Varianten lassen sich beliebig kombinieren und bearbeiten.'
},
{
question: 'Welche Dateiformate werden unterstützt?',
answer: 'ManaDeck unterstützt PDF, Word-Dokumente (.docx), Textdateien (.txt) und Markdown (.md). Du kannst auch direkt Text in die App einfügen.'
},
{
question: 'Sind meine Daten sicher?',
answer: 'Ja, alle Daten werden verschlüsselt übertragen und gespeichert. Wir verkaufen keine Nutzerdaten an Dritte und sind DSGVO-konform.'
},
{
question: 'Kann ich offline lernen?',
answer: 'Ja! Du kannst deine Decks für den Offline-Modus herunterladen und überall lernen, auch ohne Internetverbindung.'
}
];
---
<Layout title="ManaDeck - KI-gestützte Karteikarten für effektives Lernen">
<Navigation />
<main class="pt-16">
<HeroSection
title="Lerne smarter, nicht härter"
subtitle="ManaDeck verwandelt deine Notizen in intelligente Karteikarten. Mit KI-Generierung und Spaced Repetition lernst du effizienter als je zuvor."
variant="default"
primaryCta={{
text: 'Jetzt kostenlos starten',
href: '#download'
}}
secondaryCta={{
text: 'Features entdecken',
href: '#features',
variant: 'secondary'
}}
trustBadges={[
{ icon: '✓', text: 'Kostenlos testen' },
{ icon: '🔒', text: 'DSGVO-konform' },
{ icon: '📱', text: 'iOS & Android' }
]}
/>
<FeatureSection
id="features"
title="Alles was du zum Lernen brauchst"
subtitle="ManaDeck kombiniert KI-Technologie mit bewährten Lernmethoden für maximalen Lernerfolg."
features={features}
columns={3}
variant="cards"
class="bg-[var(--color-background-card)]"
>
<!-- Mana System Highlight -->
<div class="mt-12 md:mt-16 px-4" slot="highlight">
<Card variant="glow" class="max-w-4xl mx-auto" padding="lg">
<div class="flex flex-col md:flex-row items-center gap-6 md:gap-8">
<div class="text-5xl sm:text-6xl">⚡</div>
<div class="flex-1 text-center md:text-left">
<h3 class="font-bold text-xl sm:text-2xl text-[var(--color-text-primary)] mb-2 sm:mb-3">
Mana-System
</h3>
<p class="text-[var(--color-text-secondary)] text-sm sm:text-base leading-relaxed">
Nutze Mana-Punkte für KI-Generierungen. Starte kostenlos mit 100 Mana
und verdiene mehr durch regelmäßiges Lernen.
</p>
</div>
<div class="flex items-center gap-2">
<div class="w-10 h-10 sm:w-12 sm:h-12 bg-[var(--color-primary)] rounded-full flex items-center justify-center">
<span class="text-white font-bold text-sm sm:text-base">M</span>
</div>
<div class="text-left">
<div class="text-[var(--color-text-primary)] font-semibold text-sm sm:text-base">100 Mana</div>
<div class="text-[var(--color-text-secondary)] text-xs sm:text-sm">zum Start</div>
</div>
</div>
</div>
</Card>
</div>
</FeatureSection>
<StepsSection
id="how-it-works"
title="So einfach geht's"
subtitle="In nur drei Schritten zum effektiven Lernen"
steps={steps}
showImages={false}
alternateLayout={true}
/>
<PricingSection
id="pricing"
title="Wähle deinen Plan"
subtitle="Starte kostenlos und upgrade, wenn du bereit bist"
plans={pricingPlans}
class="bg-[var(--color-background-card)]"
/>
<FAQSection
id="faq"
title="Häufig gestellte Fragen"
subtitle="Alles was du über ManaDeck wissen musst"
faqs={faqs}
/>
<CTASection
id="download"
title="Bereit, smarter zu lernen?"
subtitle="Lade ManaDeck jetzt herunter und erstelle deine ersten KI-generierten Karteikarten. Kostenlos und ohne Kreditkarte."
primaryCta={{ text: 'App herunterladen', href: '#' }}
variant="highlighted"
>
<!-- App Store Buttons -->
<div class="flex flex-wrap items-center justify-center gap-4 mt-8">
<a href="#" class="inline-block hover:opacity-80 transition-opacity">
<img src="/app-store-badge.svg" alt="Download im App Store" class="h-12" />
</a>
<a href="#" class="inline-block hover:opacity-80 transition-opacity">
<img src="/google-play-badge.svg" alt="Jetzt bei Google Play" class="h-12" />
</a>
</div>
<!-- Trust Indicators -->
<div class="flex flex-wrap items-center justify-center gap-4 sm:gap-6 mt-8">
<div class="flex items-center gap-2">
<svg class="w-5 h-5 text-[var(--color-primary)]" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
<span class="text-[var(--color-text-secondary)] text-sm">100% Kostenlos starten</span>
</div>
<div class="flex items-center gap-2">
<svg class="w-5 h-5 text-[var(--color-primary)]" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"></path>
</svg>
<span class="text-[var(--color-text-secondary)] text-sm">DSGVO-konform</span>
</div>
<div class="flex items-center gap-2">
<svg class="w-5 h-5 text-[var(--color-primary)]" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2a8 8 0 100 16 8 8 0 000-16zm1 11H9v-2h2v2zm0-4H9V5h2v4z"></path>
</svg>
<span class="text-[var(--color-text-secondary)] text-sm">Keine Kreditkarte nötig</span>
</div>
</div>
</CTASection>
</main>
<Footer />
</Layout>

View file

@ -0,0 +1,103 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* ManaDeck Theme CSS Variables */
:root {
/* Primary colors - ManaDeck Purple */
--color-primary: #7C3AED;
--color-primary-hover: #8B5CF6;
--color-primary-glow: rgba(124, 58, 237, 0.3);
/* Text colors */
--color-text-primary: #f9fafb;
--color-text-secondary: #d1d5db;
--color-text-muted: #6b7280;
/* Background colors */
--color-background-page: #0f0a1a;
--color-background-card: #1a1625;
--color-background-card-hover: #2d2640;
/* Border colors */
--color-border: #3d3555;
--color-border-hover: #4d4570;
}
/* Base styles */
html {
scroll-behavior: smooth;
}
body {
font-family: 'Inter', system-ui, sans-serif;
background-color: var(--color-background-page);
color: var(--color-text-primary);
line-height: 1.6;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-background-card);
}
::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-border-hover);
}
/* Selection */
::selection {
background-color: var(--color-primary);
color: white;
}
/* Focus styles */
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Gradient text */
.text-gradient {
background: linear-gradient(135deg, #7C3AED 0%, #A78BFA 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Animation utilities */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fadeIn {
animation: fadeIn 0.6s ease-out forwards;
}
/* Button styles */
.btn-primary {
@apply inline-flex items-center justify-center px-6 py-3 bg-primary text-white font-semibold rounded-lg transition-all duration-200;
@apply hover:bg-primary-hover hover:shadow-lg hover:shadow-primary-glow;
}
.btn-secondary {
@apply inline-flex items-center justify-center px-6 py-3 border border-border text-text-primary font-semibold rounded-lg transition-all duration-200;
@apply hover:border-border-hover hover:bg-background-card;
}

View file

@ -0,0 +1,39 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}',
'../../packages/shared-landing-ui/src/**/*.{astro,html,js,jsx,ts,tsx}'
],
theme: {
extend: {
colors: {
// ManaDeck Purple Theme
primary: {
DEFAULT: '#7C3AED',
hover: '#8B5CF6',
glow: 'rgba(124, 58, 237, 0.3)'
},
background: {
page: '#0f0a1a',
card: '#1a1625',
'card-hover': '#2d2640'
},
text: {
primary: '#f9fafb',
secondary: '#d1d5db',
muted: '#6b7280'
},
border: {
DEFAULT: '#3d3555',
hover: '#4d4570'
}
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif']
}
}
},
plugins: [
require('@tailwindcss/typography')
]
};

View file

@ -0,0 +1,9 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}

View file

@ -0,0 +1,7 @@
mobile .env.production
# Supabase Configuration
EXPO_PUBLIC_SUPABASE_URL=https://vksoodohrbjwyloitvsz.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=sb_publishable_2ndX-kBHFpbDlL_ZeeOnfQ_ZlLI8ONk
# Backend API Configuration - Production
EXPO_PUBLIC_API_URL=https://manadeck-backend-111768794939.europe-west3.run.app

6
apps/manadeck/apps/mobile/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
# The following patterns were generated by expo-cli
expo-env.d.ts
# @end expo-cli

View file

@ -0,0 +1,11 @@
{
"mcpServers": {
"supabase": {
"command": "npx",
"args": ["-y", "@supabase/mcp-server-supabase@latest", "--project-ref=vksoodohrbjwyloitvsz"],
"env": {
"SUPABASE_ACCESS_TOKEN": "sbp_3622a96f728711cd06b113c17f77f84d02ff8fb2"
}
}
}
}

View file

@ -0,0 +1,118 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Manadeck is a React Native/Expo application using Expo Router for navigation, TypeScript for type safety, NativeWind (Tailwind CSS) for styling, and Supabase for backend services. The app uses Zustand for state management and is configured for cross-platform development (iOS, Android, and Web).
## Essential Commands
### Development
```bash
npm start # Start Expo dev server with dev client
npm run ios # Run on iOS simulator
npm run android # Run on Android emulator
npm run web # Run in web browser
```
### Build & Deploy
```bash
npm run build:dev # Build development build with EAS
npm run build:preview # Build preview build with EAS
npm run build:prod # Build production build with EAS
npm run prebuild # Generate native projects
```
### Code Quality
```bash
npm run lint # Run ESLint and Prettier checks
npm run format # Auto-fix ESLint and format with Prettier
```
## Architecture & Key Patterns
### File Structure
- **app/**: Expo Router pages with file-based routing
- `(tabs)/`: Tab navigation screens
- `_layout.tsx`: Root layout with navigation stack
- `modal.tsx`: Modal screens
- **components/**: Reusable UI components
- **store/**: Zustand state management stores
- **utils/**: Utility functions (e.g., Supabase client)
- **assets/**: Static assets (images, icons)
### Navigation Pattern
Uses Expo Router v5 with file-based routing:
- Tab navigation defined in `app/(tabs)/_layout.tsx`
- Stack navigation in `app/_layout.tsx`
- Typed routes enabled via `experiments.typedRoutes` in app.json
### Styling Approach
- NativeWind (Tailwind CSS for React Native) configured via `tailwind.config.js`
- Global styles imported in `app/_layout.tsx` via `global.css`
- Components use className prop for Tailwind classes
### State Management
- Zustand store pattern in `store/store.ts`
- Create typed stores with actions and selectors
- Access via hooks (e.g., `useStore`)
### Backend Integration
- Supabase client configured in `utils/supabase.ts`
- Uses AsyncStorage for session persistence
- Environment variables: `EXPO_PUBLIC_SUPABASE_URL` and `EXPO_PUBLIC_SUPABASE_ANON_KEY`
### TypeScript Configuration
- Strict mode enabled
- Path alias `~/*` configured for absolute imports
- Type definitions for Expo and React Native included
## Development Guidelines
### Component Creation
When creating new components:
1. Place in `components/` directory
2. Use TypeScript with proper prop typing
3. Apply NativeWind classes via className prop
4. Follow existing naming conventions (PascalCase for components)
### Screen/Page Creation
For new screens:
1. Add file in appropriate directory under `app/`
2. Export default React component
3. Configure navigation options if needed
4. Import and use existing UI components
### Supabase Integration
When working with Supabase:
1. Import client from `utils/supabase.ts`
2. Handle authentication state with AsyncStorage
3. Use environment variables for configuration
4. Follow RLS (Row Level Security) best practices
### Code Style
- ESLint configured with Expo preset
- Prettier for formatting with Tailwind plugin
- React display-name rule disabled
- Format before committing: `npm run format`
## Environment Setup
### Required Environment Variables
Create a `.env` or `.env.local` file:
```
EXPO_PUBLIC_SUPABASE_URL=your_supabase_url
EXPO_PUBLIC_SUPABASE_ANON_KEY=your_anon_key
```
### EAS Configuration
Project configured with EAS Build:
- Project ID: `6cb9cf81-a4d5-4c72-b57d-1be3da8eba35`
- Three build profiles: development, preview, production
- Development builds include dev client
### Platform-Specific Setup
- iOS: Bundle ID `com.tilljs.manadeck`
- Android: Package `com.tilljs.manadeck`
- Uses Expo development build for custom native code

View file

@ -0,0 +1,483 @@
> super danke lass uns jezt die ai features
> planen. es sollen diese dinge möglich werden,
> mit der open ai api: neue karten erstellen mit
1. spracheingabe (STT) 2. testeingabe 3.
bildupload - ausserdem sollen die karten per
STT vorgelesen werden können. schreibe dazu
einen plan für die implementierung
⏺ Update Todos
⎿ ☐ AI Features Planning: Spracherkennung
(STT) für Kartenerstellung
☐ AI Features Planning: Texteingabe mit
OpenAI für Kartenerstellung
☐ AI Features Planning: Bildupload mit
Vision API für Kartenerstellung
☐ AI Features Planning: Text-to-Speech
(TTS) für Kartenvorlesung
☐ AI Features Planning: Technische
Architektur und API Integration
⏺ 🤖 AI Features Implementation Plan - Manadeck
Übersicht der AI-Features
🎯 Ziel-Features:
1. Kartenerstellung via Spracheingabe (STT →
OpenAI → Karteninhalt)
2. Kartenerstellung via Texteingabe (Text →
OpenAI → strukturierte Karten)
3. Kartenerstellung via Bildupload (Bild →
Vision API → Karteninhalt)
4. Kartenvorlesung via TTS (Kartentext →
Speech → Audio)
---
📋 Phase 7: AI Integration Roadmap
Phase 7.1: Foundation & Setup (1 Woche)
7.1.1 Dependencies & Environment Setup
// Neue Dependencies
"@react-native-async-storage/async-storage":
"^1.21.0",
"expo-speech": "~12.1.0", // TTS
"expo-av": "~14.0.0", // Audio
recording/playback
"react-native-audio-recorder-player":
"^3.6.6", // Advanced audio
"openai": "^4.24.0", // OpenAI
API
"expo-image-picker": "~15.0.0", //
Bildauswahl
"expo-file-system": "~17.0.0" //
Dateisystem
7.1.2 Environment Variables
EXPO_PUBLIC_OPENAI_API_KEY=sk-...
EXPO_PUBLIC_OPENAI_MODEL=gpt-4o
EXPO_PUBLIC_OPENAI_VISION_MODEL=gpt-4o-vision-
preview
7.1.3 API Service Layer
// utils/openaiService.ts
export class OpenAIService {
static async generateCardsFromText(prompt:
string): Promise<CardContent[]>
static async
generateCardsFromSpeech(audioData: string):
Promise<CardContent[]>
static async
generateCardsFromImage(imageBase64: string):
Promise<CardContent[]>
static async enhanceCardContent(content:
string): Promise<string>
}
---
Phase 7.2: Speech-to-Text Integration (1-2
Wochen)
7.2.1 Audio Recording Component
// components/ai/AudioRecorder.tsx
interface AudioRecorderProps {
onRecordingComplete: (audioUri: string) =>
void;
onTranscriptionComplete: (text: string) =>
void;
}
export const AudioRecorder:
React.FC<AudioRecorderProps> = ({
onRecordingComplete,
onTranscriptionComplete
}) => {
// Aufnahme-Button mit
Wellenform-Visualisierung
// Live-Transkription während Aufnahme
// Aufnahme-Controls (Pause, Stopp,
Wiedergabe)
}
7.2.2 Speech-to-Text Service
// utils/speechService.ts
export class SpeechService {
static async transcribeAudio(audioUri:
string): Promise<{
text: string;
confidence: number;
language: string;
}>
static async transcribeRealtime(audioStream:
any): Promise<void>
}
7.2.3 Integration in Card Creation
- Neuer "Voice Input" Modus in Card Creation
Screen
- Real-time Transkription mit
Korrekturmöglichkeit
- Automatische Strukturierung durch OpenAI
---
Phase 7.3: Text-to-Cards AI Processing (1
Woche)
7.3.1 AI Card Generator Service
// utils/cardGenerator.ts
export class CardGenerator {
static async generateFromText(input: string,
options: {
cardTypes: ('flashcard' | 'quiz' |
'text')[];
difficulty: 'easy' | 'medium' | 'hard';
count: number;
language: 'de' | 'en';
}): Promise<GeneratedCard[]>
static async generateFromTopics(topics:
string[]): Promise<GeneratedCard[]>
static async enhanceExistingCard(card:
Card): Promise<Card>
}
interface GeneratedCard {
type: CardType;
content: CardContent;
metadata: {
confidence: number;
source: string;
tags: string[];
};
}
7.3.2 Smart Card Creation UI
// components/ai/SmartCardCreator.tsx
export const SmartCardCreator: React.FC = ()
=> {
// Multi-Input Interface:
// - Text Area für freie Eingabe
// - Topic Tags für strukturierte Eingabe
// - AI-Einstellungen (Schwierigkeit,
Kartentypen)
// - Vorschau der generierten Karten
// - Bulk-Edit Funktionen
}
---
Phase 7.4: Vision API Integration (1-2 Wochen)
7.4.1 Image Processing Service
// utils/visionService.ts
export class VisionService {
static async analyzeImage(imageUri: string):
Promise<{
text: string; // OCR-Text
objects: string[]; // Erkannte
Objekte
concepts: string[]; // Konzepte/Themen
language: string; // Erkannte
Sprache
}>
static async generateCardsFromImage(
imageUri: string,
context?: string
): Promise<GeneratedCard[]>
}
7.4.2 Image Upload Component
// components/ai/ImageCardCreator.tsx
export const ImageCardCreator: React.FC = ()
=> {
// Kamera/Galerie Integration
// Image Preview mit Crop-Funktionalität
// OCR-Text-Extraktion mit Editing
// Context-Input für bessere AI-Generation
// Generated Cards Preview
}
7.4.3 Use Cases
- Lehrbücher fotografieren → Automatische
Flashcard-Generierung
- Whiteboards/Notizen → Strukturierte Karten
- Diagramme/Grafiken → Erklärende Text-Karten
- Handschrift-Erkennung → Digitale Karten
---
Phase 7.5: Text-to-Speech Integration (1
Woche)
7.5.1 TTS Service
// utils/ttsService.ts
export class TTSService {
static async speakText(text: string,
options: {
language: string;
rate: number;
pitch: number;
voice?: string;
}): Promise<void>
static async generateAudioFile(text:
string): Promise<string> // Return audio URI
static getAvailableVoices():
Promise<Voice[]>
}
7.5.2 Audio Playback in Cards
// components/study/AudioCard.tsx
export const AudioCard: React.FC<{ card: Card
}> = ({ card }) => {
// Play/Pause Button für Karteninhalt
// Geschwindigkeitsregelung
// Auto-Play Optionen
// Audio-Caching für Offline-Nutzung
}
7.5.3 Study Session Audio Features
- Automatische Vorlesung während Study
Sessions
- Hände-frei Lernen für Unterwegs
- Aussprache-Training für Fremdsprachen
- Audio-Speed-Control für verschiedene
Lerngeschwindigkeiten
---
Phase 7.6: AI-Enhanced Study Experience (1-2
Wochen)
7.6.1 Smart Study Recommendations
// store/aiStore.ts
export const useAIStore =
create<AIState>((set, get) => ({
recommendations: [],
generateStudyPlan: async (userId: string) =>
{
// AI-basierte Lernplan-Generierung
// Berücksichtigt Fortschritt, Schwächen,
Präferenzen
},
suggestCardImprovements: async (cardId:
string) => {
// Vorschläge zur Kartenverbesserung
// Basierend auf Lernperformance
},
generateRelatedCards: async (card: Card) =>
{
// Ähnliche/verwandte Karten vorschlagen
}
}));
7.6.2 Intelligent Content Enhancement
- Automatische Hinweise zu schwierigen Karten
- Kontext-Erweiterungen für besseres
Verständnis
- Adaptive Schwierigkeit basierend auf
Performance
- Personalisierte Erklärungen je nach Lernstil
---
🏗️ Technische Architektur
Backend Extensions
7.A.1 Neue Datenbank-Tabellen
-- AI-generierte Karten tracking
CREATE TABLE ai_generated_cards (
id UUID PRIMARY KEY DEFAULT
gen_random_uuid(),
card_id UUID REFERENCES cards(id),
generation_method TEXT, -- 'speech', 'text',
'image'
source_data JSONB, -- Original input
data
confidence_score DECIMAL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Audio-Dateien für TTS
CREATE TABLE card_audio_files (
id UUID PRIMARY KEY DEFAULT
gen_random_uuid(),
card_id UUID REFERENCES cards(id),
audio_url TEXT,
language TEXT,
voice_settings JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
-- AI-Verbesserungsvorschläge
CREATE TABLE ai_suggestions (
id UUID PRIMARY KEY DEFAULT
gen_random_uuid(),
card_id UUID REFERENCES cards(id),
suggestion_type TEXT,
suggestion_data JSONB,
applied BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
);
7.A.2 Supabase Functions
-- Edge Function für OpenAI Integration
CREATE OR REPLACE FUNCTION
generate_cards_from_content(
content TEXT,
generation_type TEXT,
user_id UUID
) RETURNS JSON;
-- Edge Function für Audio-Processing
CREATE OR REPLACE FUNCTION
process_audio_upload(
audio_data BYTEA,
user_id UUID
) RETURNS JSON;
Security & Performance
7.B.1 API Rate Limiting
// utils/rateLimiter.ts
export class RateLimiter {
static checkAPILimit(userId: string,
endpoint: string): Promise<boolean>
static trackUsage(userId: string, endpoint:
string, cost: number): Promise<void>
}
7.B.2 Caching Strategy
- Audio Files: Supabase Storage mit CDN
- AI Responses: Redis/Memory Cache
- Image Processing: Optimierte Komprimierung
- Offline Support: SQLite für generierte
Inhalte
---
💰 Kostenoptimierung
7.C.1 Token/API Management
// utils/costOptimizer.ts
export class CostOptimizer {
static estimateTokens(text: string): number
static compressPrompt(prompt: string):
string
static batchRequests(requests: any[]):
Promise<any[]>
static cacheCommonResponses(key: string,
response: any): void
}
7.C.2 Smart Usage Strategies
- Prompt Engineering für effiziente
Token-Nutzung
- Batch Processing für multiple Karten
- Caching häufiger Anfragen
- Fallback Models für einfache Aufgaben
- User Limits für API-Kosten-Kontrolle
---
🎮 User Experience Features
7.D.1 AI Assistant Interface
// components/ai/AIAssistant.tsx
export const AIAssistant: React.FC = () => {
// Chat-Interface für AI-Interaktion
// Voice Commands
// Smart Suggestions Overlay
// Progress-aware Recommendations
}
7.D.2 Personalization Engine
- Lernstil-Erkennung basierend auf
Nutzungsmustern
- Adaptive Kartenformate je nach Präferenz
- Personalisierte Schwierigkeit für
AI-generierte Inhalte
- Custom Prompts für wiederkehrende
Kartenerstellung
---
📈 Success Metrics & Analytics
KPIs für AI Features:
- Card Generation Success Rate (Qualität der
generierten Karten)
- User Adoption Rate (% der Nutzer die
AI-Features verwenden)
- Time Savings (Vergleich manuelle vs.
AI-Kartenerstellung)
- Learning Effectiveness (Performance bei AI
vs. manuellen Karten)
- API Cost per User (Kostenoptimierung)
---
🚀 Implementation Timeline
| Phase | Duration | Key Deliverables
|
|-------|----------|--------------------------
---------|
| 7.1 | 1 Woche | Setup, Dependencies, API
Services |
| 7.2 | 2 Wochen | Speech-to-Text
Integration |
| 7.3 | 1 Woche | Text-to-Cards AI
Processing |
| 7.4 | 2 Wochen | Vision API Integration
|
| 7.5 | 1 Woche | Text-to-Speech
Integration |
| 7.6 | 2 Wochen | Enhanced Study Experience
|
Total: ~9 Wochen für vollständige
AI-Integration

View file

@ -0,0 +1,456 @@
Native tabs
Copy
Learn how to use the native tabs layout in Expo Router.
Native tabs is an experimental feature available in SDK 54 and above, and its API is subject to change
Tabs are a common way to navigate between different sections of an app. In Expo Router, you can use different tab layouts, depending on your needs. This guide covers the native tabs. Unlike the other tabs layout, native tabs use the native system tab bar.
For other tab-layouts see:
Custom tabs
See custom tabs if your app requires a fully custom design that is not possible using system tabs.
JavaScript tabs
See JavaScript tabs if you already use React Navigation's tabs.
Get started
You can use file-based routing to create a tabs layout. Here's an example file structure:
app
\_layout.tsx
index.tsx
settings.tsx
The above file structure produces a layout with a tab bar at the bottom of the screen. The tab bar will have two tabs: Home and Settings.
A screenshot of a tab bar with two tabs: Home and Settings.
You can use the app/\_layout.tsx file to define your app's root layout using tabs. This file is the main layout file for the tab bar and each tab. Inside it, you can control how the tab bar and each tab item look and behave.
app/\_layout.tsx
Copy
import { NativeTabs, Icon, Label } from 'expo-router/unstable-native-tabs';
export default function TabLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name="index">
<Label>Home</Label>
<Icon sf="house.fill" drawable="custom_android_drawable" />
</NativeTabs.Trigger>
<NativeTabs.Trigger name="settings">
<Icon sf="gear" drawable="custom_settings_drawable" />
<Label>Settings</Label>
</NativeTabs.Trigger>
</NativeTabs>
);
}
Finally, you have the two tab files that make up the content of the tabs: app/index.tsx and app/settings.tsx.
app/index.tsx and app/settings.tsx
Copy
import { View, Text, StyleSheet } from 'react-native';
export default function Tab() {
return (
<View style={styles.container}>
<Text>Tab [Home|Settings]</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
The tab file named index.tsx is the default tab when the app loads. The second tab file settings.tsx shows how you can add more tabs to the tab bar.
In contrast to the Stack navigator, tabs are not automatically added to the tab bar. You need to explicitly add them in your layout file using the NativeTabs.Trigger.
Customizing tab bar items
Native tabs is an experimental feature available in SDK 54 and above, and its API is subject to change.
When you want to customize the tab bar item, we recommend using the components API designed for this purpose. Currently, you can customize:
Icon: The icon displayed in the tab bar item.
Label: The label displayed in the tab bar item.
Badge: The badge displayed in the tab bar item.
Icon
Native tabs is an experimental feature available in SDK 54 and above, and its API is subject to change.
You can use the Icon component to customize the icon displayed in the tab bar item. The Icon component accepts a drawable prop for Android drawables, a sf prop for Apple's SF Symbols icons, or a src prop for custom images.
Alternatively, you can pass {default: ..., selected: ...} to either the sf or src prop to specify different icons for the default and selected states.
To use drawable props on Android, you can use built-in drawables or add custom drawables.
app/\_layout.tsx
Copy
import { NativeTabs, Icon } from 'expo-router/unstable-native-tabs';
export default function TabLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name="index">
<Icon sf={{ default: "house", selected: "house.fill" }} drawable="custom_home_drawable" />
</NativeTabs.Trigger>
<NativeTabs.Trigger name="settings">
<Icon
src={require('../../../assets/setting_icon.png')}
/>
</NativeTabs.Trigger>
</NativeTabs>
Liquid glass on iOS automatically changes colors based on if the background color is light or dark. There is no callback for this, so you need to use a PlatformColor to set the color of the icon.
app/\_layout.tsx
Copy
import { DynamicColorIOS } from 'react-native';
import { NativeTabs, Icon } from 'expo-router/unstable-native-tabs';
export default function TabLayout() {
return (
<NativeTabs
labelStyle={{
// For the text color
color: DynamicColorIOS({
dark: 'white',
light: 'black',
}),
// For the selected icon color
tintColor: DynamicColorIOS({
dark: 'white',
light: 'black',
}),
}}>
<NativeTabs.Trigger name="index">
<Icon sf={{ default: 'house', selected: 'house.fill' }} drawable="custom_home_drawable" />
</NativeTabs.Trigger>
<NativeTabs.Trigger name="settings">
<Icon
src={require('../../../assets/setting_icon.png')}
selectedSrc={require('../../../assets/selected_setting_icon.png')}
/>
</NativeTabs.Trigger>
</NativeTabs>
);
}
Show More
Label
Native tabs is an experimental feature available in SDK 54 and above, and its API is subject to change.
You can use the Label component to customize the label displayed in the tab bar item. The Label component accepts a string label passed as a child. If no label is provided, the tab bar item will use the route name as the label.
If you don't want to display a label, you can use the hidden prop to hide the label.
app/\_layout.tsx
Copy
import { NativeTabs, Label } from 'expo-router/unstable-native-tabs';
export default function TabLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name="index">
<Label>Home</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="settings">
<Label hidden />
</NativeTabs.Trigger>
</NativeTabs>
);
}
Badge
Native tabs is an experimental feature available in SDK 54 and above, and its API is subject to change.
You can use the Badge component to customize the badge displayed for the tab bar item. The badge is an additional mark on top of the tab and useful for showing notification or unread message counts.
app/\_layout.tsx
Copy
import { NativeTabs, Badge } from 'expo-router/unstable-native-tabs';
export default function TabLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name="messages">
<Badge>9+</Badge>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="settings">
<Badge />
</NativeTabs.Trigger>
</NativeTabs>
);
}
A screenshot of a tab bar with badges on the Messages and Settings tabs.
Customizing the tab bar
Since the native tab layout's appearance varies by platform, the customization options are also different. For all customization options, see the API reference for NativeTabs.
Advanced
Hiding a tab conditionally
If you want to hide a tab based on a condition, you can either remove the trigger or pass the hidden prop to the NativeTabs.Trigger component.
app/\_layout.tsx
Copy
import { NativeTabs } from 'expo-router/unstable-native-tabs';
export default function TabLayout() {
const shouldHideMessagesTab = true; // Replace with your condition
return (
<NativeTabs>
<NativeTabs.Trigger name="messages" hidden={shouldHideMessagesTab} />
</NativeTabs>
);
}
Dismiss behavior
Currently this is an iOS-only feature, but we plan to add it to Android in the future.
By default, tapping a tab that is already active closes all screens in that tab's stack and returns to the root screen. You can disable this by setting the disablePopToTop prop on the NativeTabs.Trigger component.
app/\_layout.tsx
Copy
import { NativeTabs, Label } from 'expo-router/unstable-native-tabs';
export default function TabLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name="index" disablePopToTop>
<Label>Home</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="settings">
<Label>Settings</Label>
</NativeTabs.Trigger>
</NativeTabs>
);
}
Scroll to top
Currently this is an iOS-only feature, but we plan to add it to Android in the future.
By default, tapping a tab that is already active and showing its root screen scrolls the content back to the top. You can disable this by setting the disableScrollToTop prop on the NativeTabs.Trigger component.
app/\_layout.tsx
Copy
import { NativeTabs, Label } from 'expo-router/unstable-native-tabs';
export default function TabLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name="index" disableScrollToTop>
<Label>Home</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="settings">
<Label>Settings</Label>
</NativeTabs.Trigger>
</NativeTabs>
);
}
iOS 26 features
To use features described in this section, compile your app with Xcode 26 or higher.
Separate search tab
A screenshot of a tab bar with separate search tab.
To add a separate search tab, assign the role with its value set to search to the native tab you want to display separately.
app/\_layout.tsx
Copy
import { NativeTabs, Label } from 'expo-router/unstable-native-tabs';
export default function TabLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name="index">
<Label>Home</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="search" role="search">
<Label>Search</Label>
</NativeTabs.Trigger>
</NativeTabs>
);
}
Tabbar search input
A screenshot of a tab bar search.
To add a search field to the tab bar, wrap the screen in a Stack navigator and configure headerSearchBarOptions.
app
\_layout.tsx
index.tsx
search
\_layout.tsx
index.tsx
app/\_layout.tsx
Copy
import { NativeTabs, Label } from 'expo-router/unstable-native-tabs';
export default function TabLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name="index">
<Label>Home</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="search" role="search">
<Label>Search</Label>
</NativeTabs.Trigger>
</NativeTabs>
);
}
app/search/\_layout.tsx
Copy
import { Stack } from 'expo-router';
export default function SearchLayout() {
return (
<Stack>
<Stack.Screen
name="index"
options={{
title: 'Search',
headerSearchBarOptions: {
placement: 'automatic',
placeholder: 'Search',
onChangeText: () => {},
},
}}
/>
</Stack>
);
}
Show More
app/search/index.tsx
Copy
import { ScrollView } from 'react-native';
export default function SearchIndex() {
return <ScrollView>{/_ Screen content _/}</ScrollView>;
}
Tab bar minimize behavior
To implement the minimized behavior on the tab bar, you can use minimizeBehavior prop on NativeTabs. In the example below, the tab bar is minimized when scrolling down.
app/\_layout.tsx
Copy
import { NativeTabs, Label } from 'expo-router/unstable-native-tabs';
export default function TabLayout() {
return (
<NativeTabs minimizeBehavior="onScrollDown">
<NativeTabs.Trigger name="index">
<Label>Home</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="tab-1">
<Label>Tab 1</Label>
</NativeTabs.Trigger>
</NativeTabs>
);
}
Integration with @expo/vector-icons
Recommended: Use SF Symbols on iOS. They offer higher quality and a native platform feeling than vector icons.
To use icons from @expo/vector-icons, you can use VectorIcon component.
app/\_layout.tsx
Copy
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { NativeTabs, Icon, VectorIcon } from 'expo-router/unstable-native-tabs';
import { Platform } from 'react-native';
export default function TabLayout() {
return (
<NativeTabs minimizeBehavior="onScrollDown">
<NativeTabs.Trigger name="index">
<Label>Home</Label>
{Platform.select({
ios: <Icon sf="house.fill" />,
android: <Icon src={<VectorIcon family={MIcons} name="home" />} />,
})}
</NativeTabs.Trigger>
</NativeTabs>
);
}
Migrating from JavaScript tabs
Native tabs are not designed to be a drop-in replacement for JavaScript tabs. The native tabs are constrained to the native platform behavior, whereas the JavaScript tabs can be customized more freely. If you aren't interested in the native platform behavior, you can continue using the JavaScript tabs.
Use Trigger instead of Screen
NativeTabs introduces the concept of a Trigger for adding routes to a layout. Unlike a Screen, which styles routes that are added automatically, the Trigger system gives you better control for hiding and removing tabs from the tab bar.
Use React components instead of props
NativeTabs has a React-first API that opts to use components for defining UI in favor of props objects.
- options={{
- tabBarIcon: ({ focused, color, size }) => (
- <Icon name="home" color={color} size={size} />
- ),
- }}
* <Icon sf="house" drawable="home_drawable" />
Use Stacks inside tabs
The JavaScript <Tabs /> have a mock stack header which is not present in the native tabs. Instead, you should nest a native <Stack /> layout inside the native tabs to support both headers and pushing screens.
Known limitations
A limit of 5 tabs on Android
On Android, there is a limitation of having a maximum of 5 tabs in the tab bar. This restriction comes from the platform's Material Tabs component.
Android does not support custom images
This is in active development. At the moment, you can only use build-time drawables on Android.
Cannot measure the tab bar height
The tabs move around, sometimes being on top of the screen when rendering on iPad, sometimes on the side of the screen when running on Apple Vision Pro, and so on. We're working on a layout function to provide more detailed layout info in the future.
No support for nested native tabs
Native tabs cannot be nested inside other native tabs. You can still nest JavaScript tabs inside native tabs.
Limited support for FlatList
FlatList integration with native tabs has limitations. Features like scroll-to-top and minimize-on-scroll aren't supported. Additionally, detecting scroll edges may fail, causing the tab bar to appear transparent. To fix this, use the disableTransparentOnScrollEdge prop.
app/\_layout.tsx
Copy
import { NativeTabs, Label } from 'expo-router/unstable-native-tabs';
export default function TabLayout() {
return (
<NativeTabs disableTransparentOnScrollEdge>
<NativeTabs.Trigger name="index">
<Label>Home</Label>
</NativeTabs.Trigger>
</NativeTabs>
);
}

View file

@ -0,0 +1,986 @@
Building SwiftUI apps with Expo UI
Copy
Learn how to use Expo UI to integrate SwiftUI into your Expo apps.
Available in SDK 54 and above.
Expo UI brings SwiftUI to React Native. You can use modern SwiftUI primitives to build your apps.
This guide covers the basics of using Expo UI to integrate SwiftUI into your Expo apps.
Features
SwiftUI primitives: Expo UI is not another UI library. It brings SwiftUI primitives to Expo.
1-to-1 mapping: The components in Expo UI have a 1-to-1 mapping to SwiftUI views. You can easily explore available views in the SwiftUI ecosystem, such as Explore SwiftUI or the Libraried app, and find the corresponding Expo UI component.
Full-app support: Expo UI is designed to be used throughout the entire app. You can write your app entirely in Expo UI, while maintaining flexibility at the same time. The integration works at the component level. You can also mix React Native components, Expo UI components, DOM components, or custom 2D components using react-native-skia.
Installation
You'll need to install the @expo/ui package in your Expo project. Run the following command to install it:
Terminal
Copy
npx expo install @expo/ui
Usage
Expo UI has several SwiftUI components available. You can use them in your app by importing them from @expo/ui/swift-ui. However, to cross the boundary from React Native (UIKit) to SwiftUI, you need to use the <Host> component. The <Host> is the container for SwiftUI views. You can think of it like <svg> in the DOM or <Canvas> in react-native-skia. Under the hood, it uses UIHostingController to render SwiftUI views in UIKit.
Basic usage with Host
Code
Preview
SwiftUI loading view
Copy
import { CircularProgress, Host } from '@expo/ui/swift-ui';
import { View, Text } from 'react-native';
export default function LoadingView() {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Host matchContents>
<CircularProgress />
</Host>
<Text>Loading...</Text>
</View>
);
}
Using HStack and VStack
You can also use the HStack and VStack components to build the entire layout in SwiftUI.
Code
Preview
SwiftUI loading with HStack and VStack
Copy
import { CircularProgress, Host, HStack, LinearProgress, VStack } from '@expo/ui/swift-ui';
export default function LoadingView() {
return (
<Host style={{ flex: 1, margin: 32 }}>
<VStack spacing={32}>
<HStack spacing={32}>
<CircularProgress />
<CircularProgress color="orange" />
</HStack>
<LinearProgress progress={0.5} />
<LinearProgress color="orange" progress={0.7} />
</VStack>
</Host>
);
}
Modifiers
SwiftUI modifier is a powerful way to customize the appearance and behavior of SwiftUI components. Expo UI also provides modifiers for SwiftUI components. You can import modifiers from @expo/ui/swift-ui/modifiers and pass them as an array to the modifiers prop. In the following example, the expo-mesh-gradient and glassEffect modifier are combined to create Liquid Glass text.
Code
Preview
Note: glassEffect modifier requires Xcode 26+ and iOS 26+.
SwiftUI modifiers
Copy
import { Host, Text } from '@expo/ui/swift-ui';
import { glassEffect, padding } from '@expo/ui/swift-ui/modifiers';
import { MeshGradientView } from 'expo-mesh-gradient';
import { View } from 'react-native';
export default function Page() {
return (
<View style={{ flex: 1 }}>
<MeshGradientView
style={{ flex: 1 }}
columns={3}
rows={3}
colors={['red', 'purple', 'indigo', 'orange', 'white', 'blue', 'yellow', 'green', 'cyan']}
points={[
[0.0, 0.0],
[0.5, 0.0],
[1.0, 0.0],
[0.0, 0.5],
[0.5, 0.5],
[1.0, 0.5],
[0.0, 1.0],
[0.5, 1.0],
[1.0, 1.0],
]}
/>
<Host style={{ position: 'absolute', top: 0, right: 0, left: 0, bottom: 0 }}>
<Text
size={32}
modifiers={[
padding({
all: 16,
}),
glassEffect({
glass: {
variant: 'clear',
},
}),
]}>
Glass effect text
</Text>
</Host>
</View>
);
}
Show More
iOS Settings app example
Combining the Expo UI components and modifiers, you can build a UI like iOS Settings app.
Code
Preview
SwiftUI Form example to build iOS Settings app
Copy
import {
Button,
Form,
Host,
HStack,
Image,
Section,
Spacer,
Switch,
Text,
} from '@expo/ui/swift-ui';
import { background, clipShape, frame } from '@expo/ui/swift-ui/modifiers';
import { Link } from 'expo-router';
import { useState } from 'react';
export default function SettingsView() {
const [isAirplaneMode, setIsAirplaneMode] = useState(true);
return (
<Host style={{ flex: 1 }}>
<Form>
<Section>
<HStack spacing={8}>
<Image
systemName="airplane"
color="white"
size={18}
modifiers={[
frame({ width: 28, height: 28 }),
background('#ffa500'),
clipShape('roundedRectangle'),
]}
/>
<Text>Airplane Mode</Text>
<Spacer />
<Switch value={isAirplaneMode} onValueChange={setIsAirplaneMode} />
</HStack>
<Link href="/wifi" asChild>
<Button>
<HStack spacing={8}>
<Image
systemName="wifi"
color="white"
size={18}
modifiers={[
frame({ width: 28, height: 28 }),
background('#007aff'),
clipShape('roundedRectangle'),
]}
/>
<Text color="primary">Wi-Fi</Text>
<Spacer />
<Image systemName="chevron.right" size={14} color="secondary" />
</HStack>
</Button>
</Link>
</Section>
</Form>
</Host>
);
}
Show More
Common questions
Can I use flexbox or other styles in SwiftUI components?
Flexbox styles can be applied to the <Host> component itself. Once you're inside the SwiftUI context, however, Yoga is not available — layouts should be defined using <HStack> and <VStack> instead.
What's the Host component?
<Host> is the container for SwiftUI views. You can think of it like <svg> in the DOM or <Canvas> in react-native-skia. Under the hood, it uses UIHostingController to render SwiftUI views in UIKit.
How is Expo UI different from libraries like react-native-paper or react-native-elements?
Expo UI is not "yet another" UI library and not an opinionated design kit. Instead, it's a primitives library. It exposes native SwiftUI and Jetpack Compose components directly to JavaScript, rather than re-implementing or simulating UI in JavaScript.
Can I use @expo/ui/swift-ui on Android or web?
The first milestone for Expo UI is achieving a 1-to-1 mapping from SwiftUI to Expo UI. Universal support will come in the next stage of the roadmap. Our priority is to establish strong SwiftUI support first, and then expand to Jetpack Compose on Android and DOM support on the Web.
Can I use React Native components inside SwiftUI components?
Yes, you can place React Native components as JSX children of Expo UI components. Expo UI automatically creates a UIViewRepresentable wrapper for you. However, keep in mind that the SwiftUI layout system works differently from UIKit and has some limitations. According to Apple's documentation:
SwiftUI fully controls the layout of the UIKit view's center, bounds, frame, and transform properties. Don't directly set these layout-related properties on the view managed by a UIViewRepresentable instance from your own code because that conflicts with SwiftUI and results in undefined behavior.
Also note that once you render React Native components, you're leaving the SwiftUI context. If you want to add Expo UI components again, you'll need to reintroduce a <Host> wrapper.
We recommend keeping SwiftUI layouts self-contained. Interop is possible, but it works best when boundaries are clearly defined.
I'm a SwiftUI developer. Why should I learn Expo UI?
Because React's promise of "learn once, write anywhere", it now extends to SwiftUI and Jetpack Compose. With Expo UI, you can apply your SwiftUI knowledge to build apps that run in the React Native ecosystem, extend to the Web through DOM components, and even integrate 2D and 3D rendering. The system is flexible enough that different parts of your app can use different approaches — giving you seamless integration at the component level.
SwiftUI
SwiftUI components for building native iOS interfaces with @expo/ui.
Bundled version:
~0.2.0-beta.4
Copy
This library is currently in beta and subject to breaking changes. It is not available in the Expo Go app — use development builds to try it out.
The SwiftUI components in @expo/ui/swift-ui allow you to build fully native iOS interfaces using SwiftUI from React Native.
Expo UI guide for Swift UI
Learn about the basics of @expo/ui/swift-ui
Installation
Terminal
Copy
npx expo install @expo/ui
If you are installing this in an existing React Native app, make sure to install expo in your project.
Components
BottomSheet
iOS
Code
import { BottomSheet, Host, Text } from '@expo/ui/swift-ui';
import { useWindowDimensions } from 'react-native';
const { width } = useWindowDimensions();
<Host style={{ position: 'absolute', width }}>
<BottomSheet isOpened={isOpened} onIsOpenedChange={e => setIsOpened(e)}>
<Text>Hello, world!</Text>
</BottomSheet>
</Host>
See also: official SwiftUI documentation
Button
The borderless variant is not available on Apple TV.
iOS
Code
import { Button, Host } from '@expo/ui/swift-ui';
<Host style={{ flex: 1 }}>
<Button
variant="default"
onPress={() => {
setEditingProfile(true);
}}>
Edit profile
</Button>
</Host>
See also: official SwiftUI documentation
CircularProgress
iOS
Code
import { CircularProgress, Host } from '@expo/ui/swift-ui';
<Host style={{ width: 300 }}>
<CircularProgress progress={0.5} color="blue" />
</Host>
See also: official SwiftUI documentation
ColorPicker
This component is not available on Apple TV.
iOS
Code
import { ColorPicker, Host } from '@expo/ui/swift-ui';
<Host style={{ width: 400, height: 200 }}>
<ColorPicker
label="Select a color"
selection={color}
onValueChanged={setColor}
/>
</Host>
See also: official SwiftUI documentation
ContextMenu
Note: Also known as DropdownMenu.
iOS
Code
import { ContextMenu, Host } from '@expo/ui/swift-ui';
<Host style={{ width: 150, height: 50 }}>
<ContextMenu>
<ContextMenu.Items>
<Button
systemImage="person.crop.circle.badge.xmark"
onPress={() => console.log('Pressed1')}>
Hello
</Button>
<Button
variant="bordered"
systemImage="heart"
onPress={() => console.log('Pressed2')}>
Love it
</Button>
<Picker
label="Doggos"
options={['very', 'veery', 'veeery', 'much']}
variant="menu"
selectedIndex={selectedIndex}
onOptionSelected={({ nativeEvent: { index } }) => setSelectedIndex(index)}
/>
</ContextMenu.Items>
<ContextMenu.Trigger>
<Button variant="bordered">
Show Menu
</Button>
</ContextMenu.Trigger>
</ContextMenu>
</Host>
Show More
See also: official SwiftUI documentation
DateTimePicker (date)
This component is not available on Apple TV.
iOS
Code
import { DateTimePicker, Host } from '@expo/ui/swift-ui';
<Host matchContents>
<DateTimePicker
onDateSelected={date => {
setSelectedDate(date);
}}
displayedComponents='date'
initialDate={selectedDate.toISOString()}
variant='wheel'
/>
</Host>
See also: official SwiftUI documentation
DateTimePicker (time)
This component is not available on Apple TV.
iOS
Code
import { DateTimePicker, Host } from '@expo/ui/swift-ui';
<Host matchContents>
<DateTimePicker
onDateSelected={date => {
setSelectedDate(date);
}}
displayedComponents='hourAndMinute'
initialDate={selectedDate.toISOString()}
variant='wheel'
/>
</Host>
See also: official SwiftUI documentation
Gauge
This component is not available on Apple TV.
iOS
Code
import { Gauge, Host } from "@expo/ui/swift-ui";
<Host matchContents>
<Gauge
max={{ value: 1, label: '1' }}
min={{ value: 0, label: '0' }}
current={{ value: 0.5 }}
color={[
PlatformColor('systemRed'),
PlatformColor('systemOrange'),
PlatformColor('systemYellow'),
PlatformColor('systemGreen'),
]}
type="circularCapacity"
/>
</Host>
See also: official SwiftUI documentation
Host
A component that allows you to put the other @expo/ui/swift-ui components in React Native. It acts like <svg> for DOM, <Canvas> for react-native-skia, which underlying uses UIHostingController to render the SwiftUI views in UIKit.
Since the Host component is a React Native View, you can pass the style prop to it or matchContents prop to make the Host component match the contents' size.
Wrapping Button in Host
Copy
import { Button, Host } from '@expo/ui/swift-ui';
function Example() {
return (
<Host matchContents>
<Button
onPress={() => {
console.log('Pressed');
}}>
Click
</Button>
</Host>
);
}
Host with flexbox and VStack
Copy
import { Button, Host, VStack, Text } from '@expo/ui/swift-ui';
function Example() {
return (
<Host style={{ flex: 1 }}>
<VStack spacing={8}>
<Text>Hello, world!</Text>
<Button
onPress={() => {
console.log('Pressed');
}}>
Click
</Button>
</VStack>
</Host>
);
}
LinearProgress
iOS
Code
import { LinearProgress, Host } from '@expo/ui/swift-ui';
<Host style={{ width: 300 }}>
<LinearProgress progress={0.5} color="red" />
</Host>
See also: official SwiftUI documentation
List
iOS
Code
import { Host, List } from '@expo/ui/swift-ui';
<Host style={{ flex: 1 }}>
<List
scrollEnabled={false}
editModeEnabled={editModeEnabled}
onSelectionChange={(items) => alert(`indexes of selected items: ${items.join(', ')}`)}
moveEnabled={moveEnabled}
onMoveItem={(from, to) => alert(`moved item at index ${from} to index ${to}`)}
onDeleteItem={(item) => alert(`deleted item at index: ${item}`)}
listStyle='automatic'
deleteEnabled={deleteEnabled}
selectEnabled={selectEnabled}>
{data.map((item, index) => (
<LabelPrimitive key={index} title={item.text} systemImage={item.systemImage} color={color} />
))}
</List>
</Host>
Show More
See also: official SwiftUI documentation
Picker (segmented)
iOS
Code
import { Host, Picker } from '@expo/ui/swift-ui';
<Host matchContents>
<Picker
options={['$', '$$', '$$$', '$$$$']}
selectedIndex={selectedIndex}
onOptionSelected={({ nativeEvent: { index } }) => {
setSelectedIndex(index);
}}
variant="segmented"
/>
</Host>
See also: official SwiftUI documentation
Picker (wheel)
The wheel variant is not available on Apple TV.
iOS
Code
import { Host, Picker } from '@expo/ui/swift-ui';
<Host style={{ height: 100 }}>
<Picker
options={['$', '$$', '$$$', '$$$$']}
selectedIndex={selectedIndex}
onOptionSelected={({ nativeEvent: { index } }) => {
setSelectedIndex(index);
}}
variant="wheel"
/>
</Host>
See also: official SwiftUI documentation
Slider
This component is not available on Apple TV.
iOS
Code
import { Host, Slider } from '@expo/ui/swift-ui';
<Host style={{ minHeight: 60 }}>
<Slider
value={value}
onValueChange={(value) => {
setValue(value);
}}
/>
</Host>
See also: official SwiftUI documentation
Switch (toggle)
Note: Also known as Toggle.
iOS
Code
import { Host, Switch } from '@expo/ui/swift-ui';
<Host matchContents>
<Switch
checked={checked}
onValueChange={checked => {
setChecked(checked);
}}
color="#ff0000"
label="Play music"
variant="switch"
/>
</Host>
See also: official SwiftUI documentation
Switch (checkbox)
iOS
Code
import { Host, Switch } from '@expo/ui/swift-ui';
<Host matchContents>
<Switch
checked={checked}
onValueChange={checked => {
setChecked(checked);
}}
label="Play music"
variant="checkbox"
/>
</Host>
See also: official SwiftUI documentation
TextField
iOS
Code
import { Host, TextField } from '@expo/ui/swift-ui';
<Host matchContents>
<TextField autocorrection={false} defaultValue="A single line text input" onChangeText={setValue} />
</Host>
See also: official SwiftUI documentation
More
Expo UI is still in active development. We continue to add more functionality and may change the API. Some examples in the docs may not be up to date. If you want to see the latest changes, check the examples.
API
Full documentation is not yet available. Use TypeScript types to explore the API.
// Import from the SwiftUI package
import { BottomSheet } from '@expo/ui/swift-ui';
Jetpack Compose
Jetpack Compose components for building native Android interfaces with @expo/ui.
Bundled version:
~0.2.0-beta.4
Copy
This library is currently in alpha and will frequently experience breaking changes. It is not available in the Expo Go app — use development builds to try it out.
The Jetpack Compose components in @expo/ui/jetpack-compose allow you to build fully native Android interfaces using Jetpack Compose from React Native.
Installation
Terminal
Copy
npx expo install @expo/ui
If you are installing this in an existing React Native app, make sure to install expo in your project.
Components
Button
Android
Code
import { Button } from '@expo/ui/jetpack-compose';
<Button
style={{ flex: 1 }}
variant="default"
onPress={() => {
setEditingProfile(true);
}}>
Edit profile
</Button>
See also: official Jetpack Compose documentation
CircularProgress
Android
Code
import { CircularProgress } from '@expo/ui/jetpack-compose';
<CircularProgress progress={0.5} style={{ width: 300 }} color="blue" elementColors={{ trackColor: '#cccccc' }} />
See also: official Jetpack Compose documentation
ContextMenu
Note: Also known as DropdownMenu.
Android
Code
import { ContextMenu } from '@expo/ui/jetpack-compose';
<ContextMenu style={{ width: 150, height: 50 }}>
<ContextMenu.Items>
<Button
elementColors={{ containerColor: '#0000ff', contentColor: '#00ff00' }}
onPress={() => console.log('Pressed1')}>
Hello
</Button>
<Button
variant="bordered"
color="#ff0000"
onPress={() => console.log('Pressed2')}>
Love it
</Button>
<Picker
label="Doggos"
options={['very', 'veery', 'veeery', 'much']}
variant="menu"
selectedIndex={selectedIndex}
onOptionSelected={({ nativeEvent: { index } }) => setSelectedIndex(index)}
/>
</ContextMenu.Items>
<ContextMenu.Trigger>
<Button variant="bordered" style={{ width: 150, height: 50 }}>
Show Menu
</Button>
</ContextMenu.Trigger>
</ContextMenu>
Show More
See also: official Jetpack Compose documentation
Chip
Android
Code
import { Chip } from '@expo/ui/jetpack-compose';
// Assist chip with icon
<Chip
variant="assist"
label="Book"
leadingIcon="filled.Add"
onPress={() => console.log('Opening flight booking...')}
/>
// Filter chip with selection
<Chip
variant="filter"
label="Images"
leadingIcon="filled.Star"
selected={selectedFilters.includes('Images')}
onPress={() => handleFilterToggle('Images')}
/>
// Input chip with dismiss
<Chip
variant="input"
label="Work"
leadingIcon="filled.Create"
onDismiss={() => handleInputDismiss('Work')}
/>
// Suggestion chip
<Chip
variant="suggestion"
label="Nearby"
leadingIcon="filled.LocationOn"
onPress={() => console.log('Searching nearby...')}
/>
Show More
See also: official Jetpack Compose documentation
DateTimePicker (date)
Android
Code
import { DateTimePicker } from '@expo/ui/jetpack-compose';
<DateTimePicker
onDateSelected={date => {
setSelectedDate(date);
}}
displayedComponents='date'
initialDate={selectedDate.toISOString()}
variant='picker'
/>
See also: official Jetpack Compose documentation
DateTimePicker (time)
Android
Code
import { DateTimePicker } from '@expo/ui/jetpack-compose';
<DateTimePicker
onDateSelected={date => {
setSelectedDate(date);
}}
displayedComponents='hourAndMinute'
initialDate={selectedDate.toISOString()}
variant='picker'
/>
See also: official Jetpack Compose documentation
LinearProgress
Android
Code
import { LinearProgress } from '@expo/ui/jetpack-compose';
<LinearProgress progress={0.5} style={{ width: 300 }} color="red" />
See also: official Jetpack Compose documentation
Picker (radio)
Android
Code
import { Picker } from '@expo/ui/jetpack-compose';
<Picker
options={['$', '$$', '$$$', '$$$$']}
selectedIndex={selectedIndex}
onOptionSelected={({ nativeEvent: { index } }) => {
setSelectedIndex(index);
}}
variant="radio"
/>
See also: official Jetpack Compose documentation
Picker (segmented)
Android
Code
import { Picker } from '@expo/ui/jetpack-compose';
<Picker
options={['$', '$$', '$$$', '$$$$']}
selectedIndex={selectedIndex}
onOptionSelected={({ nativeEvent: { index } }) => {
setSelectedIndex(index);
}}
variant="segmented"
/>
See also: official Jetpack Compose documentation
Slider
Android
Code
import { Slider } from '@expo/ui/jetpack-compose';
<Slider
style={{ minHeight: 60 }}
value={value}
onValueChange={(value) => {
setValue(value);
}}
/>
See also: official Jetpack Compose documentation
Switch (toggle)
Note: also known as Toggle.
Android
Code
import { Switch } from '@expo/ui/jetpack-compose';
<Switch
value={checked}
onValueChange={checked => {
setChecked(checked);
}}
color="#ff0000"
label="Play music"
variant="switch"
/>
See also: official Jetpack Compose documentation
Switch (checkbox)
Android
Code
import { Switch } from '@expo/ui/jetpack-compose';
<Switch
value={checked}
onValueChange={checked => {
setChecked(checked);
}}
label="Play music"
color="#ff0000"
variant="checkbox"
/>
See also: official Jetpack Compose documentation
TextInput
Android
Code
import { TextInput } from '@expo/ui/jetpack-compose';
<TextInput autocorrection={false} defaultValue="A single line text input" onChangeText={setValue} />
See also: official Jetpack Compose documentation
API
Full documentation is not yet available. Use TypeScript types to explore the API.
// Import from the Jetpack Compose package
import { Button } from '@expo/ui/jetpack-compose';

View file

@ -0,0 +1,293 @@
Expo UI
A set of components that allow you to build UIs directly with SwiftUI and Jetpack Compose from React.
Bundled version:
~0.2.0-beta.4
Copy
This library is currently in alpha and will frequently experience breaking changes. It is not available in the Expo Go app use development builds to try it out.
@expo/ui is a set of native input components that allows you to build fully native interfaces with SwiftUI and Jetpack Compose. It aims to provide the commonly used features and components that a typical app will need.
Expo UI guide for Swift UI
Learn about the basics of @expo/ui/swift-ui
Installation
Terminal
Copy
npx expo install @expo/ui
If you are installing this in an existing React Native app, make sure to install expo in your project.
Swift UI examples
BottomSheet
iOS
Code
BottomSheet component on iOS.
Button
The borderless variant is not available on Apple TV.
iOS
Code
Button component on iOS.
CircularProgress
iOS
Code
CircularProgress component on iOS.
ColorPicker
This component is not available on Apple TV.
iOS
Code
ColorPicker component on iOS.
ContextMenu
Note: Also known as DropdownMenu.
iOS
Code
ContextMenu component on iOS.
DateTimePicker (date)
This component is not available on Apple TV.
iOS
Code
DateTimePicker (date) component on iOS.
DateTimePicker (time)
This component is not available on Apple TV.
iOS
Code
DateTimePicker (time) component on iOS.
Gauge
This component is not available on Apple TV.
iOS
Code
Gauge component on iOS.
Host
A component that allows you to put the other @expo/ui/swift-ui components in React Native. It acts like <svg> for DOM, <Canvas> for react-native-skia, which underlying uses UIHostingController to render the SwiftUI views in UIKit.
Since the Host component is a React Native View, you can pass the style prop to it or matchContents prop to make the Host component match the contents' size.
Wrapping Button in Host
Copy
import { Button, Host } from '@expo/ui/swift-ui';
function Example() {
return (
<Host matchContents>
<Button
onPress={() => {
console.log('Pressed');
}}>
Click
</Button>
</Host>
);
}
Host with flexbox and VStack
Copy
import { Button, Host, VStack, Text } from '@expo/ui/swift-ui';
function Example() {
return (
<Host style={{ flex: 1 }}>
<VStack spacing={8}>
<Text>Hello, world!</Text>
<Button
onPress={() => {
console.log('Pressed');
}}>
Click
</Button>
</VStack>
</Host>
);
}
LinearProgress
iOS
Code
LinearProgress component on iOS.
List
iOS
Code
List component on iOS.
Picker (segmented)
iOS
Code
Picker component on iOS.
Picker (wheel)
The wheel variant is not available on Apple TV.
iOS
Code
Picker component on iOS.
Slider
This component is not available on Apple TV.
iOS
Code
Slider component on iOS.
Switch (toggle)
Note: Also known as Toggle.
iOS
Code
Switch component on iOS.
Switch (checkbox)
iOS
Code
Picker component on iOS.
TextField
iOS
Code
TextField component on iOS.
More
Expo UI is still in active development. We continue to add more functionality and may change the API. Some examples in the docs may not be up to date. If you want to see the latest changes, check the examples.
Jetpack Compose examples
Button
Android
Code
Button component on Android.
CircularProgress
Android
Code
CircularProgress component on Android.
ContextMenu
Note: Also known as DropdownMenu.
Android
Code
ContextMenu component on Android.
Chip
Android
Code
Chip component on Android.
DateTimePicker (date)
Android
Code
DateTimePicker component on Android.
DateTimePicker (time)
Android
Code
DateTimePicker (time) component on Android.
LinearProgress
Android
Code
LinearProgress component on Android.
Picker (radio)
Android
Code
Picker component (radio) on Android.
Picker (segmented)
Android
Code
Picker component on Android.
Slider
Android
Code
Slider component on Android.
Switch (toggle)
Note: also known as Toggle.
Android
Code
Switch component on Android.
Switch (checkbox)
Android
Code
Switch (checkbox variant) component on Android.
TextInput
Android
Code
TextInput component on Android.
API
Full documentation is not yet available. Use TypeScript types to explore the API.
// Import from the SwiftUI package
import { BottomSheet } from '@expo/ui/swift-ui';
// Import from the Jetpack Compose package
import { Button } from '@expo/ui/jetpack-compose';

View file

@ -0,0 +1,200 @@
# Glass/Blur Header Implementation Guide - Expo Router
## Overview
Glass headers (blur effect headers) provide a modern iOS-style frosted glass appearance that blurs content behind the header when scrolling. This guide covers implementation in Expo Router.
## Requirements
- Expo Router (latest version)
- iOS device/simulator (blur effect is iOS-only)
- `@react-navigation/elements` package (for `useHeaderHeight` hook)
## Basic Implementation
### 1. Enable Blur Effect in Layout
```typescript
// app/(tabs)/_layout.tsx or app/_layout.tsx
import { Tabs } from 'expo-router';
import { Platform } from 'react-native';
export default function Layout() {
return (
<Tabs
screenOptions={{
headerShown: true,
headerTransparent: Platform.OS === 'ios',
headerBlurEffect: Platform.OS === 'ios' ? 'regular' : undefined,
}}>
<Tabs.Screen name="index" options={{ title: 'Home' }} />
</Tabs>
);
}
```
### 2. Handle Content Padding
Since the header is transparent, content will render behind it. Use `useHeaderHeight` to add proper padding:
```typescript
// app/(tabs)/index.tsx
import { useHeaderHeight } from '@react-navigation/elements';
import { Platform, FlatList } from 'react-native';
export default function Screen() {
const headerHeight = useHeaderHeight();
return (
<FlatList
data={items}
contentContainerStyle={{
paddingTop: Platform.OS === 'ios' ? headerHeight : 0
}}
renderItem={({ item }) => <Item data={item} />}
/>
);
}
```
## Blur Effect Options (iOS Only)
Available `headerBlurEffect` values:
- `'regular'` - Standard blur (recommended)
- `'prominent'` - More pronounced blur
- `'systemMaterial'` - Adapts to system theme
- `'systemThinMaterial'` - Thinner material
- `'systemUltraThinMaterial'` - Ultra-thin material
- `'systemThickMaterial'` - Thicker material
- `'systemChromeMaterial'` - Chrome-style material
- `'systemThinMaterialLight'` - Light thin material
- `'systemThinMaterialDark'` - Dark thin material
## Platform-Specific Considerations
### iOS
- Blur effect works natively
- Requires both `headerTransparent: true` and `headerBlurEffect`
- Header automatically adapts to light/dark mode with system materials
### Android
- Native blur not supported
- Header remains solid
- Use `Platform.OS === 'ios'` checks to prevent errors
### Web
- Blur not supported
- Falls back to solid header
## Best Practices
### ✅ Do
- Always use Platform checks for iOS-specific features
- Add proper content padding with `useHeaderHeight`
- Test on both iOS and Android
- Use `'regular'` or `'systemMaterial'` for best compatibility
- Consider dark mode with appropriate blur materials
### ❌ Don't
- Don't apply blur without `headerTransparent: true`
- Don't forget to handle content padding
- Don't use iOS-specific values on Android
- Don't rely on blur for critical UI elements visibility
## Common Issues & Solutions
### Issue: Content appears behind header
**Solution**: Use `useHeaderHeight` hook to add `paddingTop`
```typescript
const headerHeight = useHeaderHeight();
contentContainerStyle={{ paddingTop: Platform.OS === 'ios' ? headerHeight : 0 }}
```
### Issue: Blur not visible
**Solution**: Ensure both `headerTransparent` and `headerBlurEffect` are set
```typescript
headerTransparent: true,
headerBlurEffect: 'regular',
```
### Issue: Android crashes or looks wrong
**Solution**: Use platform checks
```typescript
headerTransparent: Platform.OS === 'ios',
headerBlurEffect: Platform.OS === 'ios' ? 'regular' : undefined,
```
### Issue: Header text not readable
**Solution**: Use appropriate blur material or adjust header tint color
```typescript
headerBlurEffect: 'systemMaterial', // Adapts to theme
headerTintColor: colors.foreground,
```
## Complete Example
```typescript
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Platform } from 'react-native';
import { useThemeColors } from '~/utils/themeUtils';
export default function TabLayout() {
const colors = useThemeColors();
return (
<Tabs
screenOptions={{
headerShown: true,
headerTransparent: Platform.OS === 'ios',
headerBlurEffect: Platform.OS === 'ios' ? 'systemMaterial' : undefined,
headerTintColor: colors.foreground,
tabBarStyle: {
backgroundColor: colors.background,
},
}}>
<Tabs.Screen
name="index"
options={{
title: 'Home',
}}
/>
</Tabs>
);
}
```
```typescript
// app/(tabs)/index.tsx
import { View, FlatList, Platform } from 'react-native';
import { useHeaderHeight } from '@react-navigation/elements';
export default function HomeScreen() {
const headerHeight = useHeaderHeight();
return (
<View style={{ flex: 1 }}>
<FlatList
data={items}
contentContainerStyle={{
paddingTop: Platform.OS === 'ios' ? headerHeight : 0,
paddingHorizontal: 16,
}}
renderItem={({ item }) => <Card item={item} />}
/>
</View>
);
}
```
## References
- [Expo Router Stack Documentation](https://docs.expo.dev/router/advanced/stack/)
- [React Navigation Header Options](https://reactnavigation.org/docs/native-stack-navigator/#headerblureffect)
- [Aman Mittal's Guide](https://amanhimself.dev/blog/blur-effect-in-header-with-expo-router/)
## Version Info
- Last updated: 2025-01
- Expo SDK: 52+
- Expo Router: v5+

View file

@ -0,0 +1,382 @@
# Header Styling Guide - Expo Router
## Overview
This guide covers all available styling options for headers in Expo Router's Stack navigator.
## Available Header Style Options
### Basic Configuration
```typescript
import { Stack } from 'expo-router';
import { useThemeColors } from '~/utils/themeUtils';
export default function Layout() {
const colors = useThemeColors();
return (
<Stack
screenOptions={{
// Header visibility
headerShown: true,
// Header title
title: 'My Screen',
// Glass effect (iOS only)
headerTransparent: true,
headerBlurEffect: 'systemMaterial',
// Colors
headerTintColor: colors.foreground,
headerStyle: {
backgroundColor: colors.background,
},
// Title styling
headerTitleStyle: {
fontWeight: 'bold',
fontSize: 18,
fontFamily: 'System',
},
// Shadow/Border
headerShadowVisible: false,
// Back button
headerBackVisible: true,
headerBackTitle: 'Back',
headerBackTitleVisible: true,
}}>
<Stack.Screen name="index" />
</Stack>
);
}
```
## Core Style Properties
### 1. headerStyle
Controls the header container's style.
```typescript
headerStyle: {
backgroundColor: '#ff0000', // Background color (not needed with headerTransparent)
}
```
**Note**: Cannot set `height` - this is controlled by the OS.
### 2. headerTintColor
Sets the color for back button and title text.
```typescript
headerTintColor: '#ffffff' // White text and icons
```
### 3. headerTitleStyle
Customizes the title text appearance.
```typescript
headerTitleStyle: {
fontWeight: 'bold',
fontSize: 20,
fontFamily: 'CustomFont',
color: '#000000', // Can override headerTintColor for title only
}
```
### 4. headerTitleAlign
Controls title alignment.
```typescript
headerTitleAlign: 'center' | 'left' // Default: 'center' on iOS, 'left' on Android
```
### 5. headerTransparent
Makes header transparent for glass effect.
```typescript
headerTransparent: true // Required for headerBlurEffect
```
### 6. headerBlurEffect (iOS only)
Adds blur/glass effect to header.
```typescript
headerBlurEffect: 'systemMaterial' | 'regular' | 'prominent' |
'systemThinMaterial' | 'systemUltraThinMaterial' |
'systemChromeMaterial' | 'systemThinMaterialLight' |
'systemThinMaterialDark'
```
### 7. headerShadowVisible
Controls header shadow/border visibility.
```typescript
headerShadowVisible: false // Remove shadow
```
## Back Button Customization
### headerBackVisible
Show/hide back button.
```typescript
headerBackVisible: true // Show back button
```
### headerBackTitle (iOS only)
Custom text for back button.
```typescript
headerBackTitle: 'Home' // Default: Previous screen title
```
### headerBackTitleVisible (iOS only)
Show/hide back button text.
```typescript
headerBackTitleVisible: false // Only show arrow icon
```
### headerBackImageSource
Custom back button icon.
```typescript
headerBackImageSource: require('./assets/back-icon.png')
```
## Title Customization
### headerTitle
Function to render custom title component.
```typescript
headerTitle: () => (
<View>
<Text style={{ fontSize: 20, fontWeight: 'bold' }}>Custom Title</Text>
</View>
)
```
### headerTitleAlign
```typescript
headerTitleAlign: 'center' // 'center' | 'left'
```
## Header Buttons
### headerLeft
Custom left button/component.
```typescript
headerLeft: () => (
<Pressable onPress={() => console.log('Left button')}>
<Icon name="menu" size={24} />
</Pressable>
)
```
### headerRight
Custom right button/component.
```typescript
headerRight: () => (
<Pressable onPress={() => console.log('Right button')}>
<Icon name="settings" size={24} />
</Pressable>
)
```
## Search Bar (iOS 11+)
### headerSearchBarOptions
Native iOS search bar in header.
```typescript
headerSearchBarOptions: {
placeholder: 'Search...',
onChangeText: (text) => console.log(text),
hideWhenScrolling: true,
}
```
## Large Title (iOS)
### headerLargeTitle
Large title that collapses on scroll (iOS only).
```typescript
headerLargeTitle: true
```
### headerLargeTitleStyle
Style for large title.
```typescript
headerLargeTitleStyle: {
fontWeight: 'bold',
fontSize: 34,
}
```
### headerLargeTitleShadowVisible
Shadow for large title.
```typescript
headerLargeTitleShadowVisible: false
```
## Complete Example: Glass Header with Theme
```typescript
import { Stack } from 'expo-router';
import { Platform } from 'react-native';
import { useThemeColors } from '~/utils/themeUtils';
export default function Layout() {
const colors = useThemeColors();
return (
<Stack
screenOptions={{
headerShown: true,
// Glass effect (iOS only)
...(Platform.OS === 'ios' ? {
headerTransparent: true,
headerBlurEffect: 'systemMaterial',
} : {
headerStyle: {
backgroundColor: colors.background,
},
}),
// Colors
headerTintColor: colors.foreground,
// Title styling
headerTitleStyle: {
fontWeight: '600',
fontSize: 17,
},
headerTitleAlign: 'center',
// Shadow
headerShadowVisible: false,
// Back button
headerBackTitleVisible: false,
}}>
<Stack.Screen
name="index"
options={{
title: 'Home',
headerRight: () => (
<Pressable onPress={() => console.log('Settings')}>
<Icon name="settings" size={24} color={colors.foreground} />
</Pressable>
),
}}
/>
</Stack>
);
}
```
## Platform Differences
### iOS
- Default title alignment: `center`
- Supports `headerBlurEffect`
- Supports `headerLargeTitle`
- Supports `headerSearchBarOptions`
- Back button shows previous screen title by default
### Android
- Default title alignment: `left`
- No blur effect (use solid `backgroundColor`)
- No large title support
- No native search bar in header
- Back button only shows arrow icon
## Common Patterns
### 1. Remove Header Shadow
```typescript
headerShadowVisible: false
```
### 2. Custom Back Button
```typescript
headerLeft: () => (
<Pressable onPress={() => router.back()}>
<Icon name="arrow-back" size={24} color={colors.foreground} />
</Pressable>
)
```
### 3. Hide Header on Specific Screen
```typescript
<Stack.Screen
name="fullscreen"
options={{ headerShown: false }}
/>
```
### 4. Dynamic Title
```typescript
<Stack.Screen
name="detail"
options={{
title: dynamicTitle,
// or
headerTitle: () => <CustomTitleComponent title={dynamicTitle} />
}}
/>
```
### 5. Large Title with Search (iOS)
```typescript
<Stack.Screen
name="search"
options={{
headerLargeTitle: true,
headerSearchBarOptions: {
placeholder: 'Search items...',
},
}}
/>
```
## Limitations
### Cannot Customize:
- ❌ Header height (controlled by OS)
- ❌ Status bar appearance (use `expo-status-bar` package)
- ❌ Exact positioning of elements (use headerLeft/headerRight instead)
### Platform-Specific:
- ⚠️ Blur effects only on iOS
- ⚠️ Large titles only on iOS
- ⚠️ Native search bar only on iOS 11+
## Troubleshooting
### Issue: Title not visible on transparent header
**Solution**: Ensure `headerTintColor` is set to a visible color
### Issue: Back button wrong color
**Solution**: Set `headerTintColor` - it controls both title and back button
### Issue: Header too tall/short
**Solution**: Height is controlled by OS and cannot be changed. Use custom header component if needed.
### Issue: Shadow visible on transparent header
**Solution**: Set `headerShadowVisible: false`
## References
- [Expo Router Stack Documentation](https://docs.expo.dev/router/advanced/stack/)
- [React Navigation Native Stack Navigator](https://reactnavigation.org/docs/native-stack-navigator/)
- [React Navigation Headers](https://reactnavigation.org/docs/headers/)

View file

@ -0,0 +1,150 @@
# Native Tab Behavior - Manadeck App
## Übersicht
Die Manadeck-App nutzt die **Native Tabs** von Expo Router (SDK 54+), um eine native iOS/Android Tab-Navigation zu implementieren. Diese Dokumentation beschreibt die Konfiguration und das Verhalten der Tabs in unserer App.
## Aktuelle Implementierung
### Standort
`app/(tabs)/_layout.tsx`
### Konfiguration
```tsx
import { NativeTabs, Icon, Label } from 'expo-router/unstable-native-tabs';
export default function TabLayout() {
return (
<NativeTabs minimizeBehavior="automatic">
<NativeTabs.Trigger name="decks">
<Label>Decks</Label>
<Icon
sf={{ default: 'square.stack.3d.up', selected: 'square.stack.3d.up.fill' }}
drawable="ic_albums"
/>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="explore">
<Label>Entdecken</Label>
<Icon sf={{ default: 'safari', selected: 'safari.fill' }} drawable="ic_explore" />
</NativeTabs.Trigger>
<NativeTabs.Trigger name="progress">
<Label>Fortschritt</Label>
<Icon
sf={{ default: 'chart.line.uptrend.xyaxis', selected: 'chart.line.uptrend.xyaxis' }}
drawable="ic_trending_up"
/>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="profile">
<Label>Profil</Label>
<Icon sf={{ default: 'person', selected: 'person.fill' }} drawable="ic_person" />
</NativeTabs.Trigger>
</NativeTabs>
);
}
```
## Minimize Behavior
### Was ist `minimizeBehavior`?
Die `minimizeBehavior` Property steuert, wie sich die Tab-Leiste beim Scrollen verhält. Diese Funktion ist ab iOS 26 verfügbar und erfordert Xcode 26 oder höher zum Kompilieren.
### Verfügbare Optionen
| Option | Beschreibung |
|--------|--------------|
| `'automatic'` | **[AKTUELL IN APP]** iOS entscheidet automatisch, wann die Tab-Leiste minimiert wird. Dies passt sich dem nativen iOS-Kontext an. |
| `'never'` | Tab-Leiste bleibt immer sichtbar und wird nie minimiert. |
| `'onScrollDown'` | Tab-Leiste wird beim Runterscrollen minimiert und beim Hochscrollen wieder angezeigt. |
| `'onScrollUp'` | Tab-Leiste wird beim Hochscrollen minimiert (seltener verwendet). |
### Unsere Wahl: `automatic`
Wir verwenden `minimizeBehavior="automatic"`, weil:
- iOS das Verhalten optimal an den Kontext anpasst
- Es sich nativ anfühlt und konsistent mit anderen iOS-Apps ist
- Keine manuelle Konfiguration für verschiedene Scroll-Szenarien nötig ist
## Systemanforderungen
### iOS
- **Minimum:** iOS 26 (für `minimizeBehavior`)
- **Xcode:** Version 26 oder höher
- **Expo SDK:** 54 oder höher
- **Status:** ✅ Produktionsreif (experimentelle API)
### Android
- **Status:** 🚧 In Entwicklung
- Native Tab Minimize Behavior ist noch nicht verfügbar
- Geplante Unterstützung in zukünftigen Expo-Versionen
## Bekannte Einschränkungen
### FlatList-Integration
⚠️ **Wichtig:** FlatList-Komponenten haben Limitierungen mit Native Tabs:
- `scroll-to-top` funktioniert möglicherweise nicht wie erwartet
- `minimize-on-scroll` kann bei FlatList fehlschlagen
- Edge-Detection kann fehlschlagen (Tab-Leiste wird transparent)
**Lösung:** Wenn Probleme auftreten, verwende den `disableTransparentOnScrollEdge` Prop:
```tsx
<NativeTabs minimizeBehavior="automatic" disableTransparentOnScrollEdge>
{/* Tabs */}
</NativeTabs>
```
### Weitere Limitierungen
1. **Maximum 5 Tabs auf Android** - Material Design Komponenten-Beschränkung
2. **Tab-Leisten-Höhe nicht messbar** - Position variiert je nach Gerät (iPad, Vision Pro, etc.)
3. **Keine verschachtelten Native Tabs** - JavaScript Tabs können aber innerhalb von Native Tabs verschachtelt werden
## Best Practices für Manadeck
### Icons
- **iOS:** SF Symbols verwenden (höhere Qualität, native Optik)
- **Android:** Drawables verwenden
- Immer `default` und `selected` States definieren für besseres UX
### Scroll-Verhalten
- Verwende ScrollView oder ähnliche Komponenten in Tab-Screens
- Bei FlatList-Problemen: `disableTransparentOnScrollEdge` aktivieren
- Teste das Verhalten auf echten Geräten (Simulator kann abweichen)
### Entwicklung
```bash
# Development Server starten
npm start
# iOS Simulator (erfordert Xcode 26+)
npm run ios
# Android Emulator
npm run android
```
## Weiterführende Ressourcen
- **Vollständige Expo Docs:** `docs/EXPO_NATIVE_TABS.md`
- **Expo Router Native Tabs:** https://docs.expo.dev/router/advanced/native-tabs/
- **API Referenz:** https://docs.expo.dev/versions/latest/sdk/router-native-tabs/
## Änderungshistorie
| Datum | Änderung | Version |
|-------|----------|---------|
| 2025-10-20 | `minimizeBehavior="automatic"` hinzugefügt | SDK 54 |
| 2025-09 | Initiale Native Tabs Implementierung | SDK 54 |
---
**Status:** ✅ Experimentell (API kann sich ändern)
**Plattform:** iOS (primär), Android (geplant)
**Maintainer:** Manadeck Team

View file

@ -0,0 +1,173 @@
# Manadeck Datenbank-Dokumentation
## Übersicht
Die Manadeck-Anwendung verwendet **Supabase** (PostgreSQL) als Backend-Datenbank. Die Datenbank ist für eine mobile Lern- und Karteikarten-Anwendung konzipiert und unterstützt Multi-User-Funktionalität mit Row Level Security (RLS).
## Datenbankstruktur
### Tabellen
#### 1. **profiles**
Speichert Benutzerprofile und Einstellungen.
| Spalte | Typ | Beschreibung | Constraints |
|--------|-----|--------------|-------------|
| `id` | uuid | Benutzer-ID (Referenz zu auth.users) | PRIMARY KEY, NOT NULL |
| `username` | varchar | Eindeutiger Benutzername | NOT NULL, UNIQUE |
| `display_name` | varchar | Anzeigename des Benutzers | NULL |
| `avatar_url` | text | URL zum Profilbild | NULL |
| `bio` | text | Biografie/Beschreibung | NULL |
| `preferences` | jsonb | Benutzereinstellungen als JSON | DEFAULT '{}' |
| `created_at` | timestamptz | Erstellungszeitpunkt | DEFAULT now() |
| `updated_at` | timestamptz | Letzte Aktualisierung | DEFAULT now() |
**Beziehungen:**
- Foreign Key zu `auth.users(id)` über `id`
#### 2. **decks**
Verwaltet Kartenstapel/Sammlungen von Lernkarten.
| Spalte | Typ | Beschreibung | Constraints |
|--------|-----|--------------|-------------|
| `id` | uuid | Eindeutige Deck-ID | PRIMARY KEY, DEFAULT gen_random_uuid() |
| `user_id` | uuid | Besitzer des Decks | NOT NULL, FK zu auth.users |
| `title` | varchar | Titel des Decks | NOT NULL |
| `description` | text | Beschreibung des Decks | NULL |
| `cover_image_url` | text | URL zum Cover-Bild | NULL |
| `is_public` | boolean | Öffentlich sichtbar? | DEFAULT false |
| `settings` | jsonb | Deck-spezifische Einstellungen | DEFAULT '{}' |
| `tags` | text[] | Array von Tags | DEFAULT '{}' |
| `metadata` | jsonb | Zusätzliche Metadaten | DEFAULT '{}' |
| `created_at` | timestamptz | Erstellungszeitpunkt | DEFAULT now() |
| `updated_at` | timestamptz | Letzte Aktualisierung | DEFAULT now() |
**Beziehungen:**
- Foreign Key zu `auth.users(id)` über `user_id`
- One-to-Many Beziehung zu `cards`
#### 3. **cards**
Einzelne Lernkarten innerhalb der Decks.
| Spalte | Typ | Beschreibung | Constraints |
|--------|-----|--------------|-------------|
| `id` | uuid | Eindeutige Karten-ID | PRIMARY KEY, DEFAULT gen_random_uuid() |
| `deck_id` | uuid | Zugehöriges Deck | NOT NULL, FK zu decks |
| `position` | integer | Position innerhalb des Decks | NOT NULL |
| `title` | varchar | Titel/Überschrift der Karte | NULL |
| `content` | jsonb | Karteninhalt (strukturiert) | DEFAULT '{}' |
| `card_type` | card_type (enum) | Typ der Karte | NOT NULL, DEFAULT 'text' |
| `ai_model` | varchar | Verwendetes AI-Modell | NULL |
| `ai_prompt` | text | AI-Prompt für Generierung | NULL |
| `version` | integer | Versionsnummer | DEFAULT 1 |
| `is_favorite` | boolean | Als Favorit markiert? | DEFAULT false |
| `created_at` | timestamptz | Erstellungszeitpunkt | DEFAULT now() |
| `updated_at` | timestamptz | Letzte Aktualisierung | DEFAULT now() |
**Beziehungen:**
- Foreign Key zu `decks(id)` über `deck_id`
### Custom Types
#### card_type (Enum)
Definiert die verschiedenen Kartentypen:
- `text` - Reine Textkarte
- `mixed` - Gemischter Inhalt
- `quiz` - Quiz-Karte
- `flashcard` - Klassische Lernkarte
## Sicherheit (Row Level Security)
Alle Tabellen haben RLS aktiviert mit folgenden Policies:
### profiles Policies
| Policy | Operation | Beschreibung |
|--------|-----------|--------------|
| "Profiles are viewable by everyone" | SELECT | Alle können Profile einsehen |
| "Users can insert own profile" | INSERT | Authentifizierte Benutzer können eigenes Profil anlegen |
| "Users can update own profile" | UPDATE | Benutzer können nur eigenes Profil bearbeiten |
### decks Policies
| Policy | Operation | Beschreibung |
|--------|-----------|--------------|
| "View public decks and own decks" | SELECT | Öffentliche Decks und eigene Decks sind sichtbar |
| "Users can create own decks" | INSERT | Authentifizierte Benutzer können Decks erstellen |
| "Users can update own decks" | UPDATE | Nur eigene Decks können bearbeitet werden |
| "Users can delete own decks" | DELETE | Nur eigene Decks können gelöscht werden |
### cards Policies
| Policy | Operation | Beschreibung |
|--------|-----------|--------------|
| "View cards from accessible decks" | SELECT | Karten aus öffentlichen oder eigenen Decks sind sichtbar |
| "Users can create cards in own decks" | INSERT | Karten können nur in eigenen Decks erstellt werden |
| "Users can update cards in own decks" | UPDATE | Karten in eigenen Decks können bearbeitet werden |
| "Users can delete cards in own decks" | DELETE | Karten in eigenen Decks können gelöscht werden |
## Aktivierte Extensions
- **uuid-ossp** (v1.1) - UUID-Generierung
- **pgcrypto** (v1.3) - Kryptografische Funktionen
- **pg_graphql** (v1.5.11) - GraphQL-Unterstützung
- **pg_stat_statements** (v1.11) - Query-Performance-Monitoring
- **supabase_vault** (v0.3.1) - Sichere Speicherung sensibler Daten
## Migrations-Historie
Die Datenbank wurde über folgende Migrationen aufgebaut:
1. `20250819065657_create_profiles_table` - Erstellung der Profil-Tabelle
2. `20250819065714_create_decks_table` - Erstellung der Decks-Tabelle
3. `20250819065733_create_cards_table` - Erstellung der Cards-Tabelle
4. `20250819065806_enable_rls_and_policies` - Aktivierung von RLS und initiale Policies
5. `20250819081009_fix_profiles_insert_policy` - Korrektur der Profil-Insert-Policy
6. `20250819081105_update_all_rls_policies` - Aktualisierung aller RLS-Policies
7. `20250819091919_fix_decks_rls_policy` - Korrektur der Decks-RLS-Policy
8. `20250819092831_fix_decks_foreign_key` - Korrektur des Foreign Keys in Decks
9. `20250819100624_fix_cards_rls_policies` - Korrektur der Cards-RLS-Policies
## Datenmodell-Beziehungen
```
auth.users
↓ (1:1)
profiles
↓ (1:n)
decks
↓ (1:n)
cards
```
## Verwendung in der Anwendung
### Supabase Client-Konfiguration
Die Anwendung verwendet den Supabase JavaScript Client (`utils/supabase.ts`) mit:
- **AsyncStorage** für Session-Persistenz
- Umgebungsvariablen für Konfiguration:
- `EXPO_PUBLIC_SUPABASE_URL`
- `EXPO_PUBLIC_SUPABASE_ANON_KEY`
### Typische Operationen
1. **Benutzerregistrierung**: Erstellt automatisch einen Eintrag in `profiles`
2. **Deck-Erstellung**: Benutzer können mehrere Decks mit verschiedenen Einstellungen erstellen
3. **Karten-Management**: Karten können verschiedene Typen haben und AI-generiert sein
4. **Öffentliche Decks**: Decks können als öffentlich markiert und von anderen eingesehen werden
## Best Practices
1. **Immer RLS verwenden**: Alle Datenzugriffe werden durch RLS-Policies geschützt
2. **JSONB für Flexibilität**: `content`, `settings`, `preferences` und `metadata` nutzen JSONB für flexible Datenstrukturen
3. **UUID als Primary Keys**: Verwendung von UUIDs für bessere Skalierbarkeit und Sicherheit
4. **Timestamps**: Automatisches Tracking von Erstellung und Aktualisierung
5. **Soft Deletes**: Können über das `metadata` JSONB-Feld implementiert werden
## Optimierungsmöglichkeiten
1. **Indizes**: Zusätzliche Indizes auf häufig genutzte Spalten (z.B. `user_id`, `is_public`)
2. **Partitionierung**: Bei großen Datenmengen könnte Tabellen-Partitionierung sinnvoll sein
3. **Materialized Views**: Für komplexe Abfragen öffentlicher Decks
4. **Full-Text Search**: Implementierung mit PostgreSQL FTS für Deck- und Kartensuche
5. **Caching**: Redis-Integration für häufig abgerufene öffentliche Inhalte

View file

@ -0,0 +1,237 @@
# Expo SDK 54 Upgrade Guide für Manadeck
## 🎯 Übersicht
Dieses Dokument beschreibt die notwendigen Schritte für das Upgrade von Manadeck von Expo SDK 53 auf SDK 54.
**Aktueller Stand:**
- Expo SDK: 53.0.20
- React Native: 0.79.5
- React: 19.0.0
- Node.js: 20.19.0 (❌ Update erforderlich auf 20.19.4+)
## 📋 Voraussetzungen
### System-Anforderungen
- **Node.js:** Minimum 20.19.4 (aktuell: 20.19.0)
- **Xcode:** 16.1 oder höher (empfohlen: 16.0)
- **EAS CLI:** Neueste Version
### Vor dem Upgrade
```bash
# EAS CLI aktualisieren
npm install -g eas-cli@latest
# Node.js auf 20.19.4+ updaten (via nvm oder direkt)
nvm install 20.19.4
nvm use 20.19.4
```
## 🚨 Breaking Changes & Wichtige Änderungen
### 1. **expo-av wird entfernt**
- ⚠️ **Betroffen:** Ja, Projekt nutzt `expo-av@^15.1.7`
- **Migration:** Zu `expo-video` oder anderen Audio-Libraries wechseln
- **Deadline:** Muss vor SDK 55 migriert werden
### 2. **React Native 0.81 & React 19.1**
- Upgrade von RN 0.79.5 → 0.81
- React bleibt bei 19.x (19.0.0 → 19.1)
### 3. **Android Edge-to-Edge**
- Wird automatisch aktiviert und kann nicht deaktiviert werden
- UI-Anpassungen könnten nötig sein
### 4. **New Architecture**
- SDK 54 ist die letzte Version mit Legacy Architecture Support
- Migration zur New Architecture wird dringend empfohlen
## 📦 Betroffene Dependencies
### Muss aktualisiert werden:
```json
{
"expo": "^53.0.20" → "^54.0.0",
"expo-av": "^15.1.7" → Migration erforderlich!,
"expo-constants": "~17.1.4" → "~18.0.0",
"expo-dev-client": "~5.2.4" → "~6.0.0",
"expo-dev-launcher": "^5.0.17" → "^6.0.0",
"expo-file-system": "^18.1.11" → "^19.0.0",
"expo-image-picker": "^16.1.4" → "^17.0.0",
"expo-linking": "~7.1.4" → "~8.0.0",
"expo-router": "~5.1.4" → "~6.0.0",
"expo-speech": "^13.1.7" → "^14.0.0",
"expo-status-bar": "~2.2.3" → "~3.0.0",
"expo-symbols": "~0.4.5" → "~1.0.0",
"expo-system-ui": "~5.0.6" → "~6.0.0",
"expo-web-browser": "~14.2.0" → "~15.0.0",
"expo-build-properties": "~0.14.8" → "~0.15.0",
"expo-updates": "~0.28.17" → "~0.29.0",
"react-native": "0.79.5" → "0.81.0",
"react": "19.0.0" → "19.1.0",
"react-dom": "19.0.0" → "19.1.0"
}
```
### Kompatibilität prüfen:
- `react-native-gesture-handler`
- `react-native-reanimated`
- `react-native-screens`
- `react-native-safe-area-context`
## 🔧 Upgrade-Schritte
### Schritt 1: Backup erstellen
```bash
# Git Status prüfen
git status
# Alle Änderungen committen
git add .
git commit -m "Pre SDK 54 upgrade backup"
# Branch für Upgrade erstellen
git checkout -b upgrade/expo-sdk-54
```
### Schritt 2: Dependencies aktualisieren
```bash
# Expo SDK 54 installieren
npx expo install expo@^54.0.0 --fix
# Dies aktualisiert automatisch alle Expo-Packages
```
### Schritt 3: expo-av Migration
**Option A: Zu expo-video wechseln (für Video)**
```bash
npm uninstall expo-av
npx expo install expo-video
```
**Option B: Zu expo-audio wechseln (für Audio)**
```bash
npm uninstall expo-av
npx expo install expo-audio
```
**Code-Anpassungen erforderlich in:**
- Components die Audio/Video verwenden
- Imports anpassen
### Schritt 4: app.json Anpassungen
```json
{
"expo": {
// iOS Deployment Target anpassen
"plugins": [
[
"expo-build-properties",
{
"ios": {
"deploymentTarget": "16.0" // von 15.1 auf 16.0
},
"android": {
"targetSdkVersion": 36, // Neu für Android 16
"compileSdkVersion": 36
}
}
]
],
// New Architecture aktivieren (optional aber empfohlen)
"experiments": {
"typedRoutes": true,
"tsconfigPaths": true,
"newArchEnabled": true // Neu hinzufügen
}
}
}
```
### Schritt 5: Native Directories neu generieren
```bash
# Alte native Directories löschen (falls vorhanden)
rm -rf ios android
# Neu generieren mit SDK 54
npx expo prebuild --clean
```
### Schritt 6: Development Build erstellen
```bash
# Neuen Development Build erstellen
eas build --profile development --platform ios
eas build --profile development --platform android
```
### Schritt 7: Tests durchführen
```bash
# Lint ausführen
npm run lint
# Format prüfen
npm run format
# App auf verschiedenen Plattformen testen
npm run ios
npm run android
npm run web
```
## ✅ Checkliste
- [ ] Node.js auf 20.19.4+ aktualisiert
- [ ] EAS CLI auf neueste Version aktualisiert
- [ ] Backup/Branch erstellt
- [ ] Dependencies mit `npx expo install expo@^54.0.0 --fix` aktualisiert
- [ ] expo-av Migration durchgeführt
- [ ] app.json angepasst (iOS deployment target, Android SDK)
- [ ] Native Directories neu generiert
- [ ] Development Builds erstellt
- [ ] App auf allen Plattformen getestet
- [ ] UI auf Android Edge-to-Edge geprüft
- [ ] Alle Features funktionieren
## 🐛 Bekannte Probleme & Lösungen
### Problem 1: Metro Cache
```bash
# Metro Cache löschen
npx expo start -c
```
### Problem 2: Pod Installation Fehler (iOS)
```bash
cd ios
pod deintegrate
pod install --repo-update
```
### Problem 3: Android Build Fehler
```bash
cd android
./gradlew clean
cd ..
npx expo run:android
```
## 📚 Weitere Ressourcen
- [Offizielle Expo SDK 54 Changelog](https://expo.dev/changelog/sdk-54)
- [Expo Upgrade Guide](https://docs.expo.dev/workflow/upgrading-expo-sdk-walkthrough/)
- [React Native 0.81 Release Notes](https://reactnative.dev/blog)
- [New Architecture Guide](https://docs.expo.dev/guides/new-architecture/)
## 🆘 Support
Bei Problemen während des Upgrades:
1. [Expo Discord](https://chat.expo.dev)
2. [Expo GitHub Issues](https://github.com/expo/expo/issues)
3. [Stack Overflow](https://stackoverflow.com/questions/tagged/expo)
---
**Letzte Aktualisierung:** 24. September 2025
**Erstellt für:** Manadeck Projekt
**SDK Version:** 53 → 54

View file

@ -0,0 +1,255 @@
# Upgrade-Anleitung: Expo SDK 53 zu SDK 54
## Übersicht
Diese Anleitung beschreibt den Upgrade-Prozess von Expo SDK 53 auf SDK 54 für das Manadeck-Projekt.
## Wichtige Änderungen in SDK 54
### Hauptversionen
- **React Native**: 0.79.5 → 0.81.x
- **React**: 19.0.0 → 19.1.x
- **Reanimated**: v3 → v4 (optional)
- **Android API**: Unterstützung für API 36
- **iOS**: Unterstützung für iOS 26
- **Node.js**: Mindestens 20.19.x erforderlich
### Breaking Changes
1. **Legacy Architecture**
- SDK 54 ist die letzte Version mit Legacy Architecture Support
- Ab SDK 55 wird nur noch die New Architecture unterstützt
2. **File System API**
- Die neue expo-file-system API ist jetzt Standard
- Legacy API unter `expo-file-system/legacy` verfügbar
3. **JSC Support**
- React Native 0.81 hat keine eingebaute JSC-Unterstützung mehr
- Hermes ist jetzt die Standard-JavaScript-Engine
4. **Deprecations**
- `expo-av` wird in SDK 55 entfernt (Migration zu `expo-audio` und `expo-video` empfohlen)
- React Native's `<SafeAreaView>` ist veraltet
## Upgrade-Schritte
### 1. Vorbereitung
Erstelle ein Backup deines Projekts:
```bash
git add .
git commit -m "Backup vor SDK 54 Upgrade"
git branch backup-sdk-53
```
### 2. Dependencies aktualisieren
```bash
# Expo SDK 54 installieren
npm install expo@^54.0.0
# Alle Dependencies auf kompatible Versionen upgraden
npx expo install --fix
# Probleme überprüfen
npx expo-doctor@latest
```
### 3. React und React Native aktualisieren
Da SDK 54 React Native 0.81 und React 19.1 benötigt:
```bash
npm install react@19.1.0 react-native@0.81.x
```
### 4. Spezifische Package-Updates für Manadeck
Basierend auf den aktuellen Dependencies:
```bash
# React Navigation (falls Updates verfügbar)
npx expo install @react-navigation/native
# Reanimated (optional auf v4, oder bei v3 bleiben)
# Für v4 (nur mit New Architecture):
npx expo install react-native-reanimated@~4.0.0
# Oder bei v3 bleiben (mit Legacy Architecture):
npx expo install react-native-reanimated@~3.17.4
# Weitere Expo-Packages aktualisieren
npx expo install \
@expo/vector-icons \
expo-av \
expo-constants \
expo-dev-client \
expo-file-system \
expo-image-picker \
expo-linking \
expo-router \
expo-speech \
expo-status-bar \
expo-symbols \
expo-system-ui \
expo-web-browser \
expo-build-properties \
expo-updates
# React Native Community Packages
npx expo install \
@react-native-async-storage/async-storage \
react-native-gesture-handler \
react-native-safe-area-context \
react-native-screens \
react-native-svg
```
### 5. File System API Migration (falls verwendet)
Falls das Projekt `expo-file-system` nutzt:
**Quick Migration (Legacy API behalten):**
```typescript
// Alt:
import * as FileSystem from 'expo-file-system';
// Neu:
import * as FileSystem from 'expo-file-system/legacy';
```
**Oder zur neuen API migrieren:**
```typescript
// Neue API verwenden
import * as FileSystem from 'expo-file-system';
```
### 6. Native Projekte aktualisieren
**Wenn Continuous Native Generation verwendet wird:**
```bash
# Alte native Verzeichnisse löschen
rm -rf android ios
# Neu generieren
npx expo prebuild --clean
```
**Wenn eigene native Projekte vorhanden sind:**
```bash
# iOS Pods aktualisieren
cd ios && pod install && cd ..
# Native Änderungen prüfen
# Siehe: https://docs.expo.dev/bare/upgrade/
```
### 7. EAS Build Configuration
Update `eas.json` falls nötig:
```json
{
"build": {
"development": {
"node": "20.19.1"
},
"preview": {
"node": "20.19.1"
},
"production": {
"node": "20.19.1"
}
}
}
```
### 8. Testing
```bash
# Cache löschen und neu starten
npx expo start --clear
# Auf verschiedenen Plattformen testen
npm run ios
npm run android
npm run web
# Linting und Formatierung
npm run lint
npm run format
# Development Build erstellen
npm run build:dev
```
## Migrations-Checkliste
- [ ] Backup erstellt
- [ ] Dependencies aktualisiert
- [ ] `npx expo-doctor` erfolgreich
- [ ] File System Imports geprüft/migriert
- [ ] Native Projekte aktualisiert
- [ ] Node.js Version ≥ 20.19.x
- [ ] Development Build funktioniert
- [ ] iOS Build funktioniert
- [ ] Android Build funktioniert
- [ ] Web Build funktioniert
- [ ] Alle Features getestet
## Troubleshooting
### Problem: Metro Bundler Fehler
```bash
# Metro Cache löschen
npx expo start --clear
rm -rf node_modules/.cache/metro
```
### Problem: iOS Build schlägt fehl
- Xcode 16.1 oder höher erforderlich
- iOS Deployment Target prüfen
### Problem: Reanimated Fehler
- Bei Problemen mit v4: Bei v3 bleiben
- Babel Config prüfen (babel-preset-expo handled das automatisch)
### Problem: TypeScript Fehler
```bash
# TypeScript neu konfigurieren
npx expo customize tsconfig.json
```
## Rollback bei Problemen
Falls das Upgrade fehlschlägt:
```bash
git checkout backup-sdk-53
npm install
```
## Weiterführende Ressourcen
- [Expo SDK 54 Changelog](https://expo.dev/changelog/sdk-54)
- [React Native 0.81 Release Notes](https://github.com/facebook/react-native/releases/tag/v0.81.0)
- [Expo Upgrade Guide](https://docs.expo.dev/workflow/upgrading-expo-sdk-walkthrough/)
- [Native Project Upgrade Helper](https://docs.expo.dev/bare/upgrade/)
## Nächste Schritte nach dem Upgrade
1. **New Architecture aktivieren** (empfohlen für SDK 54):
```bash
npx expo prebuild --clean
```
2. **expo-av Migration** planen (wird in SDK 55 entfernt):
- Migration zu `expo-audio` für Audio
- Migration zu `expo-video` für Video
3. **Performance-Optimierungen** mit Reanimated v4 (falls New Architecture)
## Support
Bei Problemen:
- [Expo Discord](https://chat.expo.dev)
- [GitHub Issues](https://github.com/expo/expo/issues)
- [Stack Overflow](https://stackoverflow.com/questions/tagged/expo)

View file

@ -0,0 +1,501 @@
# Manadeck Frontend-Komponenten Plan
## Architektur-Übersicht
### Tech Stack
- **Framework**: React Native mit Expo
- **Navigation**: Expo Router (File-based routing)
- **Styling**: NativeWind (Tailwind CSS für React Native)
- **State Management**: Zustand
- **Backend**: Supabase (Auth, Database, Storage)
- **TypeScript**: Für Type Safety
## Navigation Struktur
```
app/
├── (auth)/ # Auth Stack (Nicht eingeloggt)
│ ├── _layout.tsx # Auth Layout
│ ├── login.tsx # Login Screen
│ ├── register.tsx # Registrierung
│ └── forgot-password.tsx # Passwort zurücksetzen
├── (tabs)/ # Hauptnavigation (Tab Bar)
│ ├── _layout.tsx # Tab Layout
│ ├── home.tsx # Home/Dashboard
│ ├── decks.tsx # Meine Decks
│ ├── explore.tsx # Öffentliche Decks entdecken
│ ├── learn.tsx # Aktives Lernen
│ └── profile.tsx # Profil & Einstellungen
├── deck/
│ ├── [id].tsx # Deck Detail View
│ ├── [id]/
│ │ ├── edit.tsx # Deck bearbeiten
│ │ ├── cards.tsx # Karten verwalten
│ │ └── study.tsx # Lernmodus starten
│ └── create.tsx # Neues Deck erstellen
├── card/
│ ├── [id].tsx # Karte Detail View
│ ├── create.tsx # Neue Karte erstellen
│ └── edit/[id].tsx # Karte bearbeiten
├── study/
│ ├── session/[deckId].tsx # Aktive Lernsession
│ ├── results.tsx # Lernergebnisse
│ └── settings.tsx # Lerneinstellungen
└── _layout.tsx # Root Layout
```
## Core Komponenten
### 1. Authentifizierung
#### AuthForm
```typescript
components/auth/AuthForm.tsx
- E-Mail/Passwort Eingabe
- Social Login Buttons
- Validierung
- Loading States
```
#### AuthGuard
```typescript
components/auth/AuthGuard.tsx
- Schützt Routen
- Redirect zu Login
- Session Check
```
### 2. Layout Komponenten
#### AppHeader
```typescript
components/layout/AppHeader.tsx
- Titel
- Navigation Back Button
- Action Buttons
- Search Bar (optional)
```
#### TabBar
```typescript
components/layout/TabBar.tsx
- Custom Tab Bar
- Icons mit Badges
- Aktive Tab Animation
```
#### FloatingActionButton
```typescript
components/layout/FloatingActionButton.tsx
- Schnellzugriff (Deck/Karte erstellen)
- Animierte Menü-Expansion
```
### 3. Deck Komponenten
#### DeckCard
```typescript
components/deck/DeckCard.tsx
Props:
- deck: Deck
- onPress: () => void
- showProgress?: boolean
- isCompact?: boolean
Features:
- Cover Image
- Titel & Beschreibung
- Tags
- Karten-Anzahl
- Lernfortschritt (optional)
- Public/Private Badge
```
#### DeckGrid
```typescript
components/deck/DeckGrid.tsx
- Responsive Grid Layout
- Lazy Loading
- Pull-to-Refresh
- Empty State
```
#### DeckForm
```typescript
components/deck/DeckForm.tsx
- Titel, Beschreibung
- Cover Image Upload
- Tag-Auswahl
- Öffentlich/Privat Toggle
- Erweiterte Einstellungen
```
#### DeckFilters
```typescript
components/deck/DeckFilters.tsx
- Sortierung (Datum, Name, Fortschritt)
- Filter (Tags, Öffentlich, Favoriten)
- Suchleiste
```
### 4. Karten Komponenten
#### CardView
```typescript
components/card/CardView.tsx
Props:
- card: Card
- mode: 'view' | 'study' | 'edit'
- onFlip?: () => void
- showAnswer?: boolean
Unterstützt verschiedene Kartentypen:
- Text
- Flashcard (Vorder-/Rückseite)
- Quiz (Multiple Choice)
- Mixed (Rich Content)
```
#### CardEditor
```typescript
components/card/CardEditor.tsx
- Rich Text Editor
- Markdown Support
- Bild/Media Upload
- AI-Generierung
- Kartentyp-Auswahl
```
#### CardList
```typescript
components/card/CardList.tsx
- Sortierbare Liste
- Drag & Drop
- Batch-Aktionen
- Suche/Filter
```
#### CardSwiper
```typescript
components/card/CardSwiper.tsx
- Swipe-Gesten (Links/Rechts)
- Karten-Stack Animation
- Progress Indicator
```
### 5. Lern-Komponenten
#### StudySession
```typescript
components/study/StudySession.tsx
- Kartenansicht
- Antwort-Eingabe
- Bewertung (Einfach/Schwer)
- Timer
- Fortschrittsanzeige
```
#### ProgressTracker
```typescript
components/study/ProgressTracker.tsx
- Lernstatistiken
- Streak-Anzeige
- Charts/Graphs
- Achievements
```
#### StudyModeSelector
```typescript
components/study/StudyModeSelector.tsx
Modi:
- Durchblättern
- Quiz
- Schreiben
- Spaced Repetition
```
### 6. Profil Komponenten
#### ProfileHeader
```typescript
components/profile/ProfileHeader.tsx
- Avatar
- Username/Display Name
- Bio
- Statistiken
- Edit Button
```
#### ProfileStats
```typescript
components/profile/ProfileStats.tsx
- Anzahl Decks
- Gelernte Karten
- Lernstreak
- Achievements
```
#### SettingsMenu
```typescript
components/profile/SettingsMenu.tsx
- Account-Einstellungen
- Benachrichtigungen
- Datenschutz
- App-Präferenzen
- Logout
```
### 7. Gemeinsame UI Komponenten
#### Button
```typescript
components/ui/Button.tsx
Varianten:
- primary, secondary, outline, ghost
- Größen: sm, md, lg
- Loading State
- Icons
```
#### Card
```typescript
components/ui/Card.tsx
- Container mit Shadow
- Padding Varianten
- Press Animation
```
#### Input
```typescript
components/ui/Input.tsx
- Text, Email, Password
- Label & Error
- Icons
- Clear Button
```
#### Modal
```typescript
components/ui/Modal.tsx
- Fullscreen oder Bottom Sheet
- Backdrop
- Swipe to Dismiss
```
#### Toast
```typescript
components/ui/Toast.tsx
- Success, Error, Info, Warning
- Auto-Dismiss
- Actions
```
#### EmptyState
```typescript
components/ui/EmptyState.tsx
- Illustration
- Titel & Beschreibung
- Action Button
```
#### LoadingSpinner
```typescript
components/ui/LoadingSpinner.tsx
- Verschiedene Größen
- Mit/ohne Text
- Overlay Option
```
#### SearchBar
```typescript
components/ui/SearchBar.tsx
- Debounced Input
- Clear Button
- Voice Input (optional)
```
## State Management (Zustand Stores)
### authStore
```typescript
store/authStore.ts
- user: User | null
- session: Session | null
- isLoading: boolean
- login(), logout(), register()
- updateProfile()
```
### deckStore
```typescript
store/deckStore.ts
- decks: Deck[]
- currentDeck: Deck | null
- isLoading: boolean
- fetchDecks(), createDeck(), updateDeck()
- deleteDeck(), toggleFavorite()
```
### cardStore
```typescript
store/cardStore.ts
- cards: Card[]
- currentCard: Card | null
- fetchCards(), createCard(), updateCard()
- deleteCard(), reorderCards()
```
### studyStore
```typescript
store/studyStore.ts
- activeSession: StudySession | null
- progress: Progress
- startSession(), endSession()
- submitAnswer(), nextCard()
- updateStatistics()
```
### uiStore
```typescript
store/uiStore.ts
- theme: 'light' | 'dark' | 'system'
- language: string
- notifications: boolean
- setTheme(), setLanguage()
```
## Utility Hooks
### useSupabase
```typescript
hooks/useSupabase.ts
- Wrapped Supabase Client
- Auto-Refresh Token
- Error Handling
```
### useAuth
```typescript
hooks/useAuth.ts
- Auth State
- Login/Logout Methods
- Session Management
```
### useOffline
```typescript
hooks/useOffline.ts
- Offline Detection
- Queue Actions
- Sync when Online
```
### useDebounce
```typescript
hooks/useDebounce.ts
- Debounced Values
- Für Suche/Filter
```
### useInfiniteScroll
```typescript
hooks/useInfiniteScroll.ts
- Pagination
- Load More
- Refresh
```
## Styling System
### Theme Configuration
```typescript
theme/
├── colors.ts # Farbpalette
├── spacing.ts # Spacing Scale
├── typography.ts # Font Sizes & Weights
└── shadows.ts # Shadow Presets
```
### NativeWind Classes
```css
/* Basis-Komponenten nutzen */
- Container: "flex-1 bg-white dark:bg-gray-900"
- Card: "bg-white dark:bg-gray-800 rounded-xl shadow-md p-4"
- Button: "bg-blue-500 px-4 py-2 rounded-lg"
- Text: "text-gray-900 dark:text-gray-100"
```
## Performance Optimierungen
1. **Lazy Loading**
- React.lazy() für Screens
- Virtualisierte Listen (FlashList)
- Image Lazy Loading
2. **Caching**
- React Query für API Calls
- AsyncStorage für Offline-Daten
- Image Cache
3. **Optimierte Renders**
- React.memo für schwere Komponenten
- useMemo/useCallback
- Zustand Selectors
4. **Bundle Size**
- Code Splitting
- Tree Shaking
- Asset Optimization
## Accessibility
- **Screen Reader Support**
- **Keyboard Navigation** (Web)
- **Touch Target Sizes** (min. 44x44)
- **Color Contrast** (WCAG AA)
- **Focus Indicators**
- **Semantic Labels**
## Testing Strategie
1. **Unit Tests**: Jest + React Testing Library
2. **Integration Tests**: Detox
3. **E2E Tests**: Maestro
4. **Visual Regression**: Percy
## Nächste Schritte
1. **Phase 1: Core Setup**
- Auth Flow implementieren
- Navigation Setup
- Basis UI Komponenten
2. **Phase 2: Deck Management**
- Deck CRUD Operationen
- Deck Liste & Detail Views
- Deck Sharing
3. **Phase 3: Karten System**
- Karten Editor
- Verschiedene Kartentypen
- Drag & Drop Sortierung
4. **Phase 4: Lern-Features**
- Study Session
- Fortschritts-Tracking
- Spaced Repetition
5. **Phase 5: Social Features**
- Öffentliche Decks
- Bewertungen
- Kommentare
6. **Phase 6: Premium Features**
- AI-Generierung
- Erweiterte Statistiken
- Offline Sync

View file

@ -0,0 +1,216 @@
# Manadeck - Nächste Implementierungsschritte
## 🎯 Aktueller Status
✅ **Phase 1 abgeschlossen:**
- Authentifizierung funktioniert (Login, Register, Logout)
- Basis UI-Komponenten erstellt
- Navigation mit Tab Bar implementiert
- Deck Store vorbereitet
- Profil-Erstellung bei Registrierung automatisiert
## 📋 Phase 2: Deck Management (Priorität: HOCH)
### 2.1 Deck Liste mit echten Daten
- [ ] Decks aus Datenbank in "Meine Decks" Tab anzeigen
- [ ] Deck-Karten mit tatsächlichen Daten
- [ ] Filter für Favoriten funktionsfähig machen
- [ ] Pull-to-Refresh implementieren
- [ ] Empty State nur wenn wirklich keine Decks vorhanden
### 2.2 Deck Detail View
- [ ] `/deck/[id].tsx` Screen erstellen
- [ ] Deck Informationen anzeigen (Titel, Beschreibung, Tags)
- [ ] Karten-Liste des Decks anzeigen
- [ ] Actions: Bearbeiten, Löschen, Favorit toggle
- [ ] Header mit Cover Image oder Gradient
### 2.3 Deck Bearbeitung
- [ ] `/deck/[id]/edit.tsx` Screen
- [ ] Deck-Informationen updaten
- [ ] Cover Image Upload (Supabase Storage)
- [ ] Tags Management
- [ ] Öffentlich/Privat Toggle
### 2.4 Deck Sharing & Discovery
- [ ] Öffentliche Decks in Explore Tab anzeigen
- [ ] Suche nach öffentlichen Decks
- [ ] Deck kopieren/forken Funktionalität
- [ ] Deck Statistiken (Views, Kopien)
## 📝 Phase 3: Karten-System (Priorität: HOCH)
### 3.1 Card CRUD Operationen
- [ ] Card Store erstellen (`cardStore.ts`)
- [ ] Karten zu Deck hinzufügen
- [ ] Karten bearbeiten/löschen
- [ ] Karten-Positionen speichern
### 3.2 Card Editor
- [ ] `/card/create.tsx` mit Deck-Kontext
- [ ] `/card/edit/[id].tsx` für Bearbeitung
- [ ] Rich Text Editor für Karten-Content
- [ ] Kartentyp-Auswahl:
- **Text**: Einfache Textkarte
- **Flashcard**: Vorder-/Rückseite
- **Quiz**: Multiple Choice
- **Mixed**: Kombiniert verschiedene Elemente
### 3.3 Card Management UI
- [ ] Karten-Liste in Deck Detail View
- [ ] Sortierung per Drag & Drop
- [ ] Bulk-Aktionen (Mehrere löschen/verschieben)
- [ ] Karten-Vorschau Modal
- [ ] Favoriten markieren
### 3.4 AI Integration (Optional)
- [ ] AI-Generierung für Karten
- [ ] Prompt-Templates für verschiedene Lernthemen
- [ ] Batch-Generierung mehrerer Karten
## 🎓 Phase 4: Lern-Features (Priorität: MITTEL)
### 4.1 Study Session
- [ ] `/study/session/[deckId].tsx` implementieren
- [ ] Karten-Navigation (Vor/Zurück)
- [ ] Flip-Animation für Flashcards
- [ ] Quiz-Antwort Validierung
- [ ] Session-Timer
### 4.2 Lernmodi
- [ ] **Browse Mode**: Einfaches Durchblättern
- [ ] **Practice Mode**: Mit Selbstbewertung
- [ ] **Quiz Mode**: Mit Punktzahl
- [ ] **Spaced Repetition**: Algorithmus implementieren
### 4.3 Fortschritts-Tracking
- [ ] Study Store für Lernfortschritt
- [ ] Karten als "gelernt" markieren
- [ ] Schwierigkeitsgrad pro Karte
- [ ] Wiederholungsintervalle berechnen
- [ ] Statistiken speichern (richtig/falsch)
### 4.4 Statistiken & Visualisierung
- [ ] Lernstatistik Dashboard
- [ ] Streak-Counter
- [ ] Fortschritts-Charts
- [ ] Zeitstatistiken
- [ ] Achievements/Badges
## 🔧 Phase 5: Optimierungen (Priorität: NIEDRIG)
### 5.1 Performance
- [ ] Lazy Loading für große Deck-Listen
- [ ] Image Optimization & Caching
- [ ] Offline-Support mit AsyncStorage
- [ ] Optimistische Updates
### 5.2 UX Verbesserungen
- [ ] Swipe-Gesten für Karten
- [ ] Haptic Feedback
- [ ] Sound-Effekte (optional)
- [ ] Dark Mode Support
- [ ] Animations & Transitions
### 5.3 Social Features
- [ ] Benutzerprofile erweitern
- [ ] Deck-Bewertungen
- [ ] Kommentare zu öffentlichen Decks
- [ ] Follower-System
- [ ] Deck-Empfehlungen
## 🚀 Phase 6: Premium Features (Zukunft)
### 6.1 Erweiterte AI Features
- [ ] GPT-4 Integration für bessere Karten
- [ ] Automatische Zusammenfassungen
- [ ] Lernpfad-Generierung
- [ ] Personalisierte Empfehlungen
### 6.2 Kollaboration
- [ ] Gemeinsame Decks
- [ ] Real-time Bearbeitung
- [ ] Gruppen-Lernsessions
- [ ] Lehrer-Schüler Modus
### 6.3 Export & Integration
- [ ] Anki Export/Import
- [ ] PDF Export
- [ ] CSV Import für Bulk-Karten
- [ ] API für Drittanbieter
## 🛠 Technische Schulden
### Refactoring Needs
- [ ] Error Boundaries hinzufügen
- [ ] Loading States vereinheitlichen
- [ ] Form Validation Library (react-hook-form)
- [ ] Tests schreiben (Jest + React Testing Library)
### Infrastructure
- [ ] CI/CD Pipeline setup
- [ ] Error Tracking (Sentry)
- [ ] Analytics Integration
- [ ] Performance Monitoring
## 📊 Priorisierung
### Sofort (Diese Woche)
1. Deck Liste mit echten Daten
2. Deck Detail View
3. Basis Card CRUD
### Kurzfristig (2-3 Wochen)
1. Card Editor mit allen Typen
2. Basis Study Mode
3. Einfaches Fortschritts-Tracking
### Mittelfristig (1-2 Monate)
1. Spaced Repetition
2. Statistiken Dashboard
3. Öffentliche Decks & Suche
### Langfristig (3+ Monate)
1. AI Features
2. Social Features
3. Premium Funktionen
## 🎯 MVP Definition
**Minimum Viable Product beinhaltet:**
- ✅ User Auth (FERTIG)
- ⏳ Deck CRUD (IN ARBEIT)
- ⏳ Card CRUD
- ⏳ Basis Study Mode
- ⏳ Einfacher Fortschritt
**Nice-to-have für MVP:**
- Öffentliche Decks
- Basis-Statistiken
- Dark Mode
## 📝 Nächste konkrete Schritte
1. **Deck Liste verbessern** (`app/(tabs)/decks.tsx`)
- useEffect mit fetchDecks() beim Mount
- Echte Deck-Daten anzeigen
- Loading & Error States
2. **Deck Detail Screen** (`app/deck/[id].tsx`)
- Route Parameter handling
- Deck Daten laden
- Card Liste anzeigen
3. **Card Store** (`store/cardStore.ts`)
- CRUD Operationen
- Sortierung
- Relationship zu Decks
4. **Card Components** (`components/card/`)
- CardEditor
- CardView
- CardList
5. **Study Mode Basis** (`app/study/session/[deckId].tsx`)
- Karten durchblättern
- Flip Animation
- Progress tracking

View file

@ -0,0 +1,378 @@
# Phase 3: Karten-System - Detaillierter Implementierungsplan
## 🎯 Ziel von Phase 3
Ein vollständiges Karten-Management-System mit verschiedenen Kartentypen, Editor-Funktionalität und intuitive Benutzeroberfläche.
## 📋 Phase 3.1: Card Store & Datenmodell (Priorität: HOCH)
### Card Store erstellen (`store/cardStore.ts`)
```typescript
interface Card {
id: string;
deck_id: string;
position: number;
title?: string;
content: CardContent; // JSON für verschiedene Kartentypen
card_type: 'text' | 'flashcard' | 'quiz' | 'mixed';
ai_model?: string;
ai_prompt?: string;
version: number;
is_favorite: boolean;
created_at: string;
updated_at: string;
}
```
### CRUD Operationen
- [x] `fetchCards(deckId)` - Alle Karten eines Decks laden
- [x] `fetchCard(id)` - Einzelne Karte laden
- [x] `createCard(deckId, cardData)` - Neue Karte erstellen
- [x] `updateCard(id, updates)` - Karte bearbeiten
- [x] `deleteCard(id)` - Karte löschen
- [x] `reorderCards(deckId, cardIds[])` - Reihenfolge ändern
- [x] `duplicateCard(id)` - Karte duplizieren
- [x] `toggleFavorite(id)` - Favorit markieren
### Content-Strukturen für verschiedene Kartentypen
```typescript
// Text Card
interface TextContent {
text: string;
formatting?: FormattingOptions;
}
// Flashcard
interface FlashcardContent {
front: string;
back: string;
hint?: string;
}
// Quiz Card
interface QuizContent {
question: string;
options: string[];
correct_answer: number;
explanation?: string;
}
// Mixed Card
interface MixedContent {
blocks: ContentBlock[];
}
type ContentBlock =
| { type: 'text'; data: { text: string } }
| { type: 'image'; data: { url: string; caption?: string } }
| { type: 'quiz'; data: QuizContent }
| { type: 'flashcard'; data: FlashcardContent };
```
## 📝 Phase 3.2: Card Create Screen (Priorität: HOCH)
### Screen: `/card/create.tsx`
- **Query Parameter**: `deckId` (aus Deck Detail View)
- **Kartentyp-Auswahl**: Radio/Segmented Control
- **Dynamischer Editor**: Je nach Kartentyp
- **Vorschau-Modus**: Live-Preview der Karte
- **Speichern & Fortfahren**: Direkt nächste Karte erstellen
### UI-Flow
1. **Kartentyp wählen** (Text, Flashcard, Quiz, Mixed)
2. **Content eingeben** (je nach Typ unterschiedliche Felder)
3. **Vorschau anzeigen** (wie Karte später aussieht)
4. **Speichern** oder **Speichern & Neue erstellen**
### Kartentyp-spezifische Eingaben
#### Text Card
- **Titel** (optional)
- **Rich Text Editor** für Hauptinhalt
- **Formatting**: Bold, Italic, Listen, Links
#### Flashcard
- **Vorderseite** (Frage/Begriff)
- **Rückseite** (Antwort/Definition)
- **Hinweis** (optional)
- **Beide Seiten**: Rich Text Support
#### Quiz Card
- **Frage**
- **2-6 Antwortoptionen** (dynamisch hinzufügbar)
- **Richtige Antwort markieren**
- **Erklärung** (optional, nach Antwort gezeigt)
#### Mixed Card (Erweitert)
- **Block-basierter Editor**
- **Drag & Drop** für Reihenfolge
- **Verschiedene Block-Typen**: Text, Bild, Quiz, Flashcard
## 🔧 Phase 3.3: Card Edit Screen (Priorität: HOCH)
### Screen: `/card/edit/[id].tsx`
- **Bestehende Daten laden** und vorausfüllen
- **Kartentyp ändern** (mit Warnung vor Datenverlust)
- **Versionierung**: Änderungen tracken
- **Autosave**: Entwürfe automatisch speichern
### Features
- **Änderungshistorie** anzeigen
- **Rückgängig/Wiederholen** Funktionalität
- **Kollaboration** (für später): Wer hat was geändert
## 📱 Phase 3.4: Card Liste in Deck Detail View (Priorität: HOCH)
### Integration in `/deck/[id].tsx`
```typescript
// Ersetze "Karten-Anzeige kommt in Phase 3" mit:
<CardList
deckId={currentDeck.id}
isCompact={true}
showActions={false}
limit={5}
/>
<TouchableOpacity onPress={() => router.push(`/deck/${id}/cards`)}>
<Text>Alle {currentDeck.card_count} Karten anzeigen</Text>
</TouchableOpacity>
```
### CardList Komponente (`components/card/CardList.tsx`)
- **Virtualisierte Liste** für Performance
- **Kompakte Ansicht** vs **Detailansicht**
- **Sortierung**: Position, Erstellung, Alphabet, Typ
- **Filter**: Nach Kartentyp, Favoriten, Lernstatus
- **Swipe-Aktionen**: Bearbeiten, Löschen, Favorit
### Card Item Komponente
```typescript
<CardItem
card={card}
isCompact={true}
onPress={() => router.push(`/card/${card.id}`)}
onEdit={() => router.push(`/card/edit/${card.id}`)}
onDelete={() => handleDelete(card.id)}
onToggleFavorite={() => toggleFavorite(card.id)}
/>
```
## 🎨 Phase 3.5: Card View Komponente (Priorität: MITTEL)
### Komponente: `components/card/CardView.tsx`
- **Multi-Mode Support**:
- `view`: Nur anzeigen
- `study`: Für Lernsessions
- `edit`: Inline-Bearbeitung
- `preview`: Während Erstellung
### Kartentyp-spezifische Renderer
```typescript
const CardRenderer = ({ card, mode }) => {
switch(card.card_type) {
case 'text':
return <TextCardView content={card.content} mode={mode} />;
case 'flashcard':
return <FlashcardView content={card.content} mode={mode} />;
case 'quiz':
return <QuizCardView content={card.content} mode={mode} />;
case 'mixed':
return <MixedCardView content={card.content} mode={mode} />;
}
};
```
### Interaktivität
- **Flashcard**: Flip-Animation
- **Quiz**: Antworten auswählen + Feedback
- **Text**: Scrolling für lange Inhalte
- **Mixed**: Verschiedene Interaktionen je Block
## 🛠 Phase 3.6: Card Management Screen (Priorität: NIEDRIG)
### Screen: `/deck/[id]/cards.tsx`
- **Vollständige Kartenliste** des Decks
- **Bulk-Aktionen**: Mehrere Karten auswählen
- **Drag & Drop Sortierung**
- **Export/Import** Funktionen
- **Erweiterte Filter** und Suche
### Bulk-Aktionen
- **Mehrfach-Auswahl**: Checkboxes
- **Löschen**: Mehrere Karten auf einmal
- **Verschieben**: In anderes Deck
- **Duplizieren**: Kopien erstellen
- **Favoriten**: Mehrere markieren
- **Export**: Als CSV/JSON
### Sortierung & Filter
- **Drag & Drop**: Position manuell ändern
- **Auto-Sort**: Nach Alphabet, Datum, Typ
- **Filter**:
- Kartentyp (Text, Flashcard, Quiz, Mixed)
- Status (Favoriten, Kürzlich bearbeitet)
- Lernfortschritt (Gelernt, Schwierig, Neu)
## 🎨 UI/UX Komponenten-Plan
### 1. Rich Text Editor (`components/ui/RichTextEditor.tsx`)
```typescript
<RichTextEditor
value={content}
onChange={setContent}
toolbar={['bold', 'italic', 'list', 'link']}
placeholder="Gib deinen Text ein..."
maxLength={1000}
/>
```
### 2. Card Type Selector (`components/card/CardTypeSelector.tsx`)
```typescript
<CardTypeSelector
selectedType={cardType}
onTypeChange={setCardType}
showDescriptions={true}
/>
```
### 3. Quiz Option Builder (`components/card/QuizOptionBuilder.tsx`)
```typescript
<QuizOptionBuilder
options={options}
correctAnswer={correctAnswer}
onOptionsChange={setOptions}
onCorrectAnswerChange={setCorrectAnswer}
minOptions={2}
maxOptions={6}
/>
```
### 4. Card Preview (`components/card/CardPreview.tsx`)
```typescript
<CardPreview
card={previewCard}
mode="preview"
showFlipButton={cardType === 'flashcard'}
/>
```
## 📊 Datenbank-Optimierungen
### Indizes für Performance
```sql
-- Index für schnelle Deck-Card Abfragen
CREATE INDEX cards_deck_id_position_idx ON cards(deck_id, position);
-- Index für Kartentyp-Filter
CREATE INDEX cards_deck_type_idx ON cards(deck_id, card_type);
-- Index für Favoriten
CREATE INDEX cards_favorite_idx ON cards(deck_id) WHERE is_favorite = true;
```
### Trigger für Auto-Position
```sql
-- Automatische Position-Zuweisung bei INSERT
CREATE OR REPLACE FUNCTION set_card_position()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.position IS NULL THEN
NEW.position := COALESCE(
(SELECT MAX(position) + 1 FROM cards WHERE deck_id = NEW.deck_id),
1
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_card_position_trigger
BEFORE INSERT ON cards
FOR EACH ROW
EXECUTE FUNCTION set_card_position();
```
## 🧪 Testing-Strategie
### Unit Tests
- [ ] Card Store CRUD Operationen
- [ ] Card Content Validierung
- [ ] Kartentyp-spezifische Renderer
### Integration Tests
- [ ] Card Create Flow (alle Typen)
- [ ] Card Edit Flow
- [ ] Bulk-Aktionen
- [ ] Sortierung & Filter
### E2E Tests
- [ ] Vollständiger Card-Lifecycle
- [ ] Deck → Card → Study Flow
- [ ] Performance mit vielen Karten
## ⚡ Performance-Überlegungen
### Optimierungen
1. **Virtualized Lists**: Für große Kartenmengen
2. **Lazy Loading**: Karten-Content nur bei Bedarf
3. **Image Optimization**: Automatische Komprimierung
4. **Debounced Search**: Verzögerte Suche bei Eingabe
5. **Caching**: Häufig genutzte Karten im Memory
### Bundle Size
- **Code Splitting**: Rich Text Editor nur bei Bedarf laden
- **Tree Shaking**: Ungenutzte Kartentyp-Komponenten entfernen
## 🚀 Nächste konkrete Schritte
### Sofort starten (Diese Woche)
1. **Card Store** erstellen (`store/cardStore.ts`)
2. **Basic Card Create Screen** (`/card/create.tsx`)
3. **Einfache Card View** für Text-Karten
4. **Card Liste** in Deck Detail einbauen
### Kurzfristig (1-2 Wochen)
1. **Flashcard Support** hinzufügen
2. **Quiz Card** implementieren
3. **Card Edit Screen** erstellen
4. **Basis Card Management**
### Mittelfristig (2-3 Wochen)
1. **Rich Text Editor** integrieren
2. **Mixed Cards** (Block-Editor)
3. **Drag & Drop Sortierung**
4. **Bulk-Aktionen**
## 💡 Nice-to-Have Features (Zukunft)
### AI Integration
- **Auto-Generierung**: Flashcards aus Text
- **Quiz-Generierung**: Fragen automatisch erstellen
- **Content-Verbesserung**: Grammatik & Stil-Tipps
### Kollaboration
- **Gemeinsame Bearbeitung**: Real-time Editing
- **Kommentare**: An Karten anhängen
- **Änderungshistorie**: Wer hat was geändert
### Import/Export
- **Anki Import**: .apkg Dateien
- **CSV Import**: Bulk-Import von Spreadsheets
- **Markdown Export**: Für Dokumentation
- **PDF Export**: Zum Ausdrucken
## 🎯 Erfolgskriterien für Phase 3
✅ **MVP erreicht wenn:**
- Alle 4 Kartentypen erstell- und bearbeitbar
- Karten in Deck Detail View sichtbar
- Basis-Sortierung funktioniert
- Performance mit 100+ Karten
🎖 **Excellent erreicht wenn:**
- Rich Text Editor funktioniert
- Drag & Drop Sortierung
- Bulk-Aktionen verfügbar
- Responsive Design perfekt
---
**Empfehlung**: Starten wir mit Phase 3.1 (Card Store) und 3.2 (Card Create) parallel, da diese die Grundlage für alles andere bilden!

View file

@ -0,0 +1,65 @@
-- Create System User for Sample Decks
-- This script creates a system user that can be used for sample/public decks
-- Insert system user into auth.users table
INSERT INTO auth.users (
id,
aud,
role,
email,
encrypted_password,
email_confirmed_at,
raw_app_meta_data,
raw_user_meta_data,
created_at,
updated_at,
confirmation_token,
email_change,
email_change_token_new,
recovery_token
) VALUES (
'00000000-0000-0000-0000-000000000001',
'authenticated',
'authenticated',
'system@manadeck.app',
'$2a$10$dummyhashforpassword', -- Dummy encrypted password
now(),
'{"provider": "system", "providers": ["system"]}',
'{"display_name": "Manadeck System", "is_system_user": true}',
now(),
now(),
'',
'',
'',
''
) ON CONFLICT (id) DO NOTHING; -- Don't fail if user already exists
-- Create corresponding profile for the system user
INSERT INTO public.profiles (
id,
username,
display_name,
bio,
preferences,
created_at,
updated_at
) VALUES (
'00000000-0000-0000-0000-000000000001',
'manadeck_system',
'Manadeck System',
'System account for managing public sample decks and community content.',
'{"is_system_account": true, "can_create_public_decks": true}',
now(),
now()
) ON CONFLICT (id) DO NOTHING;
-- Verify the system user was created
SELECT
u.id,
u.email,
u.email_confirmed_at IS NOT NULL as email_confirmed,
p.username,
p.display_name
FROM auth.users u
LEFT JOIN public.profiles p ON u.id = p.id
WHERE u.id = '00000000-0000-0000-0000-000000000001';

View file

@ -0,0 +1,168 @@
-- Sample Deck: English for Beginners (Englisch für Anfänger)
-- This script creates a sample public deck for German speakers learning English
-- Uses a system placeholder UUID that can be updated later
-- Insert the deck with a placeholder system user ID
INSERT INTO public.decks (
id,
user_id,
title,
description,
is_public,
tags,
metadata,
created_at,
updated_at
) VALUES (
'b1c2d3e4-f5a6-7890-bcde-f01234567890',
'00000000-0000-0000-0000-000000000001', -- Placeholder system user
'English for Beginners',
'Essential English vocabulary for German speakers. Perfect for A1-A2 level with everyday words and phrases.',
true,
ARRAY['Languages', 'English', 'Beginner', 'A1', 'A2', 'Vocabulary'],
'{
"category": "Sprachen",
"difficulty": "Anfänger",
"target_audience": "Deutschsprachige Englischlernende A1-A2",
"estimated_time": "3-4 Wochen",
"created_by": "System",
"version": "1.0",
"is_sample_deck": true,
"language_pair": "EN-DE"
}'::jsonb,
now(),
now()
);
-- Insert sample cards
INSERT INTO public.cards (deck_id, position, title, card_type, content, created_at, updated_at) VALUES
-- Card 1: Days of the Week
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 1, 'Days of the Week', 'mixed',
'{"blocks": [
{"type": "text", "data": {"text": "Learn the days of the week in English"}},
{"type": "flashcard", "data": {"front": "Monday", "back": "Montag", "pronunciation": "MAN-day"}},
{"type": "flashcard", "data": {"front": "Tuesday", "back": "Dienstag", "pronunciation": "TYOOS-day"}},
{"type": "flashcard", "data": {"front": "Wednesday", "back": "Mittwoch", "pronunciation": "WENZ-day"}},
{"type": "flashcard", "data": {"front": "Thursday", "back": "Donnerstag", "pronunciation": "THURZ-day"}},
{"type": "flashcard", "data": {"front": "Friday", "back": "Freitag", "pronunciation": "FRY-day"}},
{"type": "flashcard", "data": {"front": "Saturday", "back": "Samstag", "pronunciation": "SAT-ur-day"}},
{"type": "flashcard", "data": {"front": "Sunday", "back": "Sonntag", "pronunciation": "SUN-day"}}
]}'::jsonb,
now(), now()),
-- Card 2: Common Verbs - To be
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 2, 'Verb: to be', 'flashcard',
'{"front": "I am / You are / He is", "back": "Ich bin / Du bist / Er ist", "hint": "The most important verb in English", "example": "I am happy. You are my friend. He is tall.", "grammar_note": "Conjugation: am, are, is"}'::jsonb,
now(), now()),
-- Card 3: Common Verbs - To have
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 3, 'Verb: to have', 'flashcard',
'{"front": "to have", "back": "haben", "hint": "Possession verb", "example": "I have a car. She has two cats.", "pronunciation": "hav", "grammar_note": "I/you/we/they have, he/she/it has"}'::jsonb,
now(), now()),
-- Card 4: House vocabulary
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 4, 'House: Kitchen', 'flashcard',
'{"front": "kitchen", "back": "die Küche", "hint": "Where you cook food", "example": "The kitchen is very modern.", "pronunciation": "KIT-chen"}'::jsonb,
now(), now()),
-- Card 5: House: Bedroom
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 5, 'House: Bedroom', 'flashcard',
'{"front": "bedroom", "back": "das Schlafzimmer", "hint": "Where you sleep", "example": "My bedroom is upstairs.", "pronunciation": "BED-room"}'::jsonb,
now(), now()),
-- Card 6: Quiz - Articles
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 6, 'Articles Quiz', 'quiz',
'{"question": "Which article is used before singular nouns starting with consonants?", "options": ["a", "an", "the", "no article"], "correct_answer": 0, "explanation": "Use \"a\" before singular nouns starting with consonants (a book, a car). Use \"an\" before vowel sounds (an apple, an hour)."}'::jsonb,
now(), now()),
-- Card 7: Time expressions
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 7, 'Time: Morning', 'flashcard',
'{"front": "in the morning", "back": "am Morgen / morgens", "hint": "Time of day expression", "example": "I drink coffee in the morning.", "pronunciation": "in the MOR-ning"}'::jsonb,
now(), now()),
-- Card 8: Time: Evening
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 8, 'Time: Evening', 'flashcard',
'{"front": "in the evening", "back": "am Abend / abends", "hint": "Time of day expression", "example": "We watch TV in the evening.", "pronunciation": "in the EEV-ning"}'::jsonb,
now(), now()),
-- Card 9: Weather vocabulary
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 9, 'Weather: Sunny', 'flashcard',
'{"front": "sunny", "back": "sonnig", "hint": "Weather adjective", "example": "It is sunny today.", "pronunciation": "SUN-ee"}'::jsonb,
now(), now()),
-- Card 10: Weather: Rainy
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 10, 'Weather: Rainy', 'flashcard',
'{"front": "rainy", "back": "regnerisch", "hint": "Weather adjective", "example": "It is rainy in London.", "pronunciation": "RAY-nee"}'::jsonb,
now(), now()),
-- Card 11: Body parts
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 11, 'Body: Head', 'flashcard',
'{"front": "head", "back": "der Kopf", "hint": "Body part on top", "example": "My head hurts.", "pronunciation": "hed"}'::jsonb,
now(), now()),
-- Card 12: Body: Hand
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 12, 'Body: Hand', 'flashcard',
'{"front": "hand", "back": "die Hand", "hint": "Body part with fingers", "example": "Please give me your hand.", "pronunciation": "hand"}'::jsonb,
now(), now()),
-- Card 13: Common Phrases
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 13, 'Phrase: How much?', 'flashcard',
'{"front": "How much does it cost?", "back": "Wie viel kostet es?", "hint": "Shopping phrase", "example": "Excuse me, how much does this cost?", "pronunciation": "hau match daz it kost"}'::jsonb,
now(), now()),
-- Card 14: Quiz - Plural forms
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 14, 'Plural Forms Quiz', 'quiz',
'{"question": "What is the plural of \"child\"?", "options": ["childs", "children", "childes", "childer"], "correct_answer": 1, "explanation": "Child is an irregular plural. The plural of child is children, not childs."}'::jsonb,
now(), now()),
-- Card 15: Common adjectives
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 15, 'Adjective: Big/Small', 'flashcard',
'{"front": "big / small", "back": "groß / klein", "hint": "Size adjectives", "example": "The elephant is big. The mouse is small.", "pronunciation": "big / smawl"}'::jsonb,
now(), now()),
-- Card 16: Adjective: Hot/Cold
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 16, 'Adjective: Hot/Cold', 'flashcard',
'{"front": "hot / cold", "back": "heiß / kalt", "hint": "Temperature adjectives", "example": "The coffee is hot. The ice cream is cold.", "pronunciation": "hot / kohld"}'::jsonb,
now(), now()),
-- Card 17: Question words
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 17, 'Question: Where?', 'flashcard',
'{"front": "Where?", "back": "Wo?", "hint": "Question word for location", "example": "Where is the train station?", "pronunciation": "wair"}'::jsonb,
now(), now()),
-- Card 18: Question: When?
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 18, 'Question: When?', 'flashcard',
'{"front": "When?", "back": "Wann?", "hint": "Question word for time", "example": "When does the movie start?", "pronunciation": "wen"}'::jsonb,
now(), now()),
-- Card 19: Prepositions
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 19, 'Preposition: On/In/At', 'mixed',
'{"blocks": [
{"type": "text", "data": {"text": "Important prepositions for place and time"}},
{"type": "flashcard", "data": {"front": "on (the table)", "back": "auf (dem Tisch)", "example": "The book is on the table."}},
{"type": "flashcard", "data": {"front": "in (the room)", "back": "in (dem Zimmer)", "example": "She is in the room."}},
{"type": "flashcard", "data": {"front": "at (the station)", "back": "am/an (dem Bahnhof)", "example": "I am at the station."}}
]}'::jsonb,
now(), now()),
-- Card 20: Final Quiz
('b1c2d3e4-f5a6-7890-bcde-f01234567890', 20, 'Final Review Quiz', 'quiz',
'{"question": "How do you say \"Guten Tag\" in English?", "options": ["Good morning", "Good day/Hello", "Good evening", "Goodbye"], "correct_answer": 1, "explanation": "\"Guten Tag\" means \"Good day\" or simply \"Hello\" in English. It is a general greeting used during the day."}'::jsonb,
now(), now());
-- Update deck metadata with card count
UPDATE public.decks
SET metadata = metadata || '{"card_count": 20}'::jsonb
WHERE id = 'b1c2d3e4-f5a6-7890-bcde-f01234567890';
-- Verification: Show what was created
SELECT
d.title,
d.is_public,
d.user_id,
array_length(d.tags, 1) as tag_count,
COUNT(c.id) as card_count
FROM public.decks d
LEFT JOIN public.cards c ON d.id = c.deck_id
WHERE d.id = 'b1c2d3e4-f5a6-7890-bcde-f01234567890'
GROUP BY d.id, d.title, d.is_public, d.user_id, d.tags;

View file

@ -0,0 +1,128 @@
-- Sample Deck: Deutsch für Anfänger (German Basics) - Simple Version
-- This script creates a sample public deck without needing existing users
-- Uses a system placeholder UUID that can be updated later
-- Insert the deck with a placeholder system user ID
INSERT INTO public.decks (
id,
user_id,
title,
description,
is_public,
tags,
metadata,
created_at,
updated_at
) VALUES (
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
'00000000-0000-0000-0000-000000000001', -- Placeholder system user
'Deutsch für Anfänger',
'Grundwortschatz für Deutsche-Lernende. Perfekt für A1-A2 Niveau mit den wichtigsten Alltagswörtern und Phrasen.',
true,
ARRAY['Sprachen', 'Deutsch', 'Anfänger', 'A1', 'A2', 'Grundwortschatz'],
'{
"category": "Sprachen",
"difficulty": "Anfänger",
"target_audience": "Deutschlernende A1-A2",
"estimated_time": "2-3 Wochen",
"created_by": "System",
"version": "1.0",
"is_sample_deck": true
}'::jsonb,
now(),
now()
);
-- Insert sample cards
INSERT INTO public.cards (deck_id, position, title, card_type, content, created_at, updated_at) VALUES
-- Card 1: Greetings
('a0b1c2d3-e4f5-6789-abcd-ef0123456789', 1, 'Begrüßung - Hallo', 'flashcard',
'{"front": "Hallo", "back": "Hello", "hint": "Standard greeting in German", "example": "Hallo, wie geht es dir?", "pronunciation": "HAH-lo"}'::jsonb,
now(), now()),
-- Card 2: Thank you
('a0b1c2d3-e4f5-6789-abcd-ef0123456789', 2, 'Danke sagen', 'flashcard',
'{"front": "Danke", "back": "Thank you", "hint": "Basic way to express gratitude", "example": "Danke für deine Hilfe!", "pronunciation": "DAHN-keh"}'::jsonb,
now(), now()),
-- Card 3: Please
('a0b1c2d3-e4f5-6789-abcd-ef0123456789', 3, 'Höflich bitten', 'flashcard',
'{"front": "Bitte", "back": "Please", "hint": "Used when asking for something politely", "example": "Können Sie mir bitte helfen?", "pronunciation": "BIT-teh"}'::jsonb,
now(), now()),
-- Card 4: Excuse me
('a0b1c2d3-e4f5-6789-abcd-ef0123456789', 4, 'Entschuldigung', 'flashcard',
'{"front": "Entschuldigung", "back": "Excuse me / Sorry", "hint": "Used to get attention or apologize", "example": "Entschuldigung, wo ist der Bahnhof?", "pronunciation": "ent-SHUL-di-gung"}'::jsonb,
now(), now()),
-- Card 5: Yes/No Quiz
('a0b1c2d3-e4f5-6789-abcd-ef0123456789', 5, 'Ja und Nein', 'quiz',
'{"question": "Wie sagt man \"Yes\" auf Deutsch?", "options": ["Ja", "Nein", "Vielleicht", "Okay"], "correct_answer": 0, "explanation": "\"Ja\" bedeutet \"Yes\" auf Deutsch. \"Nein\" bedeutet \"No\"."}'::jsonb,
now(), now()),
-- Card 6: Good morning
('a0b1c2d3-e4f5-6789-abcd-ef0123456789', 6, 'Guten Morgen', 'flashcard',
'{"front": "Guten Morgen", "back": "Good morning", "hint": "Morning greeting until about 10 AM", "example": "Guten Morgen! Haben Sie gut geschlafen?", "pronunciation": "GOO-ten MOR-gen"}'::jsonb,
now(), now()),
-- Card 7: Good evening
('a0b1c2d3-e4f5-6789-abcd-ef0123456789', 7, 'Guten Abend', 'flashcard',
'{"front": "Guten Abend", "back": "Good evening", "hint": "Evening greeting from about 6 PM", "example": "Guten Abend, wie war Ihr Tag?", "pronunciation": "GOO-ten AH-bent"}'::jsonb,
now(), now()),
-- Card 8: Numbers
('a0b1c2d3-e4f5-6789-abcd-ef0123456789', 8, 'Zahlen 1-5', 'mixed',
'{"front": "Die Zahlen von 1 bis 5", "back": "1 = eins, 2 = zwei, 3 = drei, 4 = vier, 5 = fünf", "hint": "Basic counting in German", "additional_info": "These are the most basic numbers you need to know"}'::jsonb,
now(), now()),
-- Card 9: Colors - Red
('a0b1c2d3-e4f5-6789-abcd-ef0123456789', 9, 'Farbe: Rot', 'flashcard',
'{"front": "rot", "back": "red", "hint": "The color of a rose or blood", "example": "Das Auto ist rot.", "pronunciation": "roht"}'::jsonb,
now(), now()),
-- Card 10: Colors - Blue
('a0b1c2d3-e4f5-6789-abcd-ef0123456789', 10, 'Farbe: Blau', 'flashcard',
'{"front": "blau", "back": "blue", "hint": "The color of the sky or ocean", "example": "Der Himmel ist blau.", "pronunciation": "blau"}'::jsonb,
now(), now()),
-- Card 11: Food - Bread
('a0b1c2d3-e4f5-6789-abcd-ef0123456789', 11, 'Essen: Brot', 'flashcard',
'{"front": "das Brot", "back": "the bread", "hint": "Basic food item, neuter noun", "example": "Ich esse Brot zum Frühstück.", "pronunciation": "dahs broht", "grammar_note": "neuter noun (das)"}'::jsonb,
now(), now()),
-- Card 12: Drink - Water
('a0b1c2d3-e4f5-6789-abcd-ef0123456789', 12, 'Getränk: Wasser', 'flashcard',
'{"front": "das Wasser", "back": "the water", "hint": "Essential drink, neuter noun", "example": "Ich trinke Wasser.", "pronunciation": "dahs VAH-ser", "grammar_note": "neuter noun (das)"}'::jsonb,
now(), now()),
-- Card 13: Family - Mother
('a0b1c2d3-e4f5-6789-abcd-ef0123456789', 13, 'Familie: Mutter', 'flashcard',
'{"front": "die Mutter", "back": "the mother", "hint": "Female parent, feminine noun", "example": "Meine Mutter ist sehr nett.", "pronunciation": "dee MUT-ter", "grammar_note": "feminine noun (die)"}'::jsonb,
now(), now()),
-- Card 14: Family - Father
('a0b1c2d3-e4f5-6789-abcd-ef0123456789', 14, 'Familie: Vater', 'flashcard',
'{"front": "der Vater", "back": "the father", "hint": "Male parent, masculine noun", "example": "Mein Vater arbeitet viel.", "pronunciation": "der FAH-ter", "grammar_note": "masculine noun (der)"}'::jsonb,
now(), now()),
-- Card 15: Question - How are you?
('a0b1c2d3-e4f5-6789-abcd-ef0123456789', 15, 'Frage: Wie geht es dir?', 'flashcard',
'{"front": "Wie geht es dir?", "back": "How are you? (informal)", "hint": "Common question when greeting friends", "example": "Hallo Maria! Wie geht es dir?", "pronunciation": "vee gayt es deer", "additional_info": "Formal version: Wie geht es Ihnen?"}'::jsonb,
now(), now());
-- Update deck metadata with card count
UPDATE public.decks
SET metadata = metadata || '{"card_count": 15}'::jsonb
WHERE id = 'a0b1c2d3-e4f5-6789-abcd-ef0123456789';
-- Verification: Show what was created
SELECT
d.title,
d.is_public,
d.user_id,
array_length(d.tags, 1) as tag_count,
COUNT(c.id) as card_count
FROM public.decks d
LEFT JOIN public.cards c ON d.id = c.deck_id
WHERE d.id = 'a0b1c2d3-e4f5-6789-abcd-ef0123456789'
GROUP BY d.id, d.title, d.is_public, d.user_id, d.tags;

View file

@ -0,0 +1,661 @@
-- Sample Deck: Deutsch für Anfänger (German Basics)
-- This SQL script creates a sample public deck with basic German vocabulary
-- To be executed by administrators or for initial seeding
-- First, check if we have any existing users, otherwise create a system user
DO $$
DECLARE
system_user_id uuid;
BEGIN
-- Try to find an existing user
SELECT id INTO system_user_id FROM auth.users LIMIT 1;
-- If no user exists, we'll use a fixed UUID for the system user
-- This would typically be handled by having a proper admin user
IF system_user_id IS NULL THEN
system_user_id := '00000000-0000-0000-0000-000000000001';
-- Note: In a real system, you would have a proper admin user
-- For now, we'll just use this fixed UUID as a placeholder
RAISE NOTICE 'Using system placeholder UUID: %', system_user_id;
END IF;
END $$;
-- Insert the deck using a system user or the first available user
INSERT INTO public.decks (
id,
user_id,
title,
description,
is_public,
tags,
metadata,
created_at,
updated_at
) VALUES (
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
COALESCE(
(SELECT id FROM auth.users LIMIT 1),
'00000000-0000-0000-0000-000000000001'::uuid
), -- Use first available user or system placeholder
'Deutsch für Anfänger',
'Grundwortschatz für Deutsche-Lernende. Perfekt für A1-A2 Niveau mit den wichtigsten Alltagswörtern und Phrasen.',
true,
ARRAY['Sprachen', 'Deutsch', 'Anfänger', 'A1', 'A2', 'Grundwortschatz'],
'{
"category": "Sprachen",
"difficulty": "Anfänger",
"target_audience": "Deutschlernende A1-A2",
"estimated_time": "2-3 Wochen",
"created_by": "Admin",
"version": "1.0"
}'::jsonb,
now(),
now()
);
-- Insert cards for the German basics deck
-- Card 1: Greetings
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
1,
'Begrüßung - Hallo',
'flashcard',
'{
"front": "Hallo",
"back": "Hello",
"hint": "Standard greeting in German",
"example": "Hallo, wie geht es dir?",
"pronunciation": "HAH-lo"
}'::jsonb,
now(),
now()
);
-- Card 2: Thank you
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
2,
'Danke sagen',
'flashcard',
'{
"front": "Danke",
"back": "Thank you",
"hint": "Basic way to express gratitude",
"example": "Danke für deine Hilfe!",
"pronunciation": "DAHN-keh"
}'::jsonb,
now(),
now()
);
-- Card 3: Please
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
3,
'Höflich bitten',
'flashcard',
'{
"front": "Bitte",
"back": "Please",
"hint": "Used when asking for something politely",
"example": "Können Sie mir bitte helfen?",
"pronunciation": "BIT-teh"
}'::jsonb,
now(),
now()
);
-- Card 4: Excuse me
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
4,
'Entschuldigung',
'flashcard',
'{
"front": "Entschuldigung",
"back": "Excuse me / Sorry",
"hint": "Used to get attention or apologize",
"example": "Entschuldigung, wo ist der Bahnhof?",
"pronunciation": "ent-SHUL-di-gung"
}'::jsonb,
now(),
now()
);
-- Card 5: Yes/No
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
5,
'Ja und Nein',
'quiz',
'{
"question": "Wie sagt man ''Yes'' auf Deutsch?",
"options": ["Ja", "Nein", "Vielleicht", "Okay"],
"correct_answer": 0,
"explanation": "''Ja'' bedeutet ''Yes'' auf Deutsch. ''Nein'' bedeutet ''No''."
}'::jsonb,
now(),
now()
);
-- Card 6: Good morning
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
6,
'Guten Morgen',
'flashcard',
'{
"front": "Guten Morgen",
"back": "Good morning",
"hint": "Morning greeting until about 10 AM",
"example": "Guten Morgen! Haben Sie gut geschlafen?",
"pronunciation": "GOO-ten MOR-gen"
}'::jsonb,
now(),
now()
);
-- Card 7: Good evening
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
7,
'Guten Abend',
'flashcard',
'{
"front": "Guten Abend",
"back": "Good evening",
"hint": "Evening greeting from about 6 PM",
"example": "Guten Abend, wie war Ihr Tag?",
"pronunciation": "GOO-ten AH-bent"
}'::jsonb,
now(),
now()
);
-- Card 8: Numbers 1-5
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
8,
'Zahlen 1-5',
'mixed',
'{
"front": "Die Zahlen von 1 bis 5",
"back": "1 = eins, 2 = zwei, 3 = drei, 4 = vier, 5 = fünf",
"hint": "Basic counting in German",
"additional_info": "These are the most basic numbers you need to know"
}'::jsonb,
now(),
now()
);
-- Card 9: Colors - Red
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
9,
'Farbe: Rot',
'flashcard',
'{
"front": "rot",
"back": "red",
"hint": "The color of a rose or blood",
"example": "Das Auto ist rot.",
"pronunciation": "roht"
}'::jsonb,
now(),
now()
);
-- Card 10: Colors - Blue
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
10,
'Farbe: Blau',
'flashcard',
'{
"front": "blau",
"back": "blue",
"hint": "The color of the sky or ocean",
"example": "Der Himmel ist blau.",
"pronunciation": "blau"
}'::jsonb,
now(),
now()
);
-- Card 11: Food - Bread
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
11,
'Essen: Brot',
'flashcard',
'{
"front": "das Brot",
"back": "the bread",
"hint": "Basic food item, neuter noun",
"example": "Ich esse Brot zum Frühstück.",
"pronunciation": "dahs broht",
"grammar_note": "neuter noun (das)"
}'::jsonb,
now(),
now()
);
-- Card 12: Food - Water
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
12,
'Getränk: Wasser',
'flashcard',
'{
"front": "das Wasser",
"back": "the water",
"hint": "Essential drink, neuter noun",
"example": "Ich trinke Wasser.",
"pronunciation": "dahs VAH-ser",
"grammar_note": "neuter noun (das)"
}'::jsonb,
now(),
now()
);
-- Card 13: Family - Mother
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
13,
'Familie: Mutter',
'flashcard',
'{
"front": "die Mutter",
"back": "the mother",
"hint": "Female parent, feminine noun",
"example": "Meine Mutter ist sehr nett.",
"pronunciation": "dee MUT-ter",
"grammar_note": "feminine noun (die)"
}'::jsonb,
now(),
now()
);
-- Card 14: Family - Father
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
14,
'Familie: Vater',
'flashcard',
'{
"front": "der Vater",
"back": "the father",
"hint": "Male parent, masculine noun",
"example": "Mein Vater arbeitet viel.",
"pronunciation": "der FAH-ter",
"grammar_note": "masculine noun (der)"
}'::jsonb,
now(),
now()
);
-- Card 15: Question - How are you?
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
15,
'Frage: Wie geht es dir?',
'flashcard',
'{
"front": "Wie geht es dir?",
"back": "How are you? (informal)",
"hint": "Common question when greeting friends",
"example": "Hallo Maria! Wie geht es dir?",
"pronunciation": "vee gayt es deer",
"additional_info": "Formal version: Wie geht es Ihnen?"
}'::jsonb,
now(),
now()
);
-- Card 16: Weather - Sun
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
16,
'Wetter: Sonne',
'flashcard',
'{
"front": "die Sonne",
"back": "the sun",
"hint": "Bright object in the sky, feminine noun",
"example": "Die Sonne scheint heute.",
"pronunciation": "dee SON-neh",
"grammar_note": "feminine noun (die)"
}'::jsonb,
now(),
now()
);
-- Card 17: Days - Today
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
17,
'Zeit: Heute',
'flashcard',
'{
"front": "heute",
"back": "today",
"hint": "Refers to the current day",
"example": "Heute ist ein schöner Tag.",
"pronunciation": "HOY-teh"
}'::jsonb,
now(),
now()
);
-- Card 18: Basic verb - to be
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
18,
'Verb: sein (to be)',
'mixed',
'{
"front": "sein - to be",
"back": "ich bin (I am), du bist (you are), er/sie/es ist (he/she/it is)",
"hint": "Most important verb in German",
"example": "Ich bin müde. Du bist nett. Er ist groß.",
"pronunciation": "zine"
}'::jsonb,
now(),
now()
);
-- Card 19: Basic verb - to have
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
19,
'Verb: haben (to have)',
'mixed',
'{
"front": "haben - to have",
"back": "ich habe (I have), du hast (you have), er/sie/es hat (he/she/it has)",
"hint": "Second most important verb in German",
"example": "Ich habe Hunger. Du hast Zeit. Sie hat ein Auto.",
"pronunciation": "HAH-ben"
}'::jsonb,
now(),
now()
);
-- Card 20: Goodbye
INSERT INTO public.cards (
id,
deck_id,
position,
title,
card_type,
content,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
20,
'Abschied: Auf Wiedersehen',
'flashcard',
'{
"front": "Auf Wiedersehen",
"back": "Goodbye",
"hint": "Formal way to say goodbye",
"example": "Auf Wiedersehen, bis morgen!",
"pronunciation": "owf VEE-der-zay-en",
"additional_info": "Informal: Tschüss (CHOOS)"
}'::jsonb,
now(),
now()
);
-- Update deck metadata to reflect the actual card count
UPDATE public.decks
SET metadata = metadata || '{"card_count": 20}'::jsonb
WHERE id = 'a0b1c2d3-e4f5-6789-abcd-ef0123456789';
-- Add some sample study sessions and progress (optional)
-- This would typically be generated as users study, but can be added for demo purposes
-- Comment out the following if you don't want demo progress data:
/*
-- Sample study session (replace USER_ID with actual user)
INSERT INTO public.study_sessions (
id,
user_id,
deck_id,
mode,
started_at,
ended_at,
total_cards,
completed_cards,
correct_answers,
incorrect_answers,
total_time_seconds,
average_card_time_seconds
) VALUES (
gen_random_uuid(),
'YOUR_USER_ID',
'a0b1c2d3-e4f5-6789-abcd-ef0123456789',
'all',
now() - interval '1 day',
now() - interval '1 day' + interval '15 minutes',
20,
20,
16,
4,
900, -- 15 minutes
45 -- 45 seconds per card
);
*/
-- Verification queries (uncomment to run after insertion):
/*
-- Verify the deck was created
SELECT id, title, description, is_public, array_length(tags, 1) as tag_count
FROM public.decks
WHERE id = 'a0b1c2d3-e4f5-6789-abcd-ef0123456789';
-- Count cards in the deck
SELECT COUNT(*) as card_count
FROM public.cards
WHERE deck_id = 'a0b1c2d3-e4f5-6789-abcd-ef0123456789';
-- Show first few cards
SELECT position, title, card_type, content->>'front' as front_text, content->>'back' as back_text
FROM public.cards
WHERE deck_id = 'a0b1c2d3-e4f5-6789-abcd-ef0123456789'
ORDER BY position
LIMIT 5;
*/

View file

@ -0,0 +1,215 @@
-- Sample Deck: Weltgeschichte - Wichtige Ereignisse (World History - Key Events)
-- This script creates a sample public deck for learning important historical events
-- Uses a system placeholder UUID that can be updated later
-- Insert the deck with a placeholder system user ID
INSERT INTO public.decks (
id,
user_id,
title,
description,
is_public,
tags,
metadata,
created_at,
updated_at
) VALUES (
'd3e4f5a6-b7c8-9012-def0-123456789012',
'00000000-0000-0000-0000-000000000001', -- Placeholder system user
'Weltgeschichte - Wichtige Ereignisse',
'Die wichtigsten Ereignisse der Weltgeschichte von der Antike bis zur Moderne. Ideal für Schüler und Geschichtsinteressierte.',
true,
ARRAY['Geschichte', 'Weltgeschichte', 'Ereignisse', 'Daten', 'Epochen', 'Bildung'],
'{
"category": "Geschichte",
"difficulty": "Mittel",
"target_audience": "Schüler ab 7. Klasse, Geschichtsinteressierte",
"estimated_time": "5-6 Wochen",
"created_by": "System",
"version": "1.0",
"is_sample_deck": true,
"time_period": "3000 v.Chr. - 21. Jahrhundert"
}'::jsonb,
now(),
now()
);
-- Insert sample cards
INSERT INTO public.cards (deck_id, position, title, card_type, content, created_at, updated_at) VALUES
-- Card 1: Ancient Egypt
('d3e4f5a6-b7c8-9012-def0-123456789012', 1, 'Altes Ägypten: Pyramiden', 'flashcard',
'{"front": "Wann wurden die Pyramiden von Gizeh erbaut?", "back": "ca. 2580-2560 v.Chr.", "hint": "Während der 4. Dynastie des Alten Reichs", "context": "Die Große Pyramide des Cheops ist eines der sieben Weltwunder der Antike", "significance": "Zeugnis der fortgeschrittenen Baukunst und Organisation im alten Ägypten"}'::jsonb,
now(), now()),
-- Card 2: Ancient Greece
('d3e4f5a6-b7c8-9012-def0-123456789012', 2, 'Antikes Griechenland: Demokratie', 'mixed',
'{"blocks": [
{"type": "text", "data": {"text": "Die Geburt der Demokratie in Athen"}},
{"type": "flashcard", "data": {"front": "Wer führte demokratische Reformen in Athen ein?", "back": "Kleisthenes (508/507 v.Chr.)", "context": "Begründer der attischen Demokratie"}},
{"type": "flashcard", "data": {"front": "Was bedeutet Demokratie wörtlich?", "back": "Herrschaft des Volkes", "etymology": "demos (Volk) + kratos (Herrschaft)"}},
{"type": "quiz", "data": {"question": "Wer durfte in der athenischen Demokratie wählen?", "options": ["Alle Einwohner", "Nur freie Männer über 18", "Nur Adlige", "Männer und Frauen über 21"], "correct_answer": 1, "explanation": "Nur freie männliche Bürger über 18 Jahren durften wählen - Frauen, Sklaven und Metöken (Fremde) waren ausgeschlossen"}}
]}'::jsonb,
now(), now()),
-- Card 3: Roman Empire
('d3e4f5a6-b7c8-9012-def0-123456789012', 3, 'Römisches Reich: Untergang', 'quiz',
'{"question": "In welchem Jahr endete das Weströmische Reich?", "options": ["376 n.Chr.", "476 n.Chr.", "576 n.Chr.", "1453 n.Chr."], "correct_answer": 1, "explanation": "476 n.Chr. wurde der letzte weströmische Kaiser Romulus Augustulus abgesetzt. Das Oströmische Reich (Byzanz) bestand noch bis 1453."}'::jsonb,
now(), now()),
-- Card 4: Middle Ages
('d3e4f5a6-b7c8-9012-def0-123456789012', 4, 'Mittelalter: Karl der Große', 'flashcard',
'{"front": "Wann wurde Karl der Große zum Kaiser gekrönt?", "back": "25. Dezember 800 n.Chr.", "location": "Rom, durch Papst Leo III.", "significance": "Begründung des Heiligen Römischen Reiches", "empire": "Vereinte große Teile West- und Mitteleuropas"}'::jsonb,
now(), now()),
-- Card 5: The Crusades
('d3e4f5a6-b7c8-9012-def0-123456789012', 5, 'Kreuzzüge: Erster Kreuzzug', 'flashcard',
'{"front": "Zeitraum des Ersten Kreuzzugs?", "back": "1096-1099", "goal": "Eroberung Jerusalems von den Muslimen", "result": "Eroberung Jerusalems 1099, Gründung der Kreuzfahrerstaaten", "called_by": "Papst Urban II. auf dem Konzil von Clermont (1095)"}'::jsonb,
now(), now()),
-- Card 6: Black Death
('d3e4f5a6-b7c8-9012-def0-123456789012', 6, 'Mittelalter: Der Schwarze Tod', 'mixed',
'{"blocks": [
{"type": "text", "data": {"text": "Die verheerendste Pandemie des Mittelalters"}},
{"type": "flashcard", "data": {"front": "Zeitraum der Pest in Europa?", "back": "1347-1353", "peak": "Höhepunkt 1348-1350"}},
{"type": "flashcard", "data": {"front": "Geschätzte Todesopfer in Europa?", "back": "30-60% der Bevölkerung", "numbers": "Ca. 25 Millionen Menschen"}},
{"type": "text", "data": {"text": "Folgen: Arbeitskräftemangel, soziale Umwälzungen, Judenverfolgungen, Entstehung neuer religiöser Bewegungen"}}
]}'::jsonb,
now(), now()),
-- Card 7: Renaissance
('d3e4f5a6-b7c8-9012-def0-123456789012', 7, 'Renaissance: Beginn', 'quiz',
'{"question": "Wo begann die Renaissance?", "options": ["Frankreich", "Deutschland", "Italien", "England"], "correct_answer": 2, "explanation": "Die Renaissance begann im 14. Jahrhundert in Italien, besonders in Florenz, und verbreitete sich dann über ganz Europa."}'::jsonb,
now(), now()),
-- Card 8: Columbus
('d3e4f5a6-b7c8-9012-def0-123456789012', 8, 'Entdeckungen: Kolumbus', 'flashcard',
'{"front": "Wann erreichte Kolumbus Amerika?", "back": "12. Oktober 1492", "location": "Landung auf einer Bahamas-Insel (wahrscheinlich San Salvador)", "ships": "Mit drei Schiffen: Santa Maria, Pinta, Niña", "misconception": "Er glaubte, einen neuen Weg nach Indien gefunden zu haben"}'::jsonb,
now(), now()),
-- Card 9: Reformation
('d3e4f5a6-b7c8-9012-def0-123456789012', 9, 'Reformation: Martin Luther', 'mixed',
'{"blocks": [
{"type": "flashcard", "data": {"front": "Wann schlug Luther seine 95 Thesen an?", "back": "31. Oktober 1517", "location": "Schlosskirche zu Wittenberg"}},
{"type": "text", "data": {"text": "Kritik am Ablasshandel und Beginn der Reformation"}},
{"type": "quiz", "data": {"question": "Was war Luthers wichtigste Übersetzungsleistung?", "options": ["Lateinische Messe", "Bibel ins Deutsche", "Griechische Philosophen", "Kirchenlieder"], "correct_answer": 1, "explanation": "Luther übersetzte die Bibel ins Deutsche (Neues Testament 1522, vollständige Bibel 1534)"}}
]}'::jsonb,
now(), now()),
-- Card 10: Thirty Years War
('d3e4f5a6-b7c8-9012-def0-123456789012', 10, 'Dreißigjähriger Krieg', 'flashcard',
'{"front": "Zeitraum des Dreißigjährigen Krieges?", "back": "1618-1648", "cause": "Religiöse und machtpolitische Konflikte im Heiligen Römischen Reich", "end": "Westfälischer Friede", "consequences": "Verwüstung Mitteleuropas, ca. 30% Bevölkerungsverlust in deutschen Gebieten"}'::jsonb,
now(), now()),
-- Card 11: French Revolution
('d3e4f5a6-b7c8-9012-def0-123456789012', 11, 'Französische Revolution', 'mixed',
'{"blocks": [
{"type": "flashcard", "data": {"front": "Sturm auf die Bastille?", "back": "14. Juli 1789", "significance": "Symbol für den Beginn der Revolution"}},
{"type": "flashcard", "data": {"front": "Motto der Revolution?", "back": "Liberté, Égalité, Fraternité", "translation": "Freiheit, Gleichheit, Brüderlichkeit"}},
{"type": "quiz", "data": {"question": "Wer wurde 1793 hingerichtet?", "options": ["Napoleon", "Ludwig XIV.", "Ludwig XVI.", "Robespierre"], "correct_answer": 2, "explanation": "König Ludwig XVI. wurde am 21. Januar 1793 guillotiniert, seine Frau Marie Antoinette im Oktober 1793"}}
]}'::jsonb,
now(), now()),
-- Card 12: Napoleon
('d3e4f5a6-b7c8-9012-def0-123456789012', 12, 'Napoleon: Kaiserkrönung', 'flashcard',
'{"front": "Wann krönte sich Napoleon zum Kaiser?", "back": "2. Dezember 1804", "location": "Notre-Dame de Paris", "special": "Er krönte sich selbst, um seine Unabhängigkeit vom Papst zu demonstrieren", "end_of_reign": "Abdankung 1814, Verbannung nach Elba, 100 Tage, finale Niederlage bei Waterloo 1815"}'::jsonb,
now(), now()),
-- Card 13: Industrial Revolution
('d3e4f5a6-b7c8-9012-def0-123456789012', 13, 'Industrielle Revolution', 'quiz',
'{"question": "Wo begann die Industrielle Revolution?", "options": ["Deutschland", "USA", "Großbritannien", "Frankreich"], "correct_answer": 2, "explanation": "Die Industrielle Revolution begann um 1760 in Großbritannien mit der Mechanisierung der Textilindustrie und der Erfindung der Dampfmaschine."}'::jsonb,
now(), now()),
-- Card 14: American Civil War
('d3e4f5a6-b7c8-9012-def0-123456789012', 14, 'Amerikanischer Bürgerkrieg', 'flashcard',
'{"front": "Zeitraum des Amerikanischen Bürgerkriegs?", "back": "1861-1865", "sides": "Nordstaaten (Union) vs. Südstaaten (Konföderation)", "main_issue": "Sklaverei und Rechte der Einzelstaaten", "outcome": "Sieg der Union, Abschaffung der Sklaverei (13. Amendment)", "casualties": "Ca. 620.000 Tote"}'::jsonb,
now(), now()),
-- Card 15: German Unification
('d3e4f5a6-b7c8-9012-def0-123456789012', 15, 'Deutsche Reichsgründung', 'flashcard',
'{"front": "Wann wurde das Deutsche Reich gegründet?", "back": "18. Januar 1871", "location": "Spiegelsaal von Versailles", "first_kaiser": "Wilhelm I.", "architect": "Otto von Bismarck (Reichskanzler)", "context": "Nach dem Sieg im Deutsch-Französischen Krieg 1870/71"}'::jsonb,
now(), now()),
-- Card 16: World War I
('d3e4f5a6-b7c8-9012-def0-123456789012', 16, 'Erster Weltkrieg', 'mixed',
'{"blocks": [
{"type": "flashcard", "data": {"front": "Zeitraum des Ersten Weltkriegs?", "back": "1914-1918", "trigger": "Attentat von Sarajevo (28. Juni 1914)"}},
{"type": "flashcard", "data": {"front": "Waffenstillstand?", "back": "11. November 1918", "time": "11 Uhr (11.11. um 11 Uhr)"}},
{"type": "quiz", "data": {"question": "Welcher Vertrag beendete offiziell den Krieg mit Deutschland?", "options": ["Vertrag von Verdun", "Vertrag von Versailles", "Vertrag von Wien", "Vertrag von Potsdam"], "correct_answer": 1, "explanation": "Der Vertrag von Versailles wurde am 28. Juni 1919 unterzeichnet"}}
]}'::jsonb,
now(), now()),
-- Card 17: Russian Revolution
('d3e4f5a6-b7c8-9012-def0-123456789012', 17, 'Russische Revolution', 'flashcard',
'{"front": "Oktoberrevolution in Russland?", "back": "7. November 1917 (25. Oktober alten Stils)", "leader": "Wladimir Lenin", "party": "Bolschewiki", "result": "Sturz der provisorischen Regierung, Beginn der Sowjetmacht", "civil_war": "Russischer Bürgerkrieg 1918-1921"}'::jsonb,
now(), now()),
-- Card 18: Great Depression
('d3e4f5a6-b7c8-9012-def0-123456789012', 18, 'Weltwirtschaftskrise', 'quiz',
'{"question": "Mit welchem Ereignis begann die Weltwirtschaftskrise?", "options": ["Krieg in Europa", "Börsenkrach in New York", "Bankenkrise in London", "Revolution in Russland"], "correct_answer": 1, "explanation": "Der Schwarze Donnerstag (24. Oktober 1929) und Schwarze Dienstag (29. Oktober 1929) an der New Yorker Börse lösten die Weltwirtschaftskrise aus."}'::jsonb,
now(), now()),
-- Card 19: World War II
('d3e4f5a6-b7c8-9012-def0-123456789012', 19, 'Zweiter Weltkrieg', 'mixed',
'{"blocks": [
{"type": "flashcard", "data": {"front": "Beginn des Zweiten Weltkriegs?", "back": "1. September 1939", "event": "Deutscher Überfall auf Polen"}},
{"type": "flashcard", "data": {"front": "Ende in Europa?", "back": "8. Mai 1945", "event": "Bedingungslose Kapitulation Deutschlands"}},
{"type": "flashcard", "data": {"front": "Ende in Asien?", "back": "2. September 1945", "event": "Kapitulation Japans nach Atombomben auf Hiroshima und Nagasaki"}},
{"type": "text", "data": {"text": "Geschätzte Opferzahl: 70-85 Millionen Menschen, davon etwa 6 Millionen Juden im Holocaust"}}
]}'::jsonb,
now(), now()),
-- Card 20: United Nations
('d3e4f5a6-b7c8-9012-def0-123456789012', 20, 'Vereinte Nationen', 'flashcard',
'{"front": "Gründung der UNO?", "back": "24. Oktober 1945", "location": "San Francisco", "charter": "UN-Charta unterzeichnet am 26. Juni 1945", "members": "Ursprünglich 51 Mitgliedsstaaten, heute 193", "goals": "Weltfrieden, internationale Sicherheit, Menschenrechte"}'::jsonb,
now(), now()),
-- Card 21: Cold War
('d3e4f5a6-b7c8-9012-def0-123456789012', 21, 'Kalter Krieg: Berliner Mauer', 'mixed',
'{"blocks": [
{"type": "flashcard", "data": {"front": "Bau der Berliner Mauer?", "back": "13. August 1961", "reason": "Stopp der Massenflucht aus der DDR"}},
{"type": "flashcard", "data": {"front": "Fall der Berliner Mauer?", "back": "9. November 1989", "trigger": "Friedliche Revolution in der DDR"}},
{"type": "text", "data": {"text": "Die Mauer stand 28 Jahre und teilte Berlin und Deutschland"}}
]}'::jsonb,
now(), now()),
-- Card 22: Moon Landing
('d3e4f5a6-b7c8-9012-def0-123456789012', 22, 'Raumfahrt: Mondlandung', 'flashcard',
'{"front": "Erste bemannte Mondlandung?", "back": "20. Juli 1969", "mission": "Apollo 11", "astronauts": "Neil Armstrong (erster Mensch auf dem Mond), Buzz Aldrin, Michael Collins", "famous_quote": "That\'s one small step for man, one giant leap for mankind", "country": "USA"}'::jsonb,
now(), now()),
-- Card 23: European Union
('d3e4f5a6-b7c8-9012-def0-123456789012', 23, 'Europäische Union', 'quiz',
'{"question": "Welcher Vertrag begründete die EWG (Vorläufer der EU)?", "options": ["Vertrag von Lissabon", "Römische Verträge", "Vertrag von Maastricht", "Vertrag von Amsterdam"], "correct_answer": 1, "explanation": "Die Römischen Verträge wurden am 25. März 1957 unterzeichnet und begründeten die Europäische Wirtschaftsgemeinschaft (EWG). Die EU entstand 1993 durch den Vertrag von Maastricht."}'::jsonb,
now(), now()),
-- Card 24: 9/11 Attacks
('d3e4f5a6-b7c8-9012-def0-123456789012', 24, 'Terroranschläge 9/11', 'flashcard',
'{"front": "Datum der Anschläge auf das World Trade Center?", "back": "11. September 2001", "location": "New York City und Washington D.C.", "perpetrator": "Al-Qaida unter Osama bin Laden", "victims": "Fast 3.000 Menschen", "consequences": "Krieg gegen den Terror, Afghanistan-Krieg, veränderte Sicherheitspolitik weltweit"}'::jsonb,
now(), now()),
-- Card 25: COVID-19 Pandemic
('d3e4f5a6-b7c8-9012-def0-123456789012', 25, 'COVID-19 Pandemie', 'mixed',
'{"blocks": [
{"type": "flashcard", "data": {"front": "Wann erklärte die WHO COVID-19 zur Pandemie?", "back": "11. März 2020", "first_cases": "Dezember 2019 in Wuhan, China"}},
{"type": "text", "data": {"text": "Erste mRNA-Impfstoffe in der Geschichte, entwickelt in Rekordzeit"}},
{"type": "flashcard", "data": {"front": "Erste Impfstoffzulassung?", "back": "Dezember 2020", "vaccines": "BioNTech/Pfizer, Moderna"}},
{"type": "text", "data": {"text": "Globale Auswirkungen: Lockdowns, Wirtschaftskrise, Digitalisierungsschub, über 6 Millionen Tote weltweit"}}
]}'::jsonb,
now(), now());
-- Update deck metadata with card count
UPDATE public.decks
SET metadata = metadata || '{"card_count": 25}'::jsonb
WHERE id = 'd3e4f5a6-b7c8-9012-def0-123456789012';
-- Verification: Show what was created
SELECT
d.title,
d.is_public,
d.user_id,
array_length(d.tags, 1) as tag_count,
COUNT(c.id) as card_count
FROM public.decks d
LEFT JOIN public.cards c ON d.id = c.deck_id
WHERE d.id = 'd3e4f5a6-b7c8-9012-def0-123456789012'
GROUP BY d.id, d.title, d.is_public, d.user_id, d.tags;

View file

@ -0,0 +1,216 @@
-- Sample Deck: Mathematik Grundlagen (Mathematics Basics)
-- This script creates a sample public deck for learning basic math concepts
-- Uses a system placeholder UUID that can be updated later
-- Insert the deck with a placeholder system user ID
INSERT INTO public.decks (
id,
user_id,
title,
description,
is_public,
tags,
metadata,
created_at,
updated_at
) VALUES (
'c2d3e4f5-a6b7-8901-cdef-012345678901',
'00000000-0000-0000-0000-000000000001', -- Placeholder system user
'Mathematik Grundlagen',
'Grundlegende mathematische Konzepte und Formeln. Perfekt für Schüler der Mittelstufe und zur Auffrischung.',
true,
ARRAY['Mathematik', 'Grundlagen', 'Formeln', 'Geometrie', 'Algebra', 'Mittelstufe'],
'{
"category": "Mathematik",
"difficulty": "Grundlagen",
"target_audience": "Schüler 5.-8. Klasse",
"estimated_time": "4-5 Wochen",
"created_by": "System",
"version": "1.0",
"is_sample_deck": true,
"includes_formulas": true
}'::jsonb,
now(),
now()
);
-- Insert sample cards
INSERT INTO public.cards (deck_id, position, title, card_type, content, created_at, updated_at) VALUES
-- Card 1: Addition und Subtraktion
('c2d3e4f5-a6b7-8901-cdef-012345678901', 1, 'Grundrechenarten: Addition', 'flashcard',
'{"front": "Was ist 47 + 38?", "back": "85", "hint": "Addiere zuerst die Einer (7+8=15), schreibe 5 und merke 1, dann die Zehner (4+3+1=8)", "explanation": "Addition ist das Zusammenzählen von Zahlen"}'::jsonb,
now(), now()),
-- Card 2: Multiplication Table
('c2d3e4f5-a6b7-8901-cdef-012345678901', 2, 'Einmaleins: 7er-Reihe', 'quiz',
'{"question": "Was ist 7 × 8?", "options": ["54", "56", "58", "63"], "correct_answer": 1, "explanation": "7 × 8 = 56. Tipp: 7 × 8 ist dasselbe wie 8 × 7, und 8 × 7 = 56"}'::jsonb,
now(), now()),
-- Card 3: Brüche Grundlagen
('c2d3e4f5-a6b7-8901-cdef-012345678901', 3, 'Brüche: Grundbegriffe', 'mixed',
'{"blocks": [
{"type": "text", "data": {"text": "Ein Bruch besteht aus Zähler (oben) und Nenner (unten)"}},
{"type": "flashcard", "data": {"front": "Was ist 1/2 + 1/2?", "back": "1 (oder 2/2)", "hint": "Gleiche Nenner: Addiere die Zähler"}},
{"type": "flashcard", "data": {"front": "Was ist 3/4 in Dezimalform?", "back": "0,75", "hint": "Teile 3 durch 4"}},
{"type": "quiz", "data": {"question": "Welcher Bruch ist größer: 2/3 oder 3/4?", "options": ["2/3", "3/4", "Gleich groß"], "correct_answer": 1, "explanation": "3/4 = 0,75 und 2/3 = 0,67, also ist 3/4 größer"}}
]}'::jsonb,
now(), now()),
-- Card 4: Prozentrechnung
('c2d3e4f5-a6b7-8901-cdef-012345678901', 4, 'Prozentrechnung: Grundformel', 'flashcard',
'{"front": "Wie berechnet man 20% von 150?", "back": "30", "hint": "20% = 20/100 = 0,2. Dann: 0,2 × 150 = 30", "formula": "Prozentwert = Grundwert × Prozentsatz/100", "example": "20% von 150 = 150 × 20/100 = 30"}'::jsonb,
now(), now()),
-- Card 5: Geometrie - Flächenberechnung
('c2d3e4f5-a6b7-8901-cdef-012345678901', 5, 'Geometrie: Rechteck', 'flashcard',
'{"front": "Formel für die Fläche eines Rechtecks?", "back": "A = Länge × Breite", "hint": "Multipliziere die beiden Seitenlängen", "example": "Ein Rechteck mit Länge 5cm und Breite 3cm hat die Fläche: A = 5 × 3 = 15 cm²", "unit": "Fläche wird in Quadrateinheiten gemessen (cm², m²)"}'::jsonb,
now(), now()),
-- Card 6: Kreisberechnung
('c2d3e4f5-a6b7-8901-cdef-012345678901', 6, 'Geometrie: Kreis', 'mixed',
'{"blocks": [
{"type": "text", "data": {"text": "Wichtige Formeln für den Kreis"}},
{"type": "flashcard", "data": {"front": "Umfang eines Kreises", "back": "U = 2 × π × r", "hint": "r ist der Radius, π ≈ 3,14"}},
{"type": "flashcard", "data": {"front": "Fläche eines Kreises", "back": "A = π ×", "hint": "r² bedeutet r × r"}},
{"type": "quiz", "data": {"question": "Was ist der Umfang eines Kreises mit Radius 5cm?", "options": ["15,7 cm", "31,4 cm", "78,5 cm", "25 cm"], "correct_answer": 1, "explanation": "U = 2 × π × r = 2 × 3,14 × 5 = 31,4 cm"}}
]}'::jsonb,
now(), now()),
-- Card 7: Negative Zahlen
('c2d3e4f5-a6b7-8901-cdef-012345678901', 7, 'Negative Zahlen: Rechnen', 'quiz',
'{"question": "Was ist (-5) + 3?", "options": ["-8", "-2", "2", "8"], "correct_answer": 1, "explanation": "Bei (-5) + 3 gehst du von -5 drei Schritte nach rechts auf dem Zahlenstrahl, landest bei -2"}'::jsonb,
now(), now()),
-- Card 8: Potenzen
('c2d3e4f5-a6b7-8901-cdef-012345678901', 8, 'Potenzen: Grundlagen', 'flashcard',
'{"front": "Was ist 2⁴?", "back": "16", "hint": "2⁴ = 2 × 2 × 2 × 2", "explanation": "Eine Potenz ist eine verkürzte Schreibweise für wiederholte Multiplikation", "examples": "2¹ = 2, 2² = 4, 2³ = 8, 2⁴ = 16, 2⁵ = 32"}'::jsonb,
now(), now()),
-- Card 9: Gleichungen lösen
('c2d3e4f5-a6b7-8901-cdef-012345678901', 9, 'Gleichungen: Einfache Lösung', 'flashcard',
'{"front": "Löse: x + 5 = 12", "back": "x = 7", "hint": "Subtrahiere 5 von beiden Seiten", "steps": "x + 5 = 12 | -5 auf beiden Seiten → x = 7", "check": "Probe: 7 + 5 = 12 ✓"}'::jsonb,
now(), now()),
-- Card 10: Dreisatz
('c2d3e4f5-a6b7-8901-cdef-012345678901', 10, 'Dreisatz: Proportional', 'mixed',
'{"blocks": [
{"type": "text", "data": {"text": "Der Dreisatz löst Verhältnisaufgaben"}},
{"type": "flashcard", "data": {"front": "3 Äpfel kosten 2€. Was kosten 9 Äpfel?", "back": "6€", "hint": "9 Äpfel sind 3× so viele, also 3× so teuer"}},
{"type": "text", "data": {"text": "Rechnung: 3 Äpfel → 2€, 1 Apfel → 2€/3, 9 Äpfel → 2€/3 × 9 = 6€"}}
]}'::jsonb,
now(), now()),
-- Card 11: Primzahlen
('c2d3e4f5-a6b7-8901-cdef-012345678901', 11, 'Zahlentheorie: Primzahlen', 'quiz',
'{"question": "Welche dieser Zahlen ist KEINE Primzahl?", "options": ["17", "19", "21", "23"], "correct_answer": 2, "explanation": "21 ist keine Primzahl, da 21 = 3 × 7. Primzahlen sind nur durch 1 und sich selbst teilbar."}'::jsonb,
now(), now()),
-- Card 12: Quadratwurzeln
('c2d3e4f5-a6b7-8901-cdef-012345678901', 12, 'Wurzeln: Quadratwurzel', 'flashcard',
'{"front": "Was ist √64?", "back": "8", "hint": "Welche Zahl mal sich selbst ergibt 64?", "explanation": "√64 = 8, weil 8 × 8 = 64", "more_examples": "√4 = 2, √9 = 3, √16 = 4, √25 = 5, √36 = 6, √49 = 7"}'::jsonb,
now(), now()),
-- Card 13: Mittelwerte
('c2d3e4f5-a6b7-8901-cdef-012345678901', 13, 'Statistik: Durchschnitt', 'flashcard',
'{"front": "Durchschnitt von: 4, 7, 9, 12, 18?", "back": "10", "hint": "Addiere alle Zahlen und teile durch die Anzahl", "calculation": "(4 + 7 + 9 + 12 + 18) ÷ 5 = 50 ÷ 5 = 10", "formula": "Durchschnitt = Summe aller Werte ÷ Anzahl der Werte"}'::jsonb,
now(), now()),
-- Card 14: Winkel
('c2d3e4f5-a6b7-8901-cdef-012345678901', 14, 'Geometrie: Winkelarten', 'mixed',
'{"blocks": [
{"type": "text", "data": {"text": "Verschiedene Winkelarten und ihre Größen"}},
{"type": "flashcard", "data": {"front": "Spitzer Winkel", "back": "0° < Winkel < 90°", "example": "45° ist ein spitzer Winkel"}},
{"type": "flashcard", "data": {"front": "Rechter Winkel", "back": "Genau 90°", "symbol": ""}},
{"type": "flashcard", "data": {"front": "Stumpfer Winkel", "back": "90° < Winkel < 180°", "example": "120° ist ein stumpfer Winkel"}},
{"type": "flashcard", "data": {"front": "Gestreckter Winkel", "back": "Genau 180°", "note": "Eine gerade Linie"}}
]}'::jsonb,
now(), now()),
-- Card 15: Teilbarkeitsregeln
('c2d3e4f5-a6b7-8901-cdef-012345678901', 15, 'Teilbarkeit: Regeln', 'mixed',
'{"blocks": [
{"type": "text", "data": {"text": "Wichtige Teilbarkeitsregeln"}},
{"type": "flashcard", "data": {"front": "Teilbar durch 2", "back": "Wenn die letzte Ziffer gerade ist (0,2,4,6,8)", "example": "124 ist durch 2 teilbar"}},
{"type": "flashcard", "data": {"front": "Teilbar durch 3", "back": "Wenn die Quersumme durch 3 teilbar ist", "example": "123: 1+2+3=6, 6 ist durch 3 teilbar"}},
{"type": "flashcard", "data": {"front": "Teilbar durch 5", "back": "Wenn die letzte Ziffer 0 oder 5 ist", "example": "145 ist durch 5 teilbar"}},
{"type": "flashcard", "data": {"front": "Teilbar durch 9", "back": "Wenn die Quersumme durch 9 teilbar ist", "example": "81: 8+1=9, 9 ist durch 9 teilbar"}}
]}'::jsonb,
now(), now()),
-- Card 16: Römische Zahlen
('c2d3e4f5-a6b7-8901-cdef-012345678901', 16, 'Zahlensysteme: Römische Zahlen', 'quiz',
'{"question": "Was ist XIV in arabischen Zahlen?", "options": ["14", "16", "19", "24"], "correct_answer": 0, "explanation": "XIV = 10 (X) + 4 (IV) = 14. IV bedeutet 5-1=4"}'::jsonb,
now(), now()),
-- Card 17: Volumenberechnung
('c2d3e4f5-a6b7-8901-cdef-012345678901', 17, 'Geometrie: Würfelvolumen', 'flashcard',
'{"front": "Volumen eines Würfels mit Kantenlänge 4cm?", "back": "64 cm³", "formula": "V = a³ (a × a × a)", "calculation": "V = 4³ = 4 × 4 × 4 = 64 cm³", "hint": "Bei einem Würfel sind alle Kanten gleich lang"}'::jsonb,
now(), now()),
-- Card 18: Binomische Formeln
('c2d3e4f5-a6b7-8901-cdef-012345678901', 18, 'Algebra: Binomische Formeln', 'mixed',
'{"blocks": [
{"type": "text", "data": {"text": "Die drei binomischen Formeln"}},
{"type": "flashcard", "data": {"front": "1. Binomische Formel", "back": "(a + b)² = a² + 2ab + b²", "example": "(x + 3)² = x² + 6x + 9"}},
{"type": "flashcard", "data": {"front": "2. Binomische Formel", "back": "(a - b)² = a² - 2ab + b²", "example": "(x - 2)² = x² - 4x + 4"}},
{"type": "flashcard", "data": {"front": "3. Binomische Formel", "back": "(a + b)(a - b) = a² - b²", "example": "(x + 5)(x - 5) = x² - 25"}}
]}'::jsonb,
now(), now()),
-- Card 19: Pythagoras
('c2d3e4f5-a6b7-8901-cdef-012345678901', 19, 'Geometrie: Satz des Pythagoras', 'flashcard',
'{"front": "Satz des Pythagoras", "back": "a² + b² = c²", "hint": "In einem rechtwinkligen Dreieck", "explanation": "a und b sind die Katheten (kurze Seiten), c ist die Hypotenuse (längste Seite)", "example": "Dreieck mit a=3, b=4: c² = 9 + 16 = 25, also c = 5"}'::jsonb,
now(), now()),
-- Card 20: Wahrscheinlichkeit
('c2d3e4f5-a6b7-8901-cdef-012345678901', 20, 'Stochastik: Wahrscheinlichkeit', 'quiz',
'{"question": "Wie groß ist die Wahrscheinlichkeit, mit einem Würfel eine 6 zu würfeln?", "options": ["1/2", "1/3", "1/6", "1/12"], "correct_answer": 2, "explanation": "Ein Würfel hat 6 Seiten, nur eine zeigt die 6. Wahrscheinlichkeit = günstige Fälle / mögliche Fälle = 1/6 ≈ 16,7%"}'::jsonb,
now(), now()),
-- Card 21: Lineare Funktionen
('c2d3e4f5-a6b7-8901-cdef-012345678901', 21, 'Funktionen: Linear', 'flashcard',
'{"front": "Allgemeine Form einer linearen Funktion?", "back": "y = mx + b", "explanation": "m = Steigung, b = y-Achsenabschnitt", "example": "y = 2x + 3 hat Steigung 2 und schneidet die y-Achse bei 3", "graph_info": "Der Graph ist immer eine Gerade"}'::jsonb,
now(), now()),
-- Card 22: Größenumrechnung
('c2d3e4f5-a6b7-8901-cdef-012345678901', 22, 'Einheiten: Längen umrechnen', 'mixed',
'{"blocks": [
{"type": "text", "data": {"text": "Wichtige Längeneinheiten und ihre Umrechnung"}},
{"type": "flashcard", "data": {"front": "1 km = ? m", "back": "1000 m", "hint": "kilo = 1000"}},
{"type": "flashcard", "data": {"front": "1 m = ? cm", "back": "100 cm", "hint": "centi = 1/100"}},
{"type": "flashcard", "data": {"front": "1 cm = ? mm", "back": "10 mm", "hint": "milli = 1/1000"}},
{"type": "quiz", "data": {"question": "2,5 km sind wie viele Meter?", "options": ["25 m", "250 m", "2500 m", "25000 m"], "correct_answer": 2}}
]}'::jsonb,
now(), now()),
-- Card 23: Logarithmus Grundlagen
('c2d3e4f5-a6b7-8901-cdef-012345678901', 23, 'Logarithmus: Einführung', 'flashcard',
'{"front": "Was ist log₁₀(100)?", "back": "2", "hint": "10 hoch was ergibt 100?", "explanation": "log₁₀(100) = 2, weil 10² = 100", "rule": "Der Logarithmus ist die Umkehrfunktion der Potenz"}'::jsonb,
now(), now()),
-- Card 24: Symmetrie
('c2d3e4f5-a6b7-8901-cdef-012345678901', 24, 'Geometrie: Symmetrie', 'quiz',
'{"question": "Wie viele Symmetrieachsen hat ein Quadrat?", "options": ["2", "3", "4", "8"], "correct_answer": 2, "explanation": "Ein Quadrat hat 4 Symmetrieachsen: 2 durch die Mittelpunkte gegenüberliegender Seiten und 2 durch gegenüberliegende Ecken"}'::jsonb,
now(), now()),
-- Card 25: Abschluss-Quiz
('c2d3e4f5-a6b7-8901-cdef-012345678901', 25, 'Abschluss: Gemischtes Quiz', 'quiz',
'{"question": "Ein Rechteck hat einen Umfang von 20cm und eine Länge von 6cm. Wie breit ist es?", "options": ["2 cm", "4 cm", "8 cm", "14 cm"], "correct_answer": 1, "explanation": "Umfang = 2×(Länge + Breite). 20 = 2×(6 + Breite). 10 = 6 + Breite. Breite = 4 cm"}'::jsonb,
now(), now());
-- Update deck metadata with card count
UPDATE public.decks
SET metadata = metadata || '{"card_count": 25}'::jsonb
WHERE id = 'c2d3e4f5-a6b7-8901-cdef-012345678901';
-- Verification: Show what was created
SELECT
d.title,
d.is_public,
d.user_id,
array_length(d.tags, 1) as tag_count,
COUNT(c.id) as card_count
FROM public.decks d
LEFT JOIN public.cards c ON d.id = c.deck_id
WHERE d.id = 'c2d3e4f5-a6b7-8901-cdef-012345678901'
GROUP BY d.id, d.title, d.is_public, d.user_id, d.tags;

View file

@ -0,0 +1,276 @@
# Spaced Repetition System - Dokumentation
## Überblick
Manadeck implementiert ein wissenschaftlich fundiertes **Spaced Repetition System (SRS)** basierend auf dem **SuperMemo 2 (SM-2) Algorithmus**. Dieses System optimiert den Lernprozess durch intelligente Wiederholungsintervalle, die sich an die individuelle Lernleistung anpassen.
## Was ist Spaced Repetition?
Spaced Repetition ist eine Lernmethode, die auf der **Vergessenskurve** von Hermann Ebbinghaus basiert. Das Prinzip:
- Informationen werden in zunehmend größeren Intervallen wiederholt
- Je besser man sich erinnert, desto länger das nächste Intervall
- Schwierige Karten werden öfter wiederholt als leichte
## Der SM-2 Algorithmus
### Grundprinzipien
Der SM-2 Algorithmus berechnet optimale Wiederholungsintervalle basierend auf:
1. **Qualität der Antwort (Q)**: Bewertung von 0-5
2. **Ease Factor (EF)**: Schwierigkeitsgrad der Karte (min. 1.3)
3. **Interval (I)**: Tage bis zur nächsten Wiederholung
4. **Repetitions (R)**: Anzahl erfolgreicher Wiederholungen
### Qualitätsstufen
In Manadeck werden 4 Bewertungsstufen verwendet:
| Button | Qualität (Q) | Bedeutung | Typisches Intervall |
|--------|--------------|-----------|-------------------|
| **Nochmal** | 1 | Antwort vergessen | < 1 Minute |
| **Schwer** | 3 | Mit Mühe erinnert | ~6 Minuten |
| **Gut** | 4 | Normal erinnert | ~10 Minuten / 1 Tag |
| **Leicht** | 5 | Perfekt erinnert | ~4 Tage |
### Algorithmus-Formel
```typescript
// Ease Factor Berechnung
EF' = EF + (0.1 - (5 - Q) * (0.08 + (5 - Q) * 0.02))
EF = max(1.3, EF') // Minimum 1.3
// Intervall-Berechnung
if (Q < 3) {
// Falsche Antwort
R = 0
I = 1 Tag
} else {
// Richtige Antwort
if (R == 0) I = 1 Tag
else if (R == 1) I = 6 Tage
else I = I(prev) * EF
R = R + 1
}
```
## Implementation in Manadeck
### Datenbankstruktur
#### `card_progress` Tabelle
```sql
- id: UUID (Primary Key)
- user_id: UUID (Foreign Key → users)
- card_id: UUID (Foreign Key → cards)
- deck_id: UUID (Foreign Key → decks)
- ease_factor: Decimal (Default: 2.5)
- interval: Integer (Tage)
- repetitions: Integer
- next_review_date: Timestamp
- last_reviewed_at: Timestamp
- total_reviews: Integer
- correct_reviews: Integer
- incorrect_reviews: Integer
- status: Enum ('new', 'learning', 'review', 'relearning')
```
### Karten-Status
1. **new**: Noch nie gelernt
2. **learning**: In Lernphase (Intervall < 10 Tage)
3. **review**: Langzeit-Wiederholung (Intervall ≥ 10 Tage)
4. **relearning**: Nach Fehler zurück in Lernphase
### Lernmodi
#### 1. Alle Karten
- Zeigt alle Karten des Decks
- Standardreihenfolge nach Position
#### 2. Neue Karten
- Nur Karten mit Status `new`
- Noch nie studierte Karten
#### 3. Wiederholung
- Karten mit `next_review_date ≤ heute`
- Sortiert nach Überfälligkeit (älteste zuerst)
#### 4. Favoriten
- Nur als Favorit markierte Karten
#### 5. Zufällig
- Alle Karten in zufälliger Reihenfolge
## Lernablauf
### 1. Session Start
```typescript
1. Deck auswählen
2. Lernmodus wählen
3. Karten werden gefiltert/sortiert
4. Session wird in Datenbank erstellt
```
### 2. Karte lernen
```typescript
1. Karte anzeigen
2. Antwort geben (Flashcard umdrehen, Quiz beantworten)
3. Schwierigkeit bewerten
4. SM-2 berechnet nächstes Review-Datum
5. Progress in Datenbank speichern
```
### 3. Session Ende
```typescript
1. Statistiken berechnen
2. Session in Datenbank aktualisieren
3. Zusammenfassung anzeigen
```
## Beispiel-Lernverlauf
### Tag 1: Erste Begegnung
- Neue Karte lernen
- Bewertung: "Gut" (Q=4)
- Nächste Review: **Morgen**
### Tag 2: Erste Wiederholung
- Bewertung: "Gut" (Q=4)
- EF bleibt bei 2.5
- Nächste Review: **In 6 Tagen**
### Tag 8: Zweite Wiederholung
- Bewertung: "Leicht" (Q=5)
- EF steigt auf 2.6
- Nächste Review: **In 15 Tagen** (6 * 2.6)
### Tag 23: Dritte Wiederholung
- Bewertung: "Schwer" (Q=3)
- EF sinkt auf 2.36
- Nächste Review: **In 35 Tagen** (15 * 2.36)
## Vorteile des Systems
### 1. **Effizienz**
- Minimaler Zeitaufwand
- Fokus auf schwierige Karten
- Automatische Intervall-Anpassung
### 2. **Langzeit-Retention**
- Optimale Wiederholungszeitpunkte
- Bekämpft die Vergessenskurve
- Nachweislich 90%+ Retention möglich
### 3. **Personalisierung**
- Individuelle Ease Factors pro Karte
- Anpassung an Lerngeschwindigkeit
- Berücksichtigt Schwierigkeitsgrad
## Code-Referenzen
### Hauptdateien
1. **`utils/spacedRepetition.ts`**
- SM-2 Algorithmus Implementation
- Hilfsfunktionen für Intervalle
2. **`store/studyStore.ts`**
- Progress Management
- Session Verwaltung
- Supabase Integration
3. **`app/study/session/[id].tsx`**
- Lern-UI mit Bewertungsbuttons
- Intervall-Vorschau
4. **Datenbank-Migrationen**
- `create_card_progress_table`
- `create_study_sessions_table`
### Wichtige Funktionen
```typescript
// Hauptfunktion für SM-2 Berechnung
calculateSM2(quality, repetitions, previousInterval, previousEaseFactor)
// Schwierigkeit zu Qualität konvertieren
difficultyToQuality(difficulty: 'easy' | 'medium' | 'hard' | 'again')
// Prüfen ob Karte fällig ist
isCardDue(nextReviewDate: Date | string)
// Review-Queue organisieren
organizeReviewQueue(cards: CardProgress[])
```
## Best Practices
### Für Nutzer
1. **Ehrliche Bewertung**: Bewerte Karten ehrlich für optimale Intervalle
2. **Tägliches Lernen**: Regelmäßigkeit ist wichtiger als Intensität
3. **Reviews priorisieren**: Fällige Reviews vor neuen Karten
### Für Entwickler
1. **Ease Factor Grenzen**: Nie unter 1.3 fallen lassen
2. **Timezone-Handling**: Reviews um 4 Uhr morgens zurücksetzen
3. **Progress Backup**: Regelmäßig in Datenbank persistieren
## Wissenschaftliche Grundlagen
### Forschung
- Basiert auf Piotr Wozniak's SuperMemo Forschung (1987)
- Ebbinghaus Vergessenskurve (1885)
- Cognitive Science Prinzipien
### Effektivität
- **Studien zeigen**: 50% weniger Lernzeit bei gleicher Retention
- **Langzeit-Retention**: Bis zu 95% nach 1 Jahr möglich
- **Optimal für**: Vokabeln, Fakten, Formeln, Konzepte
## Vergleich mit anderen Systemen
| Feature | Manadeck | Anki | Quizlet |
|---------|----------|------|---------|
| Algorithmus | SM-2 | SM-2+ | Proprietary |
| Open Source | ✅ | ✅ | ❌ |
| Cloud Sync | ✅ | Partial | ✅ |
| Mobile App | ✅ | ✅ | ✅ |
| Kostenlos | ✅ | ✅ | Freemium |
## Zukünftige Verbesserungen
### Geplant
- [ ] FSRS (Free Spaced Repetition Scheduler) als Alternative
- [ ] Machine Learning für personalisierte Intervalle
- [ ] Heatmap-Kalender für Lernstreak
- [ ] Detaillierte Statistiken pro Karte
- [ ] Import/Export von Anki-Decks
### Experimentell
- [ ] Adaptive Tageszeit-Optimierung
- [ ] Kontext-basierte Schwierigkeit
- [ ] Gruppen-Lernstatistiken
## Troubleshooting
### Problem: Zu viele Reviews
**Lösung**: Tägliches Limit einführen, schwierige Karten vereinfachen
### Problem: Karten werden zu schnell vergessen
**Lösung**: Ease Factor manuell reduzieren, mehr Kontext hinzufügen
### Problem: Intervalle zu lang
**Lösung**: Bei Unsicherheit "Schwer" statt "Gut" wählen
## Fazit
Das Spaced Repetition System in Manadeck bietet eine wissenschaftlich fundierte, effiziente Methode zum Langzeit-Lernen. Durch die SM-2 Implementation erreichen Nutzer optimale Lernresultate mit minimalem Zeitaufwand.
---
*Letzte Aktualisierung: November 2024*
*Version: 1.0.0*

View file

@ -0,0 +1,139 @@
# Decks erstellen in Manadeck
## Übersicht
Manadeck ermöglicht es Benutzern, eigene Lernkarten-Decks zu erstellen und zu verwalten. Diese Anleitung zeigt dir, wie du neue Decks erstellen und konfigurieren kannst.
## Deck-Erstellung Schritt für Schritt
### 1. Deck-Erstellung starten
Es gibt mehrere Wege, ein neues Deck zu erstellen:
- **Home-Seite**: Klicke auf den "Neues Deck erstellen" Button im Schnellstart-Bereich
- **Decks-Seite**: Wenn noch keine Decks vorhanden sind, klicke auf "Deck erstellen"
- **Direkter Weg**: Navigiere zu `/deck/create`
### 2. Deck-Informationen eingeben
Beim Erstellen eines neuen Decks musst du folgende Informationen angeben:
#### Grunddaten
- **Titel**: Ein aussagekräftiger Name für dein Deck
- **Beschreibung**: Eine kurze Erklärung, worum es in dem Deck geht
- **Kategorie**: Wähle eine passende Kategorie (Sprachen, Wissenschaft, Mathematik, etc.)
#### Sichtbarkeits-Einstellungen
- **Privat**: Nur du kannst das Deck sehen und verwenden
- **Öffentlich**: Andere Benutzer können dein Deck in der Explore-Sektion finden
### 3. Karten hinzufügen
Nach der Deck-Erstellung kannst du Karten hinzufügen:
#### Kartentypen
Manadeck unterstützt verschiedene Kartentypen:
1. **Text-Karten**: Klassische Vorderseite/Rückseite Karten
2. **Bild-Karten**: Karten mit Bildern
3. **Audio-Karten**: Karten mit Audiodateien
4. **Multiple-Choice**: Karten mit Auswahlmöglichkeiten
#### Karten-Erstellung
- **Vorderseite**: Die Frage oder der Begriff
- **Rückseite**: Die Antwort oder Erklärung
- **Hinweise**: Optionale Hilfestellungen
- **Tags**: Zur besseren Organisation und Suche
### 4. KI-unterstützte Erstellung
Manadeck bietet KI-Features zur Deck-Erstellung:
#### Smart Card Creator
- Automatische Generierung von Karten basierend auf einem Thema
- KI schlägt passende Fragen und Antworten vor
- Unterstützt verschiedene Schwierigkeitsgrade
#### Image Card Creator
- Erstellung von Bildkarten mit KI-generierten oder hochgeladenen Bildern
- Automatische Beschreibungen und Fragen zu Bildern
### 5. Deck-Verwaltung
#### Einstellungen
- **Metadaten bearbeiten**: Titel, Beschreibung, Kategorie ändern
- **Sichtbarkeit**: Zwischen privat und öffentlich wechseln
- **Tags**: Deck-Tags für bessere Organisation
#### Organisation
- **Favoriten**: Wichtige Decks als Favoriten markieren
- **Sortierung**: Decks nach verschiedenen Kriterien sortieren
- **Suchfunktion**: Decks schnell finden
## Best Practices
### Deck-Design
1. **Klare Titel**: Verwende aussagekräftige und spezifische Titel
2. **Gute Beschreibungen**: Erkläre kurz, was gelernt wird und für wen das Deck geeignet ist
3. **Konsistente Struktur**: Halte das Format der Karten einheitlich
4. **Angemessene Größe**: 20-50 Karten pro Deck für optimales Lernen
### Karten-Gestaltung
1. **Eine Idee pro Karte**: Jede Karte sollte nur ein Konzept behandeln
2. **Klare Formulierungen**: Vermeide mehrdeutige Fragen
3. **Sinnvolle Reihenfolge**: Ordne Karten logisch an
4. **Regelmäßige Updates**: Überarbeite und verbessere deine Karten
### Öffentliche Decks
Wenn du dein Deck öffentlich machst:
1. **Qualität sicherstellen**: Überprüfe alle Karten auf Richtigkeit
2. **Vollständigkeit**: Stelle sicher, dass das Deck ein Thema vollständig abdeckt
3. **Zielgruppe definieren**: Gib an, für wen das Deck geeignet ist
4. **Lizenz beachten**: Verwende nur Inhalte, die du verwenden darfst
## Technische Details
### Datenbankstruktur
Decks werden in der `decks` Tabelle gespeichert mit folgenden Feldern:
- `id`: Eindeutige ID
- `title`: Deck-Titel
- `description`: Beschreibung
- `is_public`: Sichtbarkeits-Flag
- `user_id`: Ersteller-ID
- `category`: Kategorie
- `created_at`: Erstellungsdatum
- `metadata`: Zusätzliche Informationen (JSON)
### API-Endpunkte
- `POST /api/decks`: Neues Deck erstellen
- `PUT /api/decks/:id`: Deck bearbeiten
- `DELETE /api/decks/:id`: Deck löschen
- `GET /api/decks/public`: Öffentliche Decks abrufen
## Fehlerbehebung
### Häufige Probleme
1. **Deck wird nicht gespeichert**
- Überprüfe die Internetverbindung
- Stelle sicher, dass alle Pflichtfelder ausgefüllt sind
2. **Karten werden nicht angezeigt**
- Lade die Seite neu
- Überprüfe, ob die Karten korrekt gespeichert wurden
3. **Bilder werden nicht hochgeladen**
- Überprüfe die Dateigröße (max. 5MB)
- Unterstützte Formate: JPG, PNG, WebP
## Support
Bei Problemen oder Fragen:
- Überprüfe diese Dokumentation
- Schaue in die FAQ-Sektion
- Kontaktiere den Support über die App
---
*Letzte Aktualisierung: 2025-09-24*

View file

@ -0,0 +1,242 @@
# Vorschläge für öffentliche Decks
## Übersicht
Diese Liste enthält Vorschläge für hochwertige öffentliche Decks, die der Manadeck-Community zugute kommen würden. Diese Decks können von Admins oder erfahrenen Benutzern erstellt und als Startinhalt für die Plattform verwendet werden.
## 🌍 Sprachen
### Deutsch für Anfänger
- **Zielgruppe**: Deutschlernende (A1-A2 Niveau)
- **Inhalt**: Grundwortschatz, häufige Phrasen, Zahlen, Farben
- **Kartenzahl**: ~100 Karten
- **Besonderheiten**: Audio-Karten für Aussprache
### English Business Vocabulary
- **Zielgruppe**: Berufstätige, Studenten
- **Inhalt**: Geschäftsvokabular, Meeting-Phrasen, E-Mail-Formulierungen
- **Kartenzahl**: ~75 Karten
- **Besonderheiten**: Kontextuelle Beispiele
### Französische Verben (Présent)
- **Zielgruppe**: Französischlernende (A2-B1)
- **Inhalt**: Konjugation häufiger Verben im Präsens
- **Kartenzahl**: ~80 Karten
- **Besonderheiten**: Irregular verbs highlighted
### Spanisch: Reisevokabular
- **Zielgruppe**: Reisende, Touristen
- **Inhalt**: Hotel, Restaurant, Transport, Notfälle
- **Kartenzahl**: ~60 Karten
- **Besonderheiten**: Situative Phrasen
## 🧬 Wissenschaft
### Grundlagen der Chemie
- **Zielgruppe**: Schüler, Studienanfänger
- **Inhalt**: Periodensystem, chemische Formeln, Grundreaktionen
- **Kartenzahl**: ~90 Karten
- **Besonderheiten**: Strukturformeln als Bilder
### Anatomie: Skelett-System
- **Zielgruppe**: Medizinstudenten, Interessierte
- **Inhalt**: Knochen, Gelenke, anatomische Begriffe
- **Kartenzahl**: ~120 Karten
- **Besonderheiten**: Anatomische Diagramme
### Physik: Mechanik Formeln
- **Zielgruppe**: Schüler, Studenten
- **Inhalt**: Grundformeln der Mechanik, Einheiten, Konstanten
- **Kartenzahl**: ~50 Karten
- **Besonderheiten**: Formel-Karten mit Erklärungen
### Biologie: Zellbiologie
- **Zielgruppe**: Schüler, Biologiestudenten
- **Inhalt**: Zellorganellen, Funktionen, Prozesse
- **Kartenzahl**: ~70 Karten
- **Besonderheiten**: Mikroskopische Bilder
## 🧮 Mathematik
### Grundrechenarten für Kinder
- **Zielgruppe**: Grundschüler (6-10 Jahre)
- **Inhalt**: Addition, Subtraktion, kleines Einmaleins
- **Kartenzahl**: ~100 Karten
- **Besonderheiten**: Bunte, kinderfreundliche Gestaltung
### Algebra: Grundformeln
- **Zielgruppe**: Schüler (7.-10. Klasse)
- **Inhalt**: Binomische Formeln, Gleichungen, Terme
- **Kartenzahl**: ~45 Karten
- **Besonderheiten**: Schritt-für-Schritt Lösungen
### Geometrie: Flächenberechnung
- **Zielgruppe**: Schüler, Handwerker
- **Inhalt**: Formeln für Kreis, Dreieck, Rechteck, etc.
- **Kartenzahl**: ~30 Karten
- **Besonderheiten**: Visualisierungen der Formeln
### Statistik Grundlagen
- **Zielgruppe**: Studenten, Berufstätige
- **Inhalt**: Mittelwert, Median, Standardabweichung
- **Kartenzahl**: ~40 Karten
- **Besonderheiten**: Praktische Beispiele
## 🏛️ Geschichte
### Deutsche Geschichte: 20. Jahrhundert
- **Zielgruppe**: Schüler, Geschichtsinteressierte
- **Inhalt**: Wichtige Ereignisse, Personen, Daten
- **Kartenzahl**: ~80 Karten
- **Besonderheiten**: Historische Fotos
### Antikes Rom
- **Zielgruppe**: Geschichtsstudenten, Interessierte
- **Inhalt**: Kaiser, Schlachten, Kultur, Architektur
- **Kartenzahl**: ~100 Karten
- **Besonderheiten**: Karten und Abbildungen
### Zweiter Weltkrieg: Schlüsselereignisse
- **Zielgruppe**: Oberstufenschüler, Studenten
- **Inhalt**: Wichtige Schlachten, Wendepunkte, Personen
- **Kartenzahl**: ~60 Karten
- **Besonderheiten**: Chronologische Anordnung
## 🎨 Kunst & Kultur
### Berühmte Gemälde und ihre Maler
- **Zielgruppe**: Kunstinteressierte, Studenten
- **Inhalt**: Meisterwerke, Künstler, Epochen
- **Kartenzahl**: ~75 Karten
- **Besonderheiten**: Hochauflösende Kunstwerke
### Klassische Musik: Komponisten
- **Zielgruppe**: Musikliebhaber, Studenten
- **Inhalt**: Biografien, wichtige Werke, Epochen
- **Kartenzahl**: ~50 Karten
- **Besonderheiten**: Audio-Snippets der Werke
### Architektur-Stile
- **Zielgruppe**: Architekturstudenten, Interessierte
- **Inhalt**: Gotik, Renaissance, Bauhaus, etc.
- **Kartenzahl**: ~40 Karten
- **Besonderheiten**: Beispielgebäude mit Fotos
## 💻 Technologie
### HTML & CSS Grundlagen
- **Zielgruppe**: Webentwicklung-Anfänger
- **Inhalt**: Tags, Properties, Selektoren
- **Kartenzahl**: ~85 Karten
- **Besonderheiten**: Code-Beispiele
### JavaScript: Array-Methoden
- **Zielgruppe**: Programmier-Anfänger
- **Inhalt**: map, filter, reduce, forEach, etc.
- **Kartenzahl**: ~25 Karten
- **Besonderheiten**: Praktische Beispiele
### Git-Befehle
- **Zielgruppe**: Entwickler, Studenten
- **Inhalt**: Häufige Git-Commands und ihre Funktionen
- **Kartenzahl**: ~35 Karten
- **Besonderheiten**: Terminal-Syntax
### Cybersecurity Basics
- **Zielgruppe**: IT-Interessierte, Berufstätige
- **Inhalt**: Begriffe, Bedrohungen, Schutzmaßnahmen
- **Kartenzahl**: ~55 Karten
- **Besonderheiten**: Aktuelle Beispiele
## 🏥 Medizin & Gesundheit
### Erste Hilfe
- **Zielgruppe**: Allgemeine Bevölkerung
- **Inhalt**: Notfallmaßnahmen, Telefonnummern, Handlungsschritte
- **Kartenzahl**: ~40 Karten
- **Besonderheiten**: Lebensrettende Informationen
### Medikamente: Häufige Wirkstoffe
- **Zielgruppe**: Medizinstudenten, Pflegekräfte
- **Inhalt**: Wirkstoffnamen, Anwendungsgebiete, Nebenwirkungen
- **Kartenzahl**: ~60 Karten
- **Besonderheiten**: Systematische Kategorisierung
### Anatomie: Herz-Kreislauf-System
- **Zielgruppe**: Medizinstudenten
- **Inhalt**: Herzfunktion, Blutkreislauf, Erkrankungen
- **Kartenzahl**: ~80 Karten
- **Besonderheiten**: Medizinische Diagramme
## 🌱 Nachhaltigkeit & Umwelt
### Klimawandel: Grundlagen
- **Zielgruppe**: Schüler, umweltbewusste Menschen
- **Inhalt**: Ursachen, Folgen, Lösungsansätze
- **Kartenzahl**: ~50 Karten
- **Besonderheiten**: Aktuelle Daten und Grafiken
### Recycling & Müllvermeidung
- **Zielgruppe**: Haushalte, umweltbewusste Menschen
- **Inhalt**: Mülltrennung, Wiederverwertung, Tipps
- **Kartenzahl**: ~30 Karten
- **Besonderheiten**: Praktische Alltagstipps
## 🍳 Alltag & Freizeit
### Kochen: Grundtechniken
- **Zielgruppe**: Kochanfänger
- **Inhalt**: Schneidetechniken, Garmethoden, Begriffe
- **Kartenzahl**: ~45 Karten
- **Besonderheiten**: Step-by-step Anleitungen
### Gartenpflege: Saisonkalender
- **Zielgruppe**: Hobbygärtner
- **Inhalt**: Pflanzzeiten, Pflegetipps, häufige Pflanzen
- **Kartenzahl**: ~65 Karten
- **Besonderheiten**: Monatsbasierte Organisation
### Finanzbildung: Grundlagen
- **Zielgruppe**: Junge Erwachsene, Finanzinteressierte
- **Inhalt**: Sparen, Investieren, Versicherungen
- **Kartenzahl**: ~50 Karten
- **Besonderheiten**: Praktische Tipps und Beispiele
## Implementierungsrichtlinien
### Qualitätsstandards
1. **Faktische Richtigkeit**: Alle Informationen müssen korrekt und aktuell sein
2. **Quellenangaben**: Bei wissenschaftlichen/historischen Inhalten Quellen angeben
3. **Altersangemessenheit**: Inhalte an die Zielgruppe anpassen
4. **Rechtliche Aspekte**: Urheberrechte bei Bildern und Inhalten beachten
### Technische Anforderungen
1. **Einheitliche Formatierung**: Konsistente Kartenerstellung
2. **Multimedia**: Bilder in hoher Qualität, Audio in guter Qualität
3. **Kategorisierung**: Richtige Zuordnung zu Kategorien
4. **Tags**: Aussagekräftige Tags für bessere Auffindbarkeit
### Priorisierung
**Phase 1** (Sofort umsetzen):
- Deutsch für Anfänger
- HTML & CSS Grundlagen
- Erste Hilfe
- Grundrechenarten für Kinder
**Phase 2** (Nach Nutzerfeedback):
- English Business Vocabulary
- Deutsche Geschichte: 20. Jahrhundert
- Grundlagen der Chemie
**Phase 3** (Erweiterung):
- Spezialisierte Decks basierend auf Nutzeranfragen
## Wartung und Updates
- **Regelmäßige Überprüfung**: Halbjährliche Kontrolle auf Aktualität
- **Community-Feedback**: Nutzervorschläge für Verbesserungen
- **Erweiterungen**: Neue Karten basierend auf Lernfortschritt-Daten
---
*Letzte Aktualisierung: 2025-09-24*

View file

@ -0,0 +1,522 @@
# Social Authentication Setup Guide for Manadeck
This guide explains how to set up Google Sign-In and Apple Sign-In for the Manadeck mobile app.
## Overview
Manadeck uses Mana Core authentication middleware which supports:
- Email/Password authentication
- Google Sign-In (Android & iOS)
- Apple Sign-In (iOS only)
All authentication methods issue JWT tokens that work with your Supabase backend and respect Row-Level Security (RLS) policies.
## Prerequisites
### Required Accounts and Credentials
1. **Google Cloud Console Account**
- OAuth 2.0 credentials for Android and iOS
- Web Client ID (for Android)
- iOS Client ID (for iOS)
2. **Apple Developer Account**
- Sign in with Apple capability enabled
- App ID configured with Sign in with Apple
3. **Mana Core Backend**
- Backend must support `/v1/auth/google-signin` and `/v1/auth/apple-signin` endpoints
- Backend URL configured in `.env.local`
## Installation
The required dependencies are already installed:
- `@react-native-google-signin/google-signin@^14.0.1`
- `expo-apple-authentication@~8.0.7`
- `base64-js`
## Configuration Steps
### 1. Google Cloud Console Setup
#### Create OAuth Credentials
1. Go to [Google Cloud Console](https://console.cloud.google.com)
2. Create a new project or select existing project
3. Navigate to **APIs & Services → Credentials**
#### For Android:
1. Click **Create Credentials → OAuth 2.0 Client ID**
2. Select **Android** as application type
3. Fill in:
- **Package name**: `com.tilljs.manadeck`
- **SHA-1 certificate fingerprint**: Get from `cd android && ./gradlew signingReport`
4. Click **Create**
#### For iOS:
1. Click **Create Credentials → OAuth 2.0 Client ID**
2. Select **iOS** as application type
3. Fill in:
- **Bundle ID**: `com.tilljs.manadeck`
4. Click **Create**
5. **Save the iOS Client ID** (format: `XXXXX.apps.googleusercontent.com`)
#### For Web (Required for Android):
1. Click **Create Credentials → OAuth 2.0 Client ID**
2. Select **Web application** as application type
3. Fill in:
- **Name**: Manadeck Web (for Android)
4. Click **Create**
5. **Save the Web Client ID** (format: `XXXXX.apps.googleusercontent.com`)
### 2. Apple Developer Console Setup
#### Enable Sign in with Apple
1. Go to [Apple Developer Portal](https://developer.apple.com/account)
2. Navigate to **Certificates, Identifiers & Profiles**
3. Click **Identifiers** in the sidebar
4. Find and select your App ID: `com.tilljs.manadeck`
5. In the **Capabilities** section, find **Sign in with Apple**
6. Check the **Sign in with Apple** checkbox
7. Click **Save**
#### Configure in Xcode (Optional but Recommended)
1. Open your iOS project:
```bash
cd /Users/tillschneider/Documents/__00__Code/manadeck/apps/mobile
open ios/manadeck.xcworkspace
```
2. Select your project target in the left sidebar
3. Go to **Signing & Capabilities** tab
4. Click **+ Capability** button
5. Add **Sign in with Apple**
6. Ensure proper signing team is selected (Team ID: QP3GLU8PH3)
### 3. Update Environment Variables
Edit `/Users/tillschneider/Documents/__00__Code/manadeck/apps/mobile/.env.local`:
```bash
# Existing Supabase Configuration
EXPO_PUBLIC_SUPABASE_URL=https://vksoodohrbjwyloitvsz.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=sb_publishable_2ndX-kBHFpbDlL_ZeeOnfQ_ZlLI8ONk
# Existing Backend API Configuration
EXPO_PUBLIC_API_URL=https://manadeck-backend-pduya7fsoq-ey.a.run.app
# Google OAuth Configuration (ADD THESE)
# Web Client ID (used for Android authentication)
EXPO_PUBLIC_GOOGLE_CLIENT_ID=YOUR_WEB_CLIENT_ID.apps.googleusercontent.com
# iOS Client ID (used for iOS authentication)
EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID=YOUR_IOS_CLIENT_ID.apps.googleusercontent.com
```
**Replace the placeholder values with your actual OAuth Client IDs from Google Cloud Console.**
### 4. Update app.json
The `app.json` has already been configured with:
```json
{
"expo": {
"ios": {
"config": {
"googleSignIn": {
"reservedClientId": "com.googleusercontent.apps.PLACEHOLDER_IOS_CLIENT_ID"
}
},
"usesAppleSignIn": true
},
"plugins": [
"expo-router",
"@react-native-google-signin/google-signin",
"expo-apple-authentication"
]
}
}
```
**Important**: Replace `PLACEHOLDER_IOS_CLIENT_ID` in `app.json` with your actual iOS Client ID (without the `com.googleusercontent.apps.` prefix).
For example, if your iOS Client ID is `123456789-abc123.apps.googleusercontent.com`, the `reservedClientId` should be:
```json
"reservedClientId": "com.googleusercontent.apps.123456789-abc123"
```
### 5. Rebuild Native Code
After updating `app.json` and environment variables, rebuild the native code:
```bash
cd /Users/tillschneider/Documents/__00__Code/manadeck/apps/mobile
# For iOS
npx expo prebuild --clean
npx expo run:ios
# For Android
npx expo prebuild --clean
npx expo run:android
```
### 6. **IMPORTANT**: Manually Add iOS URL Scheme
**⚠️ Critical Step**: The Expo plugin may not automatically add the Google Sign-In URL scheme to iOS. You must verify and add it manually.
#### Verify and Add URL Scheme to Info.plist
1. Open `ios/manadeck/Info.plist`
2. Find the `CFBundleURLTypes` array
3. Add a new URL scheme entry for Google Sign-In:
```xml
<key>CFBundleURLTypes</key>
<array>
<!-- Existing URL schemes -->
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>manadeck</string>
<string>com.tilljs.manadeck</string>
</array>
</dict>
<!-- Add this new entry for Google Sign-In -->
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.111768794939-cgen6eklloo7k8vppcaq01o8r8nd7anb</string>
</array>
</dict>
</array>
```
**Replace** `111768794939-cgen6eklloo7k8vppcaq01o8r8nd7anb` with your actual iOS Client ID.
#### Why This is Required
Without this URL scheme, Google Sign-In will fail with:
```
Your app is missing support for the following URL schemes:
com.googleusercontent.apps.XXXXX
```
iOS uses this URL scheme to redirect back to your app after Google authentication.
## Backend Requirements
Your Mana Core backend must support the following endpoints:
### POST `/v1/auth/google-signin`
**⚠️ Important**: The backend expects `deviceInfo` as a **nested object**, not spread at the root level.
Request body:
```json
{
"token": "GOOGLE_ID_TOKEN",
"deviceInfo": {
"deviceId": "device-uuid",
"deviceName": "iPhone 15 Pro",
"deviceType": "ios"
}
}
```
Response:
```json
{
"appToken": "JWT_TOKEN",
"refreshToken": "REFRESH_TOKEN"
}
```
### POST `/v1/auth/apple-signin`
**⚠️ Important**: The backend expects `deviceInfo` as a **nested object**, not spread at the root level.
Request body:
```json
{
"token": "APPLE_IDENTITY_TOKEN",
"deviceInfo": {
"deviceId": "device-uuid",
"deviceName": "iPhone 15 Pro",
"deviceType": "ios"
}
}
```
Response:
```json
{
"appToken": "JWT_TOKEN",
"refreshToken": "REFRESH_TOKEN"
}
```
**Common Error**: If you send device info spread at root level instead of nested:
```json
// ❌ WRONG - Will fail with "Complete device information is required"
{
"token": "...",
"deviceId": "...",
"deviceName": "...",
"deviceType": "ios"
}
// ✅ CORRECT - deviceInfo as nested object
{
"token": "...",
"deviceInfo": {
"deviceId": "...",
"deviceName": "...",
"deviceType": "ios"
}
}
```
## Testing
### Test on iOS
1. Build and run:
```bash
cd /Users/tillschneider/Documents/__00__Code/manadeck/apps/mobile
npx expo run:ios
```
2. Test Google Sign-In:
- Tap "Mit Google anmelden"
- Should show Google account picker
- Select account
- Should authenticate and navigate to home
3. Test Apple Sign-In:
- Tap "Mit Apple anmelden"
- Should show Face ID/Touch ID prompt
- Authenticate
- Should navigate to home
### Test on Android
1. Build and run:
```bash
npx expo run:android
```
2. Test Google Sign-In:
- Tap "Mit Google anmelden"
- Should show Google account picker
- Select account
- Should authenticate and navigate to home
3. Apple Sign-In won't show on Android (iOS only)
## Project Structure
The social authentication implementation includes:
```
manadeck/apps/mobile/
├── services/
│ └── authService.ts # Extended with Google/Apple methods
├── store/
│ └── authStore.ts # Updated with social sign-in actions
├── components/
│ └── auth/
│ ├── GoogleSignInButton.tsx # Google Sign-In button component
│ └── AppleSignInButton.tsx # Apple Sign-In button component
├── app/
│ └── (auth)/
│ └── login.tsx # Updated with social buttons
├── app.json # Configured with plugins
└── .env.local # OAuth credentials
```
## Troubleshooting
### Google Sign-In Issues
#### "Your app is missing support for the following URL schemes" on iOS
**Cause**: Google Sign-In URL scheme not added to Info.plist
**Solution**:
1. Open `ios/manadeck/Info.plist`
2. Add the Google Sign-In URL scheme to `CFBundleURLTypes`:
```xml
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.YOUR_IOS_CLIENT_ID</string>
</array>
</dict>
```
3. Replace `YOUR_IOS_CLIENT_ID` with your actual iOS Client ID
4. Rebuild the app: `npx expo run:ios`
#### "DEVELOPER_ERROR" on Android
**Cause**: SHA-1 fingerprint mismatch or incorrect web client ID
**Solution**:
```bash
# Get your SHA-1
cd android && ./gradlew signingReport
# Add SHA-1 to Google Cloud Console
# Use Web Client ID (not Android Client ID) in EXPO_PUBLIC_GOOGLE_CLIENT_ID
```
#### "Sign-in failed" on iOS
**Cause**: Incorrect iOS Client ID or bundle identifier mismatch
**Solution**:
- Verify `EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID` matches Google Cloud Console
- Verify bundle identifier in `app.json` matches Google Cloud Console
- Update `reservedClientId` in `app.json` with correct iOS Client ID
#### "Play Services not available"
**Cause**: Google Play Services missing or outdated on Android
**Solution**:
- Update Google Play Services on device
- Test on device with Google Play Store installed
- Use emulator with Google Play
### Apple Sign-In Issues
#### Button doesn't appear
**Cause**: Platform is not iOS
**Solution**: Apple Sign-In only works on iOS 13+
#### "Operation canceled" every time
**Cause**: Sign in with Apple not enabled in developer portal
**Solution**:
- Enable capability in Apple Developer Portal
- Add capability in Xcode
- Ensure `usesAppleSignIn: true` in `app.json`
### Backend Connection Issues
#### "Complete device information is required"
**Cause**: Device info sent incorrectly (spread at root instead of nested object)
**Solution**:
Ensure your authService sends `deviceInfo` as a nested object:
```typescript
// ✅ Correct
body: JSON.stringify({
token: idToken,
deviceInfo, // Nested object
})
// ❌ Wrong
body: JSON.stringify({
token: idToken,
...deviceInfo, // Spread at root - will fail
})
```
#### "Network error" or timeout
**Cause**: Backend URL incorrect or unreachable
**Solution**:
- Verify `EXPO_PUBLIC_API_URL` is correct
- Test backend health endpoint
- Check network connectivity
- Ensure HTTPS is used (not HTTP)
#### "Invalid token" or "Authentication failed"
**Cause**: Backend endpoints not implemented or configured incorrectly
**Solution**:
- Verify backend supports `/v1/auth/google-signin` and `/v1/auth/apple-signin`
- Check backend logs for detailed error messages
- Ensure backend validates tokens correctly with Google/Apple
## Security Considerations
1. **Never commit credentials**: Keep `.env.local` in `.gitignore`
2. **Use HTTPS only**: Always use HTTPS for backend communication
3. **Validate tokens on backend**: Always validate social tokens on the backend
4. **Device binding**: Tokens are bound to device IDs for security
5. **Secure storage**: Tokens stored using platform-specific secure storage
## Production Setup
### For Production Release
**Android Production SHA-1:**
When ready to publish to Google Play, get your production SHA-1:
```bash
# For EAS builds
eas credentials -p android
# Or if using your own keystore
keytool -list -v -keystore /path/to/release.keystore -alias YOUR_KEY_ALIAS
```
Add the production SHA-1 to your Android OAuth client in Google Cloud Console.
**iOS Production:**
The same iOS Client ID works for both development and production.
## Additional Resources
- [React Native Google Sign-In Documentation](https://github.com/react-native-google-signin/google-signin)
- [Expo Apple Authentication Documentation](https://docs.expo.dev/versions/latest/sdk/apple-authentication/)
- [Google OAuth 2.0 Documentation](https://developers.google.com/identity/protocols/oauth2)
- [Apple Sign In Guidelines](https://developer.apple.com/sign-in-with-apple/)
## Quick Reference
### Your Credentials (Fill in as you obtain them)
```
Web OAuth Client ID: ________________________________.apps.googleusercontent.com
iOS OAuth Client ID: ________________________________.apps.googleusercontent.com
Android SHA-1 (Debug): ________________________________
Android SHA-1 (Prod): ________________________________
Apple Team ID: QP3GLU8PH3
Bundle ID: com.tilljs.manadeck
Package Name: com.tilljs.manadeck
```
### Important Commands
```bash
# Get Android SHA-1
cd android && ./gradlew signingReport
# Rebuild native code
npx expo prebuild --clean
# Run on iOS
npx expo run:ios
# Run on Android
npx expo run:android
# Build with EAS
eas build --profile development
```
## Support
For issues or questions:
1. Check the troubleshooting section above
2. Review backend logs for detailed error messages
3. Consult Google Cloud Console and Apple Developer Portal documentation
4. Check that all credentials are correctly configured
---
**Integration completed successfully!** Social authentication is now available in the Manadeck mobile app.

View file

@ -0,0 +1,126 @@
# Spacing System Migration - Completed ✅
## Zentrales Spacing-System
Alle Abstände in der App werden jetzt zentral über `utils/spacing.ts` verwaltet.
### Import
```tsx
import { spacing } from '~/utils/spacing';
```
### Verwendung
#### Container/Screen Padding
```tsx
<View style={{ paddingHorizontal: spacing.container.horizontal, paddingVertical: spacing.container.vertical }}>
```
#### Section Spacing (zwischen Cards)
```tsx
<Card style={{ marginBottom: spacing.section }}>
```
#### Content Spacing
```tsx
// Titel Abstand
<Text style={{ marginBottom: spacing.content.title }}>Titel</Text>
// Item Abstand
<View style={{ gap: spacing.content.item }}>
// Kleine Abstände
<Text style={{ marginTop: spacing.content.small }}>
```
## Migration Status - Alle Screens ✅
### Tab Screens (Main Navigation)
✅ app/(tabs)/decks/index.tsx
✅ app/(tabs)/explore/index.tsx
✅ app/(tabs)/progress/index.tsx
✅ app/(tabs)/profile/index.tsx
### Deck Screens
✅ app/deck/[id].tsx (Detail Screen)
✅ app/deck/create.tsx
✅ app/deck/[id]/edit.tsx
### Card Screens
✅ app/card/create.tsx
✅ app/card/edit/[id].tsx
### Study Screens
✅ app/study/session/[id].tsx
✅ app/study/summary/[id].tsx
### Auth Screens
✅ app/(auth)/login.tsx
✅ app/(auth)/register.tsx
✅ app/(auth)/forgot-password.tsx
## Spacing Reference
```typescript
spacing = {
// Base Units (4px increments)
xs: 4,
sm: 8,
md: 12,
lg: 16,
xl: 24,
xxl: 32,
xxxl: 48,
// Semantic Spacing
container: {
horizontal: 16, // Standard horizontal padding
vertical: 24, // Standard vertical padding
top: 24, // Top padding after PageHeader
bottom: 24, // Bottom padding before elements
},
section: 24, // Gap between major sections/cards
card: {
gap: 24, // Gap between cards
padding: 16, // Internal card padding
},
content: {
title: 16, // Space after section titles
item: 16, // Space between list items
small: 12, // Small spacing between related elements
micro: 8, // Very small spacing
},
header: {
paddingBottom: 16,
},
tabBar: {
clearance: 100, // Bottom padding to clear tab bar
},
}
```
## Benefits
**Zentrale Verwaltung** - Alle Abstände an einem Ort
**Konsistenz** - Gleiche Werte über die ganze App
**Wartbarkeit** - Einfach änderbar
**Semantisch** - Klare Bedeutung statt magische Zahlen
**TypeScript Support** - Vollständige Type-Safety
**Skalierbar** - Kann pro Theme angepasst werden
## Nächste Schritte
Das Spacing-System ist vollständig implementiert. Zukünftige Screens sollten das System von Anfang an nutzen:
```tsx
import { spacing } from '~/utils/spacing';
// In Komponenten
<View style={{ padding: spacing.container.horizontal }}>
```

View file

@ -0,0 +1,88 @@
Was ist Supabase MCP in Claude Code?
Supabase MCP ist eine Schnittstelle, die es Claude Code ermöglicht, über das Model Context Protocol mit Supabase zu interagieren. Dadurch kannst du deine Supabase-Projekte direkt über Claude Code verwalten also Tabellen erstellen, SQL-Abfragen ausführen, Funktionen schreiben und vieles mehr alles per natürlicher Sprache ohne manuelle Bedienung des Dashboards.
Schritt-für-Schritt Anleitung zur Einrichtung
1. Supabase Personal Access Token (PAT) erstellen
Gehe im Supabase-Dashboard zu Account Settings (oben rechts auf dein Profilbild klicken).
Navigiere zu Access Tokens.
Klicke auf Generate New Token, gib ihm z.B. den Namen „ClaudeCodeMCP“ und speichere den Token sicher ab (du kannst ihn später nicht nochmal einsehen).
2. Supabase MCP in Claude Code konfigurieren
Variante A: Projektbezogen (über .mcp.json-Datei)
Erstelle im Root-Verzeichnis deines Projekts eine Datei namens .mcp.json (falls noch nicht vorhanden).
Füge folgende Konfiguration hinzu, ersetze <project-ref> mit deinem Supabase-Projekt-Referenz und <personal-access-token> mit deinem PAT:
json
{
"mcpServers": {
"supabase": {
"command": "npx",
"args": [
"-y",
"@supabase/mcp-server-supabase@latest",
"--read-only",
"--project-ref=<project-ref>"
],
"env": {
"SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
}
}
}
}
Speichere die Datei und starte Claude Code neu, damit die Konfiguration wirksam wird.
3. Nutzung in Claude Code
Nach erfolgreicher Einrichtung siehst du im Chat-Bereich von Claude Code das MCP-Symbol (Hammer).
Du kannst Claude nun mithilfe natürlicher Sprache anweisen, Supabase-Aktionen auszuführen, z.B.:
text
Create a new table called comments with columns id, text, user_id.
Fetch all rows from the comments table.
Add an RLS policy that allows only the owner to update their comment.
Claude übersetzt diese Befehle in API-Aufrufe und SQL-Statements und führt sie in Supabase aus.
Zusammenfassung
Erstelle einen Supabase Personal Access Token.
Konfiguriere Supabase MCP entweder durch .mcp.json oder CLI in Claude Code.
Starte Claude Code neu und nutze die MCP-Schnittstelle, um Supabase per natürlicher Sprache zu steuern.
So kannst du deine Supabase-Datenbanken, Funktionen und Policies bequem aus deinem Terminal über Claude Code verwalten, ohne Umwege über das Web-Dashboard.
Wenn du möchtest, kann ich dir auch eine Beispiel-.mcp.json-Datei zum einfachen Kopieren erstellen.
# Credetials
Die Supabase-Credentials, die du für die Einrichtung von Supabase MCP in Claude Code brauchst, findest und erstellst du so:
Personal Access Token (PAT) erstellen:
Melde dich im Supabase-Dashboard an.
Gehe oben rechts auf dein Profilbild und klicke auf Settings (Einstellungen).
Wähle dort den Bereich Access Tokens aus.
Klicke auf Generate New Token (Neues Token generieren).
Gib dem Token einen Namen (z.B. „ClaudeCodeMCP“) und speichere es ab. Das Token wird nur einmal angezeigt, also sichere es gut.
Projekt-URL bzw. Project Ref:
Du findest den Projekt-Referenzcode (project-ref) in deinem Projekt-Dashboard unter Settings > General > Project API keys oder meist in der URL deines Projekts als Kurzbezeichnung (z.B. abcdefg).
Diese beiden Credentials brauchst du für die Supabase MCP-Konfiguration:
Project Ref (dein Supabase Projekt-Identifikator)
Personal Access Token (zum autorisierten Zugriff per MCP)
Diese Daten gibst du dann z.B. in der .mcp.json oder im CLI-Befehl von Claude Code für Supabase MCP als Umgebungsvariable SUPABASE_ACCESS_TOKEN und als project-ref an.
# Entferne das Read only flag um auch in die datenbank schreiben zu können

View file

@ -0,0 +1,2 @@
// @ts-ignore
/// <reference types="nativewind/types" />

View file

@ -0,0 +1,51 @@
import { ExpoConfig, ConfigContext } from 'expo/config';
const IS_DEV = process.env.EAS_BUILD_PROFILE === 'development';
export default ({ config }: ConfigContext): ExpoConfig => {
// Base plugins for all builds
const basePlugins = [
'expo-router',
'expo-font',
'expo-web-browser',
[
'expo-image-picker',
{
photosPermission: 'Diese App benötigt Zugriff auf deine Fotos, um Bilder für Lernkarten auszuwählen.',
cameraPermission: 'Diese App benötigt Zugriff auf die Kamera, um Fotos für Lernkarten aufzunehmen.',
},
],
[
'expo-build-properties',
{
ios: {
deploymentTarget: '16.0',
newArchEnabled: true,
},
android: {
targetSdkVersion: 36,
compileSdkVersion: 36,
newArchEnabled: true,
},
},
],
];
// Only add dev-launcher in development builds
const plugins = IS_DEV
? [
...basePlugins,
[
'expo-dev-launcher',
{
launchMode: 'most-recent',
},
],
]
: basePlugins;
return {
...config,
plugins,
};
};

View file

@ -0,0 +1,74 @@
{
"expo": {
"name": "manadeck",
"slug": "manadeck",
"version": "1.0.0",
"scheme": "manadeck",
"runtimeVersion": {
"policy": "appVersion"
},
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/favicon.png"
},
"experiments": {
"typedRoutes": true,
"tsconfigPaths": true
},
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.tilljs.manadeck",
"infoPlist": {
"ITSAppUsesNonExemptEncryption": false,
"NSMicrophoneUsageDescription": "Diese App benötigt Zugriff auf das Mikrofon, um Sprachaufnahmen für die Lernkarten-Erstellung zu ermöglichen.",
"NSCameraUsageDescription": "Diese App benötigt Zugriff auf die Kamera, um Fotos für die Lernkarten-Erstellung aufzunehmen.",
"NSPhotoLibraryUsageDescription": "Diese App benötigt Zugriff auf die Fotobibliothek, um Bilder für die Lernkarten-Erstellung auszuwählen.",
"NSSpeechRecognitionUsageDescription": "Diese App verwendet Spracherkennung, um Sprachaufnahmen in Text umzuwandeln.",
"NSDocumentsFolderUsageDescription": "Diese App benötigt Zugriff auf Dokumente, um Lernmaterialien zu importieren."
},
"appleTeamId": "QP3GLU8PH3",
"config": {
"googleSignIn": {
"reservedClientId": "com.googleusercontent.apps.111768794939-cgen6eklloo7k8vppcaq01o8r8nd7anb"
}
},
"usesAppleSignIn": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.tilljs.manadeck",
"permissions": [
"android.permission.RECORD_AUDIO",
"android.permission.CAMERA",
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE"
]
},
"extra": {
"router": {},
"eas": {
"projectId": "6cb9cf81-a4d5-4c72-b57d-1be3da8eba35"
}
},
"plugins": [
"expo-router",
"@react-native-google-signin/google-signin",
"expo-apple-authentication"
]
}
}

View file

@ -0,0 +1,15 @@
import { Stack } from 'expo-router';
export default function AuthLayout() {
return (
<Stack
screenOptions={{
headerShown: false,
contentStyle: { backgroundColor: '#f3f4f6' },
}}>
<Stack.Screen name="login" />
<Stack.Screen name="register" />
<Stack.Screen name="forgot-password" />
</Stack>
);
}

View file

@ -0,0 +1,135 @@
import React, { useState } from 'react';
import {
View,
ScrollView,
KeyboardAvoidingView,
Platform,
Pressable,
Alert,
} from 'react-native';
import { Link, router } from 'expo-router';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Text } from '~/components/ui/Text';
import { Icon } from '~/components/ui/Icon';
import { useAuthStore } from '../../store/authStore';
import { Button } from '../../components/ui/Button';
import { Input } from '../../components/ui/Input';
import { Card } from '../../components/ui/Card';
import { useThemeColors } from '~/utils/themeUtils';
import { spacing } from '~/utils/spacing';
export default function ForgotPasswordScreen() {
const [email, setEmail] = useState('');
const [errors, setErrors] = useState<{ email?: string }>({});
const [isSubmitted, setIsSubmitted] = useState(false);
const colors = useThemeColors();
const { resetPassword, isLoading, clearError } = useAuthStore();
const validateForm = () => {
const newErrors: { email?: string } = {};
if (!email) {
newErrors.email = 'E-Mail ist erforderlich';
} else if (!/\S+@\S+\.\S+/.test(email)) {
newErrors.email = 'Ungültige E-Mail-Adresse';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleResetPassword = async () => {
if (!validateForm()) return;
try {
clearError();
await resetPassword(email);
setIsSubmitted(true);
} catch (err: any) {
Alert.alert('Zurücksetzen fehlgeschlagen', err.message || 'Ein Fehler ist aufgetreten');
}
};
if (isSubmitted) {
return (
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }} edges={['top']}>
<View style={{ flex: 1, justifyContent: 'center', paddingHorizontal: 24 }}>
<Card padding="lg" variant="elevated" style={{ alignItems: 'center' }}>
<View style={{ marginBottom: spacing.lg, height: 80, width: 80, alignItems: 'center', justifyContent: 'center', borderRadius: 40, backgroundColor: '#10b98120' }}>
<Icon name="checkmark-circle" size={48} color="#10b981" library="Ionicons" />
</View>
<Text style={{ marginBottom: 8, textAlign: 'center', fontSize: 24, fontWeight: 'bold', color: colors.foreground }}>
E-Mail gesendet!
</Text>
<Text style={{ marginBottom: spacing.xl, textAlign: 'center', color: colors.mutedForeground }}>
Wir haben dir eine E-Mail mit Anweisungen zum Zurücksetzen deines Passworts an {email}{' '}
gesendet.
</Text>
<Button onPress={() => router.replace('/(auth)/login')} fullWidth size="lg">
Zurück zur Anmeldung
</Button>
</Card>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }} edges={['top']}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}>
<ScrollView contentContainerStyle={{ flexGrow: 1 }} keyboardShouldPersistTaps="handled">
<View style={{ flex: 1, paddingHorizontal: 24, paddingVertical: 32 }}>
<Pressable onPress={() => router.back()} style={{ marginBottom: spacing.xl, flexDirection: 'row', alignItems: 'center' }}>
<Icon name="arrow-back" size={24} color={colors.foreground} library="Ionicons" />
<Text style={{ marginLeft: 8, color: colors.foreground }}>Zurück</Text>
</Pressable>
<View style={{ flex: 1, justifyContent: 'center' }}>
<View style={{ marginBottom: spacing.xxl }}>
<Text style={{ marginBottom: spacing.sm, textAlign: 'center', fontSize: 32, fontWeight: 'bold', color: colors.foreground }}>
Passwort vergessen?
</Text>
<Text style={{ textAlign: 'center', color: colors.mutedForeground }}>
Kein Problem! Gib deine E-Mail-Adresse ein und wir senden dir Anweisungen zum
Zurücksetzen.
</Text>
</View>
<Card padding="lg" variant="elevated">
<Input
label="E-Mail-Adresse"
type="email"
placeholder="deine@email.de"
value={email}
onChangeText={setEmail}
error={errors.email}
leftIcon="mail-outline"
autoComplete="email"
/>
<Button onPress={handleResetPassword} loading={isLoading} fullWidth size="lg">
Zurücksetzen-Link senden
</Button>
<View style={{ marginTop: spacing.xl, flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }}>
<Text style={{ color: colors.mutedForeground }}>Erinnerst du dich wieder? </Text>
<Link href="/(auth)/login" asChild>
<Pressable>
<Text style={{ fontWeight: '600', color: colors.primary }}>Anmelden</Text>
</Pressable>
</Link>
</View>
</Card>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}

View file

@ -0,0 +1,166 @@
import React, { useState } from 'react';
import {
View,
ScrollView,
KeyboardAvoidingView,
Platform,
Pressable,
Alert,
} from 'react-native';
import { Link, router } from 'expo-router';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Text } from '~/components/ui/Text';
import { useAuthStore } from '../../store/authStore';
import { Button } from '../../components/ui/Button';
import { Input } from '../../components/ui/Input';
import { Card } from '../../components/ui/Card';
import { useThemeColors } from '~/utils/themeUtils';
import { GoogleSignInButton } from '../../components/auth/GoogleSignInButton';
import { AppleSignInButton } from '../../components/auth/AppleSignInButton';
import { spacing } from '~/utils/spacing';
export default function LoginScreen() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
const colors = useThemeColors();
const { signIn, signInWithGoogle, signInWithApple, isLoading, error, clearError } = useAuthStore();
const validateForm = () => {
const newErrors: { email?: string; password?: string } = {};
if (!email) {
newErrors.email = 'E-Mail ist erforderlich';
} else if (!/\S+@\S+\.\S+/.test(email)) {
newErrors.email = 'Ungültige E-Mail-Adresse';
}
if (!password) {
newErrors.password = 'Passwort ist erforderlich';
} else if (password.length < 6) {
newErrors.password = 'Passwort muss mindestens 6 Zeichen lang sein';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleLogin = async () => {
if (!validateForm()) return;
try {
clearError();
await signIn(email, password);
router.replace('/(tabs)');
} catch (err: any) {
Alert.alert('Login fehlgeschlagen', err.message || 'Ein Fehler ist aufgetreten');
}
};
const handleGoogleSignIn = async (idToken: string) => {
try {
clearError();
await signInWithGoogle(idToken);
router.replace('/(tabs)');
} catch (err: any) {
// Error is already handled in GoogleSignInButton
throw err;
}
};
const handleAppleSignIn = async (identityToken: string) => {
try {
clearError();
await signInWithApple(identityToken);
router.replace('/(tabs)');
} catch (err: any) {
// Error is already handled in AppleSignInButton
throw err;
}
};
return (
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }} edges={['top']}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}>
<ScrollView contentContainerStyle={{ flexGrow: 1 }} keyboardShouldPersistTaps="handled">
<View style={{ flex: 1, justifyContent: 'center', paddingHorizontal: 24, paddingVertical: 32 }}>
<View style={{ marginBottom: spacing.xxl }}>
<Text style={{ marginBottom: spacing.sm, textAlign: 'center', fontSize: 32, fontWeight: 'bold', color: colors.foreground }}>
Willkommen zurück
</Text>
<Text style={{ textAlign: 'center', color: colors.mutedForeground }}>Melde dich an, um fortzufahren</Text>
</View>
<Card padding="lg" variant="elevated">
<Input
label="E-Mail"
type="email"
placeholder="deine@email.de"
value={email}
onChangeText={setEmail}
error={errors.email}
leftIcon="mail-outline"
autoComplete="email"
/>
<Input
label="Passwort"
type="password"
placeholder="••••••••"
value={password}
onChangeText={setPassword}
error={errors.password}
leftIcon="lock-closed-outline"
/>
<Pressable style={{ marginBottom: spacing.lg }}>
<Link href="/(auth)/forgot-password" asChild>
<Text style={{ textAlign: 'right', fontSize: 14, color: colors.primary }}>Passwort vergessen?</Text>
</Link>
</Pressable>
<Button onPress={handleLogin} loading={isLoading} fullWidth size="lg">
Anmelden
</Button>
{/* Social Sign-In Divider */}
<View style={{ marginVertical: spacing.xl, flexDirection: 'row', alignItems: 'center' }}>
<View style={{ flex: 1, height: 1, backgroundColor: colors.border }} />
<Text style={{ marginHorizontal: spacing.lg, color: colors.mutedForeground, fontSize: 14 }}>
oder
</Text>
<View style={{ flex: 1, height: 1, backgroundColor: colors.border }} />
</View>
{/* Social Sign-In Buttons */}
<View style={{ gap: spacing.content.small }}>
<GoogleSignInButton
onSignIn={handleGoogleSignIn}
onSignInSuccess={() => console.log('Google sign-in successful')}
onSignInError={(error) => console.error('Google sign-in error:', error)}
/>
<AppleSignInButton
onSignIn={handleAppleSignIn}
onSignInSuccess={() => console.log('Apple sign-in successful')}
onSignInError={(error) => console.error('Apple sign-in error:', error)}
/>
</View>
<View style={{ marginTop: spacing.xl, flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }}>
<Text style={{ color: colors.mutedForeground }}>Noch kein Konto? </Text>
<Link href="/(auth)/register" asChild>
<Pressable>
<Text style={{ fontWeight: '600', color: colors.primary }}>Registrieren</Text>
</Pressable>
</Link>
</View>
</Card>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}

View file

@ -0,0 +1,156 @@
import React, { useState } from 'react';
import {
View,
ScrollView,
KeyboardAvoidingView,
Platform,
Pressable,
Alert,
} from 'react-native';
import { Link, router } from 'expo-router';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Text } from '~/components/ui/Text';
import { useAuthStore } from '../../store/authStore';
import { Button } from '../../components/ui/Button';
import { Input } from '../../components/ui/Input';
import { Card } from '../../components/ui/Card';
import { useThemeColors } from '~/utils/themeUtils';
import { spacing } from '~/utils/spacing';
export default function RegisterScreen() {
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [errors, setErrors] = useState<{
username?: string;
email?: string;
password?: string;
confirmPassword?: string;
}>({});
const colors = useThemeColors();
const { signUp, isLoading, clearError } = useAuthStore();
const validateForm = () => {
const newErrors: typeof errors = {};
if (!username) {
newErrors.username = 'Benutzername ist erforderlich';
} else if (username.length < 3) {
newErrors.username = 'Benutzername muss mindestens 3 Zeichen lang sein';
}
if (!email) {
newErrors.email = 'E-Mail ist erforderlich';
} else if (!/\S+@\S+\.\S+/.test(email)) {
newErrors.email = 'Ungültige E-Mail-Adresse';
}
if (!password) {
newErrors.password = 'Passwort ist erforderlich';
} else if (password.length < 6) {
newErrors.password = 'Passwort muss mindestens 6 Zeichen lang sein';
}
if (!confirmPassword) {
newErrors.confirmPassword = 'Passwort-Bestätigung ist erforderlich';
} else if (password !== confirmPassword) {
newErrors.confirmPassword = 'Passwörter stimmen nicht überein';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleRegister = async () => {
if (!validateForm()) return;
try {
clearError();
await signUp(email, password, username);
Alert.alert(
'Registrierung erfolgreich!',
'Bitte überprüfe deine E-Mail, um dein Konto zu bestätigen.',
[{ text: 'OK', onPress: () => router.replace('/(auth)/login') }]
);
} catch (err: any) {
Alert.alert('Registrierung fehlgeschlagen', err.message || 'Ein Fehler ist aufgetreten');
}
};
return (
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }} edges={['top']}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}>
<ScrollView contentContainerStyle={{ flexGrow: 1 }} keyboardShouldPersistTaps="handled">
<View style={{ flex: 1, justifyContent: 'center', paddingHorizontal: 24, paddingVertical: 32 }}>
<View style={{ marginBottom: spacing.xxl }}>
<Text style={{ marginBottom: spacing.sm, textAlign: 'center', fontSize: 32, fontWeight: 'bold', color: colors.foreground }}>
Konto erstellen
</Text>
<Text style={{ textAlign: 'center', color: colors.mutedForeground }}>Registriere dich, um loszulegen</Text>
</View>
<Card padding="lg" variant="elevated">
<Input
label="Benutzername"
placeholder="deinbenutzername"
value={username}
onChangeText={setUsername}
error={errors.username}
leftIcon="person-outline"
autoComplete="username"
/>
<Input
label="E-Mail"
type="email"
placeholder="deine@email.de"
value={email}
onChangeText={setEmail}
error={errors.email}
leftIcon="mail-outline"
autoComplete="email"
/>
<Input
label="Passwort"
type="password"
placeholder="••••••••"
value={password}
onChangeText={setPassword}
error={errors.password}
leftIcon="lock-closed-outline"
/>
<Input
label="Passwort bestätigen"
type="password"
placeholder="••••••••"
value={confirmPassword}
onChangeText={setConfirmPassword}
error={errors.confirmPassword}
leftIcon="lock-closed-outline"
/>
<Button onPress={handleRegister} loading={isLoading} fullWidth size="lg">
Registrieren
</Button>
<View style={{ marginTop: spacing.xl, flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }}>
<Text style={{ color: colors.mutedForeground }}>Bereits ein Konto? </Text>
<Link href="/(auth)/login" asChild>
<Pressable>
<Text style={{ fontWeight: '600', color: colors.primary }}>Anmelden</Text>
</Pressable>
</Link>
</View>
</Card>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}

Some files were not shown because too many files have changed in this diff Show more