mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 16:26:43 +02:00
refactor: restructure
monorepo with apps/ and services/ directories
This commit is contained in:
parent
25824ed0ac
commit
ff80aeec1f
4062 changed files with 2592 additions and 1278 deletions
342
apps/manadeck/.github/workflows/deploy-backend.yml
vendored
Normal file
342
apps/manadeck/.github/workflows/deploy-backend.yml
vendored
Normal 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
31
apps/manadeck/.gitignore
vendored
Normal 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
|
||||
1246
apps/manadeck/CI_CD_SETUP_GUIDE.md
Normal file
1246
apps/manadeck/CI_CD_SETUP_GUIDE.md
Normal file
File diff suppressed because it is too large
Load diff
506
apps/manadeck/CREDIT_SYSTEM.md
Normal file
506
apps/manadeck/CREDIT_SYSTEM.md
Normal 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)
|
||||
298
apps/manadeck/DEPLOYMENT_CHECKLIST.md
Normal file
298
apps/manadeck/DEPLOYMENT_CHECKLIST.md
Normal 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
|
||||
143
apps/manadeck/EDGE_FUNCTION_FIX.md
Normal file
143
apps/manadeck/EDGE_FUNCTION_FIX.md
Normal 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
|
||||
591
apps/manadeck/MANA_CORE_ARCHITECTURE.md
Normal file
591
apps/manadeck/MANA_CORE_ARCHITECTURE.md
Normal 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
|
||||
445
apps/manadeck/MANA_CORE_INTEGRATION_CHECKLIST.md
Normal file
445
apps/manadeck/MANA_CORE_INTEGRATION_CHECKLIST.md
Normal 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)
|
||||
1114
apps/manadeck/MANA_CORE_INTEGRATION_GUIDE.md
Normal file
1114
apps/manadeck/MANA_CORE_INTEGRATION_GUIDE.md
Normal file
File diff suppressed because it is too large
Load diff
590
apps/manadeck/MANA_CORE_README.md
Normal file
590
apps/manadeck/MANA_CORE_README.md
Normal 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
442
apps/manadeck/README.md
Normal 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
|
||||
339
apps/manadeck/SETUP_GUIDE.md
Normal file
339
apps/manadeck/SETUP_GUIDE.md
Normal 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!
|
||||
57
apps/manadeck/apps/backend/.dockerignore
Normal file
57
apps/manadeck/apps/backend/.dockerignore
Normal 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
|
||||
21
apps/manadeck/apps/backend/.env.example
Normal file
21
apps/manadeck/apps/backend/.env.example
Normal 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
|
||||
20
apps/manadeck/apps/backend/.env.production
Normal file
20
apps/manadeck/apps/backend/.env.production
Normal 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
1
apps/manadeck/apps/backend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules
|
||||
4
apps/manadeck/apps/backend/.prettierrc
Normal file
4
apps/manadeck/apps/backend/.prettierrc
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
487
apps/manadeck/apps/backend/DEPLOY_MANUAL.md
Normal file
487
apps/manadeck/apps/backend/DEPLOY_MANUAL.md
Normal 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
|
||||
110
apps/manadeck/apps/backend/Dockerfile
Normal file
110
apps/manadeck/apps/backend/Dockerfile
Normal 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"]
|
||||
98
apps/manadeck/apps/backend/README.md
Normal file
98
apps/manadeck/apps/backend/README.md
Normal 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>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](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).
|
||||
300
apps/manadeck/apps/backend/README_MANA_INTEGRATION.md
Normal file
300
apps/manadeck/apps/backend/README_MANA_INTEGRATION.md
Normal 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
|
||||
176
apps/manadeck/apps/backend/SSH_LOCKFILE_SOLUTION.md
Normal file
176
apps/manadeck/apps/backend/SSH_LOCKFILE_SOLUTION.md
Normal 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. 🎯
|
||||
30
apps/manadeck/apps/backend/cloudbuild.yaml
Normal file
30
apps/manadeck/apps/backend/cloudbuild.yaml
Normal 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'
|
||||
257
apps/manadeck/apps/backend/create-secrets.sh
Executable file
257
apps/manadeck/apps/backend/create-secrets.sh
Executable 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 ""
|
||||
34
apps/manadeck/apps/backend/eslint.config.mjs
Normal file
34
apps/manadeck/apps/backend/eslint.config.mjs
Normal 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'
|
||||
},
|
||||
},
|
||||
);
|
||||
13
apps/manadeck/apps/backend/manadeck-sa-key.json
Normal file
13
apps/manadeck/apps/backend/manadeck-sa-key.json
Normal 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"
|
||||
}
|
||||
8
apps/manadeck/apps/backend/nest-cli.json
Normal file
8
apps/manadeck/apps/backend/nest-cli.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
83
apps/manadeck/apps/backend/package.json
Normal file
83
apps/manadeck/apps/backend/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
173
apps/manadeck/apps/backend/setup-github-secrets.sh
Executable file
173
apps/manadeck/apps/backend/setup-github-secrets.sh
Executable 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 ""
|
||||
22
apps/manadeck/apps/backend/src/app.controller.spec.ts
Normal file
22
apps/manadeck/apps/backend/src/app.controller.spec.ts
Normal 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!');
|
||||
});
|
||||
});
|
||||
});
|
||||
12
apps/manadeck/apps/backend/src/app.controller.ts
Normal file
12
apps/manadeck/apps/backend/src/app.controller.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
81
apps/manadeck/apps/backend/src/app.module.ts
Normal file
81
apps/manadeck/apps/backend/src/app.module.ts
Normal 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
|
||||
}
|
||||
}
|
||||
8
apps/manadeck/apps/backend/src/app.service.ts
Normal file
8
apps/manadeck/apps/backend/src/app.service.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
||||
51
apps/manadeck/apps/backend/src/config/credit-operations.ts
Normal file
51
apps/manadeck/apps/backend/src/config/credit-operations.ts
Normal 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;
|
||||
}
|
||||
26
apps/manadeck/apps/backend/src/config/validation.schema.ts
Normal file
26
apps/manadeck/apps/backend/src/config/validation.schema.ts
Normal 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(),
|
||||
});
|
||||
426
apps/manadeck/apps/backend/src/controllers/api.controller.ts
Normal file
426
apps/manadeck/apps/backend/src/controllers/api.controller.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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'),
|
||||
};
|
||||
}
|
||||
}
|
||||
106
apps/manadeck/apps/backend/src/controllers/public.controller.ts
Normal file
106
apps/manadeck/apps/backend/src/controllers/public.controller.ts
Normal 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(),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
29
apps/manadeck/apps/backend/src/database/database.module.ts
Normal file
29
apps/manadeck/apps/backend/src/database/database.module.ts
Normal 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 };
|
||||
2
apps/manadeck/apps/backend/src/database/index.ts
Normal file
2
apps/manadeck/apps/backend/src/database/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { DatabaseModule, DATABASE_TOKEN, type Database } from './database.module';
|
||||
export * from './repositories';
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
51
apps/manadeck/apps/backend/src/main.ts
Normal file
51
apps/manadeck/apps/backend/src/main.ts
Normal 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();
|
||||
314
apps/manadeck/apps/backend/src/services/ai.service.ts
Normal file
314
apps/manadeck/apps/backend/src/services/ai.service.ts
Normal 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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
25
apps/manadeck/apps/backend/test/app.e2e-spec.ts
Normal file
25
apps/manadeck/apps/backend/test/app.e2e-spec.ts
Normal 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!');
|
||||
});
|
||||
});
|
||||
9
apps/manadeck/apps/backend/test/jest-e2e.json
Normal file
9
apps/manadeck/apps/backend/test/jest-e2e.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
||||
4
apps/manadeck/apps/backend/tsconfig.build.json
Normal file
4
apps/manadeck/apps/backend/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
25
apps/manadeck/apps/backend/tsconfig.json
Normal file
25
apps/manadeck/apps/backend/tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
220
apps/manadeck/apps/backend/verify-build.sh
Executable file
220
apps/manadeck/apps/backend/verify-build.sh
Executable 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 ""
|
||||
17
apps/manadeck/apps/landing/astro.config.mjs
Normal file
17
apps/manadeck/apps/landing/astro.config.mjs
Normal 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']
|
||||
}
|
||||
}
|
||||
});
|
||||
27
apps/manadeck/apps/landing/package.json
Normal file
27
apps/manadeck/apps/landing/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
4
apps/manadeck/apps/landing/public/favicon.svg
Normal file
4
apps/manadeck/apps/landing/public/favicon.svg
Normal 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 |
4
apps/manadeck/apps/landing/public/robots.txt
Normal file
4
apps/manadeck/apps/landing/public/robots.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://manadeck.app/sitemap-index.xml
|
||||
78
apps/manadeck/apps/landing/src/components/Footer.astro
Normal file
78
apps/manadeck/apps/landing/src/components/Footer.astro
Normal 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">
|
||||
© {currentYear} ManaDeck. Alle Rechte vorbehalten.
|
||||
</p>
|
||||
<p class="text-text-muted text-sm">
|
||||
Made with 💜 in Germany
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
84
apps/manadeck/apps/landing/src/components/Navigation.astro
Normal file
84
apps/manadeck/apps/landing/src/components/Navigation.astro
Normal 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>
|
||||
1
apps/manadeck/apps/landing/src/env.d.ts
vendored
Normal file
1
apps/manadeck/apps/landing/src/env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="astro/client" />
|
||||
47
apps/manadeck/apps/landing/src/layouts/Layout.astro
Normal file
47
apps/manadeck/apps/landing/src/layouts/Layout.astro
Normal 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>
|
||||
287
apps/manadeck/apps/landing/src/pages/index.astro
Normal file
287
apps/manadeck/apps/landing/src/pages/index.astro
Normal 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>
|
||||
103
apps/manadeck/apps/landing/src/styles/global.css
Normal file
103
apps/manadeck/apps/landing/src/styles/global.css
Normal 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;
|
||||
}
|
||||
39
apps/manadeck/apps/landing/tailwind.config.mjs
Normal file
39
apps/manadeck/apps/landing/tailwind.config.mjs
Normal 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')
|
||||
]
|
||||
};
|
||||
9
apps/manadeck/apps/landing/tsconfig.json
Normal file
9
apps/manadeck/apps/landing/tsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
7
apps/manadeck/apps/mobile/.env.production
Normal file
7
apps/manadeck/apps/mobile/.env.production
Normal 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
6
apps/manadeck/apps/mobile/.gitignore
vendored
Normal 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
|
||||
11
apps/manadeck/apps/mobile/.mcp.json
Normal file
11
apps/manadeck/apps/mobile/.mcp.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
118
apps/manadeck/apps/mobile/CLAUDE.md
Normal file
118
apps/manadeck/apps/mobile/CLAUDE.md
Normal 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
|
||||
483
apps/manadeck/apps/mobile/Docs/AI-Implementation-Plan.md
Normal file
483
apps/manadeck/apps/mobile/Docs/AI-Implementation-Plan.md
Normal 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
|
||||
456
apps/manadeck/apps/mobile/Docs/EXPO_NATIVE_TABS.md
Normal file
456
apps/manadeck/apps/mobile/Docs/EXPO_NATIVE_TABS.md
Normal 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>
|
||||
);
|
||||
}
|
||||
986
apps/manadeck/apps/mobile/Docs/EXPO_UI_DOCS.md
Normal file
986
apps/manadeck/apps/mobile/Docs/EXPO_UI_DOCS.md
Normal 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';
|
||||
293
apps/manadeck/apps/mobile/Docs/EXPO_UI_OFFFICIAL_DOCS.md
Normal file
293
apps/manadeck/apps/mobile/Docs/EXPO_UI_OFFFICIAL_DOCS.md
Normal 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';
|
||||
200
apps/manadeck/apps/mobile/Docs/GLASS_HEADER_GUIDE.md
Normal file
200
apps/manadeck/apps/mobile/Docs/GLASS_HEADER_GUIDE.md
Normal 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+
|
||||
382
apps/manadeck/apps/mobile/Docs/HEADER_STYLING_GUIDE.md
Normal file
382
apps/manadeck/apps/mobile/Docs/HEADER_STYLING_GUIDE.md
Normal 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/)
|
||||
150
apps/manadeck/apps/mobile/Docs/NATIVE_TAB_BEHAVIOR.md
Normal file
150
apps/manadeck/apps/mobile/Docs/NATIVE_TAB_BEHAVIOR.md
Normal 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
|
||||
173
apps/manadeck/apps/mobile/Docs/database.md
Normal file
173
apps/manadeck/apps/mobile/Docs/database.md
Normal 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
|
||||
237
apps/manadeck/apps/mobile/Docs/expo-sdk-54-upgrade-guide.md
Normal file
237
apps/manadeck/apps/mobile/Docs/expo-sdk-54-upgrade-guide.md
Normal 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
|
||||
255
apps/manadeck/apps/mobile/Docs/expo-sdk-54-upgrade.md
Normal file
255
apps/manadeck/apps/mobile/Docs/expo-sdk-54-upgrade.md
Normal 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)
|
||||
501
apps/manadeck/apps/mobile/Docs/frontend-components-plan.md
Normal file
501
apps/manadeck/apps/mobile/Docs/frontend-components-plan.md
Normal 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
|
||||
216
apps/manadeck/apps/mobile/Docs/next-steps-implementation-plan.md
Normal file
216
apps/manadeck/apps/mobile/Docs/next-steps-implementation-plan.md
Normal 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
|
||||
378
apps/manadeck/apps/mobile/Docs/phase-3-card-system-plan.md
Normal file
378
apps/manadeck/apps/mobile/Docs/phase-3-card-system-plan.md
Normal 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!
|
||||
|
|
@ -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';
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
*/
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 = π × r²", "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;
|
||||
276
apps/manadeck/apps/mobile/Docs/spaced-repetition.md
Normal file
276
apps/manadeck/apps/mobile/Docs/spaced-repetition.md
Normal 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*
|
||||
139
apps/manadeck/apps/mobile/Docs/user-guides/deck-creation.md
Normal file
139
apps/manadeck/apps/mobile/Docs/user-guides/deck-creation.md
Normal 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*
|
||||
|
|
@ -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*
|
||||
522
apps/manadeck/apps/mobile/SOCIAL_AUTH_SETUP.md
Normal file
522
apps/manadeck/apps/mobile/SOCIAL_AUTH_SETUP.md
Normal 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.
|
||||
126
apps/manadeck/apps/mobile/SPACING_MIGRATION.md
Normal file
126
apps/manadeck/apps/mobile/SPACING_MIGRATION.md
Normal 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 }}>
|
||||
```
|
||||
|
|
@ -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
|
||||
2
apps/manadeck/apps/mobile/app-env.d.ts
vendored
Normal file
2
apps/manadeck/apps/mobile/app-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// @ts-ignore
|
||||
/// <reference types="nativewind/types" />
|
||||
51
apps/manadeck/apps/mobile/app.config.ts
Normal file
51
apps/manadeck/apps/mobile/app.config.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
74
apps/manadeck/apps/mobile/app.json
Normal file
74
apps/manadeck/apps/mobile/app.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
15
apps/manadeck/apps/mobile/app/(auth)/_layout.tsx
Normal file
15
apps/manadeck/apps/mobile/app/(auth)/_layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
135
apps/manadeck/apps/mobile/app/(auth)/forgot-password.tsx
Normal file
135
apps/manadeck/apps/mobile/app/(auth)/forgot-password.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
166
apps/manadeck/apps/mobile/app/(auth)/login.tsx
Normal file
166
apps/manadeck/apps/mobile/app/(auth)/login.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
156
apps/manadeck/apps/mobile/app/(auth)/register.tsx
Normal file
156
apps/manadeck/apps/mobile/app/(auth)/register.tsx
Normal 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
Loading…
Add table
Add a link
Reference in a new issue