mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:41:09 +02:00
feat(feedback): add centralized feedback system with AI-generated titles
- Add shared-feedback-types package with TypeScript types - Add shared-feedback-service package with factory function - Add shared-feedback-ui package with Svelte 5 components - Add feedback module to mana-core-auth backend - Add AI service using Gemini 2.0 Flash for title/category generation - Add database schema and migration for feedback tables - Integrate feedback page into Chat web app - Add CORS support for X-App-Id header - Add COMMANDS.md documentation for all dev commands 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
05fe8ca5b6
commit
819e4c9a2f
41 changed files with 4290 additions and 338 deletions
343
COMMANDS.md
Normal file
343
COMMANDS.md
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
# Manacore Monorepo - Befehle
|
||||
|
||||
# Alles starten (PostgreSQL, Redis, Auth, Chat)
|
||||
pnpm docker:up:all
|
||||
|
||||
pnpm docker:down
|
||||
|
||||
pnpm dev:chat:app
|
||||
|
||||
pnpm dev:picture:app
|
||||
|
||||
|
||||
|
||||
Übersicht aller wichtigen Befehle zum Starten, Stoppen und Verwalten der Apps.
|
||||
|
||||
## Inhaltsverzeichnis
|
||||
|
||||
- [Voraussetzungen](#voraussetzungen)
|
||||
- [Docker & Infrastruktur](#docker--infrastruktur)
|
||||
- [Mana Core Auth](#mana-core-auth)
|
||||
- [Chat](#chat)
|
||||
- [Picture](#picture)
|
||||
- [Manadeck](#manadeck)
|
||||
- [Zitare](#zitare)
|
||||
- [Presi](#presi)
|
||||
- [Manacore](#manacore)
|
||||
- [Mana Games](#mana-games)
|
||||
- [Allgemeine Befehle](#allgemeine-befehle)
|
||||
- [Stoppen & Aufräumen](#stoppen--aufräumen)
|
||||
|
||||
---
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
```bash
|
||||
# Dependencies installieren (generiert auch .env Dateien)
|
||||
pnpm install
|
||||
|
||||
# Nur .env Dateien neu generieren
|
||||
pnpm setup:env
|
||||
|
||||
# Shared Packages bauen
|
||||
pnpm build:packages
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker & Infrastruktur
|
||||
|
||||
### Starten
|
||||
|
||||
```bash
|
||||
# Nur PostgreSQL + Redis (Basis-Infrastruktur)
|
||||
pnpm docker:up
|
||||
|
||||
# Mit Mana Core Auth Service
|
||||
pnpm docker:up:auth
|
||||
|
||||
# Mit Chat Backend
|
||||
pnpm docker:up:chat
|
||||
|
||||
# Alles starten (PostgreSQL, Redis, Auth, Chat)
|
||||
pnpm docker:up:all
|
||||
```
|
||||
|
||||
### Status & Logs
|
||||
|
||||
```bash
|
||||
# Container-Status anzeigen
|
||||
pnpm docker:ps
|
||||
|
||||
# Alle Logs anzeigen (live)
|
||||
pnpm docker:logs
|
||||
|
||||
# Nur Auth-Logs
|
||||
pnpm docker:logs:auth
|
||||
|
||||
# Nur Chat-Logs
|
||||
pnpm docker:logs:chat
|
||||
```
|
||||
|
||||
### Stoppen
|
||||
|
||||
```bash
|
||||
# Alle Container stoppen
|
||||
pnpm docker:down
|
||||
|
||||
# Stoppen + Volumes löschen (Datenbank-Reset!)
|
||||
pnpm docker:clean
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mana Core Auth
|
||||
|
||||
Der zentrale Authentifizierungs-Service (Port 3001).
|
||||
|
||||
```bash
|
||||
# Im Docker starten (empfohlen)
|
||||
pnpm docker:up:auth
|
||||
|
||||
# Oder lokal starten
|
||||
cd services/mana-core-auth
|
||||
pnpm start:dev
|
||||
|
||||
# Datenbank-Migration
|
||||
cd services/mana-core-auth
|
||||
pnpm migration:generate # Migration erstellen
|
||||
pnpm migration:run # Migration ausführen
|
||||
pnpm db:push # Schema direkt pushen (dev)
|
||||
pnpm db:studio # Drizzle Studio öffnen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chat
|
||||
|
||||
| App | Port | Befehl |
|
||||
|-----|------|--------|
|
||||
| Web | 5174 | `pnpm dev:chat:web` |
|
||||
| Backend | 3002 | `pnpm dev:chat:backend` |
|
||||
| Mobile | 8081 | `pnpm dev:chat:mobile` |
|
||||
| Landing | - | `pnpm dev:chat:landing` |
|
||||
|
||||
```bash
|
||||
# Web + Backend zusammen starten
|
||||
pnpm dev:chat:app
|
||||
|
||||
# Alles (Web, Backend, Mobile, Landing)
|
||||
pnpm chat:dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Picture
|
||||
|
||||
| App | Port | Befehl |
|
||||
|-----|------|--------|
|
||||
| Web | 5173 | `pnpm dev:picture:web` |
|
||||
| Backend | - | `pnpm dev:picture:backend` |
|
||||
| Mobile | 8081 | `pnpm dev:picture:mobile` |
|
||||
| Landing | - | `pnpm dev:picture:landing` |
|
||||
|
||||
```bash
|
||||
# Web + Backend zusammen starten
|
||||
pnpm dev:picture:app
|
||||
|
||||
# Alles
|
||||
pnpm picture:dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Manadeck
|
||||
|
||||
| App | Port | Befehl |
|
||||
|-----|------|--------|
|
||||
| Web | - | `pnpm dev:manadeck:web` |
|
||||
| Backend | - | `pnpm dev:manadeck:backend` |
|
||||
| Mobile | 8081 | `pnpm dev:manadeck:mobile` |
|
||||
| Landing | - | `pnpm dev:manadeck:landing` |
|
||||
|
||||
```bash
|
||||
# Web + Backend zusammen starten
|
||||
pnpm dev:manadeck:app
|
||||
|
||||
# Alles
|
||||
pnpm manadeck:dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Zitare
|
||||
|
||||
| App | Port | Befehl |
|
||||
|-----|------|--------|
|
||||
| Web | - | `pnpm dev:zitare:web` |
|
||||
| Backend | - | `pnpm dev:zitare:backend` |
|
||||
| Mobile | 8081 | `pnpm dev:zitare:mobile` |
|
||||
| Landing | - | `pnpm dev:zitare:landing` |
|
||||
|
||||
```bash
|
||||
# Web + Backend zusammen starten
|
||||
pnpm dev:zitare:app
|
||||
|
||||
# Alles
|
||||
pnpm zitare:dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Presi
|
||||
|
||||
| App | Port | Befehl |
|
||||
|-----|------|--------|
|
||||
| Web | - | `pnpm dev:presi:web` |
|
||||
| Backend | - | `pnpm dev:presi:backend` |
|
||||
| Mobile | 8081 | `pnpm dev:presi:mobile` |
|
||||
|
||||
```bash
|
||||
# Web + Backend zusammen starten
|
||||
pnpm dev:presi:app
|
||||
|
||||
# Alles
|
||||
pnpm presi:dev
|
||||
|
||||
# Datenbank
|
||||
pnpm presi:db:push # Schema pushen
|
||||
pnpm presi:db:studio # Drizzle Studio
|
||||
pnpm presi:db:seed # Seed-Daten
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Manacore
|
||||
|
||||
| App | Port | Befehl |
|
||||
|-----|------|--------|
|
||||
| Web | - | `pnpm dev:manacore:web` |
|
||||
| Mobile | 8081 | `pnpm dev:manacore:mobile` |
|
||||
| Landing | - | `pnpm dev:manacore:landing` |
|
||||
|
||||
```bash
|
||||
# Alles
|
||||
pnpm manacore:dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mana Games
|
||||
|
||||
```bash
|
||||
# Web + Backend zusammen starten
|
||||
pnpm dev:mana-games:app
|
||||
|
||||
# Einzeln
|
||||
pnpm dev:mana-games:web
|
||||
pnpm dev:mana-games:backend
|
||||
|
||||
# Alles
|
||||
pnpm mana-games:dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Allgemeine Befehle
|
||||
|
||||
```bash
|
||||
# Alles bauen
|
||||
pnpm build
|
||||
|
||||
# Type-Check
|
||||
pnpm type-check
|
||||
|
||||
# Linting
|
||||
pnpm lint
|
||||
|
||||
# Formatierung
|
||||
pnpm format # Formatieren
|
||||
pnpm format:check # Nur prüfen
|
||||
|
||||
# Tests
|
||||
pnpm test
|
||||
|
||||
# Cache leeren
|
||||
pnpm clean
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stoppen & Aufräumen
|
||||
|
||||
### Prozesse stoppen
|
||||
|
||||
```bash
|
||||
# Alle Node-Prozesse beenden (macOS/Linux)
|
||||
pkill -f "node"
|
||||
|
||||
# Spezifischen Port freigeben
|
||||
lsof -ti:3001 | xargs kill -9 # Port 3001 (Auth)
|
||||
lsof -ti:3002 | xargs kill -9 # Port 3002 (Chat Backend)
|
||||
lsof -ti:5174 | xargs kill -9 # Port 5174 (Chat Web)
|
||||
|
||||
# Turbo Cache leeren
|
||||
pnpm clean
|
||||
```
|
||||
|
||||
### Docker stoppen
|
||||
|
||||
```bash
|
||||
# Container stoppen (Daten bleiben)
|
||||
pnpm docker:down
|
||||
|
||||
# Container + Volumes löschen (Datenbank-Reset!)
|
||||
pnpm docker:clean
|
||||
```
|
||||
|
||||
### Komplettes Cleanup
|
||||
|
||||
```bash
|
||||
# Alles stoppen und aufräumen
|
||||
pnpm docker:down
|
||||
pkill -f "node"
|
||||
pnpm clean
|
||||
rm -rf node_modules/.cache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Typische Workflows
|
||||
|
||||
### Chat-Entwicklung starten
|
||||
|
||||
```bash
|
||||
# 1. Infrastruktur starten
|
||||
pnpm docker:up
|
||||
|
||||
# 2. Auth-Service starten (neues Terminal)
|
||||
cd services/mana-core-auth && pnpm start:dev
|
||||
|
||||
# 3. Chat App starten (neues Terminal)
|
||||
pnpm dev:chat:app
|
||||
```
|
||||
|
||||
### Alles mit Docker
|
||||
|
||||
```bash
|
||||
# Alles in Docker starten
|
||||
pnpm docker:up:all
|
||||
|
||||
# Nur Frontend lokal
|
||||
pnpm dev:chat:web
|
||||
```
|
||||
|
||||
### Nach Code-Änderungen in Shared Packages
|
||||
|
||||
```bash
|
||||
# Packages neu bauen
|
||||
pnpm build:packages
|
||||
|
||||
# Oder spezifisches Package
|
||||
pnpm --filter @manacore/shared-ui build
|
||||
```
|
||||
15
apps/chat/apps/web/src/lib/services/feedback.ts
Normal file
15
apps/chat/apps/web/src/lib/services/feedback.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Feedback Service Instance for Chat Web App
|
||||
*/
|
||||
|
||||
import { createFeedbackService } from '@manacore/shared-feedback-service';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public';
|
||||
|
||||
const MANA_AUTH_URL = PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
|
||||
export const feedbackService = createFeedbackService({
|
||||
apiUrl: MANA_AUTH_URL,
|
||||
appId: 'chat',
|
||||
getAuthToken: async () => authStore.getAccessToken(),
|
||||
});
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { FeedbackPage } from '@manacore/shared-feedback-ui';
|
||||
import { feedbackService } from '$lib/services/feedback';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
</script>
|
||||
|
||||
<FeedbackPage
|
||||
{feedbackService}
|
||||
appName="ManaChat"
|
||||
currentUserId={authStore.user?.id}
|
||||
/>
|
||||
24
packages/shared-feedback-service/package.json
Normal file
24
packages/shared-feedback-service/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "@manacore/shared-feedback-service",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"files": ["src"],
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/shared-feedback-types": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
150
packages/shared-feedback-service/src/createFeedbackService.ts
Normal file
150
packages/shared-feedback-service/src/createFeedbackService.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
/**
|
||||
* Feedback Service Factory
|
||||
*
|
||||
* Creates a feedback service instance configured for a specific app.
|
||||
* Handles feedback submission, retrieval, and voting.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { createFeedbackService } from '@manacore/shared-feedback-service';
|
||||
* import { authStore } from '$lib/stores/auth.svelte';
|
||||
*
|
||||
* export const feedbackService = createFeedbackService({
|
||||
* apiUrl: 'https://auth.manacore.app',
|
||||
* appId: 'chat',
|
||||
* getAuthToken: async () => authStore.getToken(),
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
import type {
|
||||
Feedback,
|
||||
CreateFeedbackInput,
|
||||
FeedbackQueryParams,
|
||||
FeedbackResponse,
|
||||
FeedbackListResponse,
|
||||
VoteResponse,
|
||||
} from '@manacore/shared-feedback-types';
|
||||
import type { FeedbackServiceConfig } from './types';
|
||||
|
||||
/**
|
||||
* Create a feedback service instance
|
||||
*/
|
||||
export function createFeedbackService(config: FeedbackServiceConfig) {
|
||||
const { apiUrl, appId, getAuthToken, feedbackEndpoint = '/api/v1/feedback' } = config;
|
||||
|
||||
// Normalize API URL (remove trailing slash)
|
||||
const baseUrl = apiUrl.replace(/\/$/, '');
|
||||
|
||||
/**
|
||||
* Helper to make authenticated requests
|
||||
*/
|
||||
async function fetchWithAuth<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'X-App-Id': appId,
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit new feedback
|
||||
*/
|
||||
async function createFeedback(input: CreateFeedbackInput): Promise<FeedbackResponse> {
|
||||
return fetchWithAuth<FeedbackResponse>(feedbackEndpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public community feedback
|
||||
*/
|
||||
async function getPublicFeedback(query?: FeedbackQueryParams): Promise<FeedbackListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// Always filter by current app
|
||||
params.set('appId', appId);
|
||||
|
||||
if (query?.status) params.set('status', query.status);
|
||||
if (query?.category) params.set('category', query.category);
|
||||
if (query?.sort) params.set('sort', query.sort);
|
||||
if (query?.limit) params.set('limit', String(query.limit));
|
||||
if (query?.offset) params.set('offset', String(query.offset));
|
||||
|
||||
return fetchWithAuth<FeedbackListResponse>(`${feedbackEndpoint}/public?${params}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's own feedback
|
||||
*/
|
||||
async function getMyFeedback(): Promise<FeedbackListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
params.set('appId', appId);
|
||||
|
||||
return fetchWithAuth<FeedbackListResponse>(`${feedbackEndpoint}/my?${params}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vote on a feedback item
|
||||
*/
|
||||
async function vote(feedbackId: string): Promise<VoteResponse> {
|
||||
return fetchWithAuth<VoteResponse>(`${feedbackEndpoint}/${feedbackId}/vote`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove vote from a feedback item
|
||||
*/
|
||||
async function unvote(feedbackId: string): Promise<VoteResponse> {
|
||||
return fetchWithAuth<VoteResponse>(`${feedbackEndpoint}/${feedbackId}/vote`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle vote on a feedback item
|
||||
*/
|
||||
async function toggleVote(feedbackId: string, currentlyVoted: boolean): Promise<VoteResponse> {
|
||||
if (currentlyVoted) {
|
||||
return unvote(feedbackId);
|
||||
} else {
|
||||
return vote(feedbackId);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
createFeedback,
|
||||
getPublicFeedback,
|
||||
getMyFeedback,
|
||||
vote,
|
||||
unvote,
|
||||
toggleVote,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Type for the feedback service instance
|
||||
*/
|
||||
export type FeedbackService = ReturnType<typeof createFeedbackService>;
|
||||
24
packages/shared-feedback-service/src/index.ts
Normal file
24
packages/shared-feedback-service/src/index.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* Shared feedback service for Manacore monorepo
|
||||
*
|
||||
* This package provides a factory function to create feedback service
|
||||
* instances that can be used across all web and mobile apps.
|
||||
*/
|
||||
|
||||
export { createFeedbackService, type FeedbackService } from './createFeedbackService';
|
||||
export type { FeedbackServiceConfig } from './types';
|
||||
|
||||
// Re-export types from shared-feedback-types for convenience
|
||||
export type {
|
||||
Feedback,
|
||||
FeedbackCategory,
|
||||
FeedbackStatus,
|
||||
FeedbackVote,
|
||||
CreateFeedbackInput,
|
||||
FeedbackQueryParams,
|
||||
FeedbackResponse,
|
||||
FeedbackListResponse,
|
||||
VoteResponse,
|
||||
FEEDBACK_CATEGORY_LABELS,
|
||||
FEEDBACK_STATUS_CONFIG,
|
||||
} from '@manacore/shared-feedback-types';
|
||||
13
packages/shared-feedback-service/src/types.ts
Normal file
13
packages/shared-feedback-service/src/types.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Configuration for creating a feedback service instance
|
||||
*/
|
||||
export interface FeedbackServiceConfig {
|
||||
/** Base API URL for the feedback endpoints */
|
||||
apiUrl: string;
|
||||
/** App identifier for multi-app support */
|
||||
appId: string;
|
||||
/** Function to get the current auth token */
|
||||
getAuthToken: () => Promise<string | null>;
|
||||
/** Optional custom endpoint prefix (default: '/api/v1/feedback') */
|
||||
feedbackEndpoint?: string;
|
||||
}
|
||||
17
packages/shared-feedback-service/tsconfig.json
Normal file
17
packages/shared-feedback-service/tsconfig.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
19
packages/shared-feedback-types/package.json
Normal file
19
packages/shared-feedback-types/package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "@manacore/shared-feedback-types",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./feedback": "./src/feedback.ts",
|
||||
"./api": "./src/api.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
41
packages/shared-feedback-types/src/api.ts
Normal file
41
packages/shared-feedback-types/src/api.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* API request/response types for feedback
|
||||
*/
|
||||
|
||||
import type { Feedback, FeedbackCategory, FeedbackStatus } from './feedback';
|
||||
|
||||
export interface CreateFeedbackInput {
|
||||
title?: string;
|
||||
feedbackText: string;
|
||||
category?: FeedbackCategory;
|
||||
deviceInfo?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface FeedbackQueryParams {
|
||||
appId?: string;
|
||||
status?: FeedbackStatus;
|
||||
category?: FeedbackCategory;
|
||||
sort?: 'votes' | 'recent';
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface FeedbackResponse {
|
||||
success: boolean;
|
||||
feedback?: Feedback;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface FeedbackListResponse {
|
||||
success: boolean;
|
||||
items: Feedback[];
|
||||
total: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface VoteResponse {
|
||||
success: boolean;
|
||||
newVoteCount: number;
|
||||
userHasVoted: boolean;
|
||||
error?: string;
|
||||
}
|
||||
59
packages/shared-feedback-types/src/feedback.ts
Normal file
59
packages/shared-feedback-types/src/feedback.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Core feedback types
|
||||
*/
|
||||
|
||||
export type FeedbackCategory = 'bug' | 'feature' | 'improvement' | 'question' | 'other';
|
||||
|
||||
export type FeedbackStatus =
|
||||
| 'submitted'
|
||||
| 'under_review'
|
||||
| 'planned'
|
||||
| 'in_progress'
|
||||
| 'completed'
|
||||
| 'declined';
|
||||
|
||||
export interface Feedback {
|
||||
id: string;
|
||||
userId: string;
|
||||
appId: string;
|
||||
title?: string;
|
||||
feedbackText: string;
|
||||
category: FeedbackCategory;
|
||||
status: FeedbackStatus;
|
||||
isPublic: boolean;
|
||||
adminResponse?: string;
|
||||
voteCount: number;
|
||||
userHasVoted: boolean;
|
||||
deviceInfo?: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
publishedAt?: string;
|
||||
completedAt?: string;
|
||||
}
|
||||
|
||||
export interface FeedbackVote {
|
||||
id: string;
|
||||
feedbackId: string;
|
||||
userId: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export const FEEDBACK_CATEGORY_LABELS: Record<FeedbackCategory, string> = {
|
||||
bug: 'Bug',
|
||||
feature: 'Feature',
|
||||
improvement: 'Verbesserung',
|
||||
question: 'Frage',
|
||||
other: 'Sonstiges',
|
||||
};
|
||||
|
||||
export const FEEDBACK_STATUS_CONFIG: Record<
|
||||
FeedbackStatus,
|
||||
{ label: string; color: string; icon: string }
|
||||
> = {
|
||||
submitted: { label: 'Eingereicht', color: '#999', icon: 'clock' },
|
||||
under_review: { label: 'Wird geprüft', color: '#3498DB', icon: 'eye' },
|
||||
planned: { label: 'Geplant', color: '#9B59B6', icon: 'calendar' },
|
||||
in_progress: { label: 'In Arbeit', color: '#F39C12', icon: 'loader' },
|
||||
completed: { label: 'Umgesetzt', color: '#27AE60', icon: 'check-circle' },
|
||||
declined: { label: 'Abgelehnt', color: '#E74C3C', icon: 'x-circle' },
|
||||
};
|
||||
25
packages/shared-feedback-types/src/index.ts
Normal file
25
packages/shared-feedback-types/src/index.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* Shared feedback types for Manacore monorepo
|
||||
*
|
||||
* This package contains TypeScript types for feedback submissions,
|
||||
* voting, and API contracts.
|
||||
*/
|
||||
|
||||
// Feedback types
|
||||
export {
|
||||
type FeedbackCategory,
|
||||
type FeedbackStatus,
|
||||
type Feedback,
|
||||
type FeedbackVote,
|
||||
FEEDBACK_CATEGORY_LABELS,
|
||||
FEEDBACK_STATUS_CONFIG,
|
||||
} from './feedback';
|
||||
|
||||
// API types
|
||||
export {
|
||||
type CreateFeedbackInput,
|
||||
type FeedbackQueryParams,
|
||||
type FeedbackResponse,
|
||||
type FeedbackListResponse,
|
||||
type VoteResponse,
|
||||
} from './api';
|
||||
17
packages/shared-feedback-types/tsconfig.json
Normal file
17
packages/shared-feedback-types/tsconfig.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
55
packages/shared-feedback-ui/package.json
Normal file
55
packages/shared-feedback-ui/package.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "@manacore/shared-feedback-ui",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"svelte": "./src/index.ts",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"svelte": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./FeedbackPage.svelte": {
|
||||
"svelte": "./src/FeedbackPage.svelte",
|
||||
"default": "./src/FeedbackPage.svelte"
|
||||
},
|
||||
"./FeedbackForm.svelte": {
|
||||
"svelte": "./src/FeedbackForm.svelte",
|
||||
"default": "./src/FeedbackForm.svelte"
|
||||
},
|
||||
"./FeedbackList.svelte": {
|
||||
"svelte": "./src/FeedbackList.svelte",
|
||||
"default": "./src/FeedbackList.svelte"
|
||||
},
|
||||
"./FeedbackCard.svelte": {
|
||||
"svelte": "./src/FeedbackCard.svelte",
|
||||
"default": "./src/FeedbackCard.svelte"
|
||||
},
|
||||
"./VoteButton.svelte": {
|
||||
"svelte": "./src/VoteButton.svelte",
|
||||
"default": "./src/VoteButton.svelte"
|
||||
},
|
||||
"./StatusBadge.svelte": {
|
||||
"svelte": "./src/StatusBadge.svelte",
|
||||
"default": "./src/StatusBadge.svelte"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/shared-feedback-types": "workspace:*",
|
||||
"@manacore/shared-feedback-service": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0"
|
||||
}
|
||||
}
|
||||
165
packages/shared-feedback-ui/src/FeedbackCard.svelte
Normal file
165
packages/shared-feedback-ui/src/FeedbackCard.svelte
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
<script lang="ts">
|
||||
import type { Feedback } from '@manacore/shared-feedback-types';
|
||||
import StatusBadge from './StatusBadge.svelte';
|
||||
import VoteButton from './VoteButton.svelte';
|
||||
|
||||
interface Props {
|
||||
feedback: Feedback;
|
||||
onVote: (feedbackId: string, hasVoted: boolean) => void;
|
||||
showStatus?: boolean;
|
||||
isOwner?: boolean;
|
||||
votingDisabled?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
feedback,
|
||||
onVote,
|
||||
showStatus = true,
|
||||
isOwner = false,
|
||||
votingDisabled = false,
|
||||
}: Props = $props();
|
||||
|
||||
function handleVote() {
|
||||
onVote(feedback.id, feedback.userHasVoted);
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="feedback-card" class:feedback-card--owner={isOwner}>
|
||||
<div class="feedback-card__vote">
|
||||
<VoteButton
|
||||
count={feedback.voteCount}
|
||||
hasVoted={feedback.userHasVoted}
|
||||
onToggle={handleVote}
|
||||
disabled={votingDisabled || !feedback.isPublic}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="feedback-card__content">
|
||||
<div class="feedback-card__header">
|
||||
{#if feedback.title}
|
||||
<h3 class="feedback-card__title">{feedback.title}</h3>
|
||||
{/if}
|
||||
{#if showStatus}
|
||||
<StatusBadge status={feedback.status} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<p class="feedback-card__text">{feedback.feedbackText}</p>
|
||||
|
||||
{#if feedback.adminResponse}
|
||||
<div class="feedback-card__response">
|
||||
<span class="feedback-card__response-label">Admin-Antwort:</span>
|
||||
<p class="feedback-card__response-text">{feedback.adminResponse}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="feedback-card__footer">
|
||||
<span class="feedback-card__date">{formatDate(feedback.createdAt)}</span>
|
||||
{#if isOwner}
|
||||
<span class="feedback-card__owner-badge">Dein Feedback</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.feedback-card {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border-radius: 0.75rem;
|
||||
background: hsl(var(--color-surface, 0 0% 100%));
|
||||
border: 1px solid hsl(var(--color-border, 0 0% 90%));
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.feedback-card:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.feedback-card--owner {
|
||||
border-left: 3px solid hsl(var(--color-primary, 47 95% 58%));
|
||||
}
|
||||
|
||||
.feedback-card__vote {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.feedback-card__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.feedback-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.feedback-card__title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-foreground, 0 0% 17%));
|
||||
}
|
||||
|
||||
.feedback-card__text {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--color-foreground, 0 0% 17%));
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.feedback-card__response {
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(var(--color-primary, 47 95% 58%) / 0.1);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.feedback-card__response-label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-primary, 47 95% 58%));
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.feedback-card__response-text {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--color-foreground, 0 0% 17%));
|
||||
}
|
||||
|
||||
.feedback-card__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.feedback-card__date {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--color-muted-foreground, 0 0% 40%));
|
||||
}
|
||||
|
||||
.feedback-card__owner-badge {
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
background: hsl(var(--color-primary, 47 95% 58%) / 0.1);
|
||||
color: hsl(var(--color-primary, 47 95% 58%));
|
||||
}
|
||||
</style>
|
||||
195
packages/shared-feedback-ui/src/FeedbackForm.svelte
Normal file
195
packages/shared-feedback-ui/src/FeedbackForm.svelte
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
<script lang="ts">
|
||||
import type { CreateFeedbackInput } from '@manacore/shared-feedback-types';
|
||||
|
||||
interface Props {
|
||||
onSubmit: (input: CreateFeedbackInput) => Promise<void>;
|
||||
onCancel?: () => void;
|
||||
isSubmitting?: boolean;
|
||||
feedbackLabel?: string;
|
||||
submitLabel?: string;
|
||||
cancelLabel?: string;
|
||||
feedbackPlaceholder?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
onSubmit,
|
||||
onCancel,
|
||||
isSubmitting = false,
|
||||
feedbackLabel = 'Dein Feedback',
|
||||
submitLabel = 'Feedback senden',
|
||||
cancelLabel = 'Abbrechen',
|
||||
feedbackPlaceholder = 'Was gefällt dir? Was können wir verbessern?',
|
||||
}: Props = $props();
|
||||
|
||||
let feedbackText = $state('');
|
||||
let error = $state('');
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
error = '';
|
||||
|
||||
if (feedbackText.trim().length < 10) {
|
||||
error = 'Bitte gib mindestens 10 Zeichen ein.';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await onSubmit({
|
||||
feedbackText: feedbackText.trim(),
|
||||
});
|
||||
|
||||
// Reset form on success
|
||||
feedbackText = '';
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten.';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="feedback-form" onsubmit={handleSubmit}>
|
||||
<div class="feedback-form__field">
|
||||
<label for="feedback-text" class="feedback-form__label">{feedbackLabel}</label>
|
||||
<textarea
|
||||
id="feedback-text"
|
||||
class="feedback-form__textarea"
|
||||
placeholder={feedbackPlaceholder}
|
||||
bind:value={feedbackText}
|
||||
rows="5"
|
||||
maxlength="2000"
|
||||
disabled={isSubmitting}
|
||||
required
|
||||
></textarea>
|
||||
<span class="feedback-form__counter">{feedbackText.length}/2000</span>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="feedback-form__error">{error}</div>
|
||||
{/if}
|
||||
|
||||
<div class="feedback-form__actions">
|
||||
{#if onCancel}
|
||||
<button
|
||||
type="button"
|
||||
class="feedback-form__button feedback-form__button--secondary"
|
||||
onclick={onCancel}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="submit"
|
||||
class="feedback-form__button feedback-form__button--primary"
|
||||
disabled={isSubmitting || feedbackText.trim().length < 10}
|
||||
>
|
||||
{#if isSubmitting}
|
||||
Wird gesendet...
|
||||
{:else}
|
||||
{submitLabel}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.feedback-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.feedback-form__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.feedback-form__label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-foreground, 0 0% 17%));
|
||||
}
|
||||
|
||||
.feedback-form__input,
|
||||
.feedback-form__select,
|
||||
.feedback-form__textarea {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid hsl(var(--color-border, 0 0% 90%));
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--color-foreground, 0 0% 17%));
|
||||
background: hsl(var(--color-input, 0 0% 100%));
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.feedback-form__textarea::placeholder {
|
||||
color: hsl(var(--color-muted-foreground, 0 0% 40%));
|
||||
}
|
||||
|
||||
.feedback-form__input:focus,
|
||||
.feedback-form__select:focus,
|
||||
.feedback-form__textarea:focus {
|
||||
outline: none;
|
||||
border-color: hsl(var(--color-primary, 47 95% 58%));
|
||||
}
|
||||
|
||||
.feedback-form__textarea {
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.feedback-form__counter {
|
||||
align-self: flex-end;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--color-muted-foreground, 0 0% 40%));
|
||||
}
|
||||
|
||||
.feedback-form__error {
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(var(--color-error, 6 78% 57%) / 0.1);
|
||||
color: hsl(var(--color-error, 6 78% 57%));
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.feedback-form__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.feedback-form__button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.feedback-form__button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.feedback-form__button--primary {
|
||||
background: hsl(var(--color-primary, 47 95% 58%));
|
||||
color: hsl(var(--color-primary-foreground, 0 0% 0%));
|
||||
}
|
||||
|
||||
.feedback-form__button--primary:hover:not(:disabled) {
|
||||
background: hsl(var(--color-primary, 47 95% 58%) / 0.9);
|
||||
}
|
||||
|
||||
.feedback-form__button--secondary {
|
||||
background: transparent;
|
||||
border: 1px solid hsl(var(--color-border, 0 0% 90%));
|
||||
color: hsl(var(--color-foreground, 0 0% 17%));
|
||||
}
|
||||
|
||||
.feedback-form__button--secondary:hover:not(:disabled) {
|
||||
background: hsl(var(--color-muted, 0 0% 90%));
|
||||
}
|
||||
</style>
|
||||
58
packages/shared-feedback-ui/src/FeedbackList.svelte
Normal file
58
packages/shared-feedback-ui/src/FeedbackList.svelte
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<script lang="ts">
|
||||
import type { Feedback } from '@manacore/shared-feedback-types';
|
||||
import FeedbackCard from './FeedbackCard.svelte';
|
||||
|
||||
interface Props {
|
||||
items: Feedback[];
|
||||
currentUserId?: string;
|
||||
onVote: (feedbackId: string, hasVoted: boolean) => void;
|
||||
votingDisabled?: boolean;
|
||||
emptyMessage?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
items,
|
||||
currentUserId,
|
||||
onVote,
|
||||
votingDisabled = false,
|
||||
emptyMessage = 'Noch kein Feedback vorhanden',
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="feedback-list">
|
||||
{#if items.length === 0}
|
||||
<div class="feedback-list__empty">
|
||||
<p>{emptyMessage}</p>
|
||||
</div>
|
||||
{:else}
|
||||
{#each items as feedback (feedback.id)}
|
||||
<FeedbackCard
|
||||
{feedback}
|
||||
{onVote}
|
||||
{votingDisabled}
|
||||
isOwner={currentUserId === feedback.userId}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.feedback-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.feedback-list__empty {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
border-radius: 0.75rem;
|
||||
background: hsl(var(--color-surface, 0 0% 100%) / 0.5);
|
||||
border: 1px dashed hsl(var(--color-border, 0 0% 90%));
|
||||
}
|
||||
|
||||
.feedback-list__empty p {
|
||||
margin: 0;
|
||||
color: hsl(var(--color-muted-foreground, 0 0% 40%));
|
||||
}
|
||||
</style>
|
||||
384
packages/shared-feedback-ui/src/FeedbackPage.svelte
Normal file
384
packages/shared-feedback-ui/src/FeedbackPage.svelte
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
<script lang="ts">
|
||||
import type { FeedbackService, Feedback } from '@manacore/shared-feedback-service';
|
||||
import FeedbackForm from './FeedbackForm.svelte';
|
||||
import FeedbackList from './FeedbackList.svelte';
|
||||
|
||||
interface Props {
|
||||
/** Pre-configured feedback service instance */
|
||||
feedbackService: FeedbackService;
|
||||
/** App name for display */
|
||||
appName: string;
|
||||
/** Current user ID for highlighting own feedback */
|
||||
currentUserId?: string;
|
||||
/** Page title */
|
||||
pageTitle?: string;
|
||||
/** Page subtitle */
|
||||
pageSubtitle?: string;
|
||||
/** Tab label for own feedback */
|
||||
myFeedbackLabel?: string;
|
||||
/** Tab label for community feedback */
|
||||
communityLabel?: string;
|
||||
/** Empty state message for own feedback */
|
||||
myFeedbackEmptyMessage?: string;
|
||||
/** Empty state message for community feedback */
|
||||
communityEmptyMessage?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
feedbackService,
|
||||
appName,
|
||||
currentUserId,
|
||||
pageTitle = 'Feedback & Vorschläge',
|
||||
pageSubtitle = 'Teile deine Ideen und stimme für Feature-Wünsche ab',
|
||||
myFeedbackLabel = 'Mein Feedback',
|
||||
communityLabel = 'Community',
|
||||
myFeedbackEmptyMessage = 'Du hast noch kein Feedback eingereicht',
|
||||
communityEmptyMessage = 'Noch keine öffentlichen Vorschläge',
|
||||
}: Props = $props();
|
||||
|
||||
// State
|
||||
let activeTab = $state<'my' | 'community'>('community');
|
||||
let myFeedback = $state<Feedback[]>([]);
|
||||
let publicFeedback = $state<Feedback[]>([]);
|
||||
let isLoading = $state(true);
|
||||
let isSubmitting = $state(false);
|
||||
let showForm = $state(false);
|
||||
let successMessage = $state('');
|
||||
|
||||
// Load data on mount
|
||||
$effect(() => {
|
||||
loadFeedback();
|
||||
});
|
||||
|
||||
async function loadFeedback() {
|
||||
isLoading = true;
|
||||
try {
|
||||
const [myResult, publicResult] = await Promise.all([
|
||||
feedbackService.getMyFeedback(),
|
||||
feedbackService.getPublicFeedback({ sort: 'votes' }),
|
||||
]);
|
||||
myFeedback = myResult.items;
|
||||
publicFeedback = publicResult.items;
|
||||
} catch (error) {
|
||||
console.error('[FeedbackPage] Error loading feedback:', error);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(input: { title?: string; feedbackText: string; category?: string }) {
|
||||
isSubmitting = true;
|
||||
try {
|
||||
await feedbackService.createFeedback(input);
|
||||
showForm = false;
|
||||
successMessage = 'Feedback erfolgreich gesendet!';
|
||||
setTimeout(() => {
|
||||
successMessage = '';
|
||||
}, 3000);
|
||||
await loadFeedback();
|
||||
} finally {
|
||||
isSubmitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleVote(feedbackId: string, hasVoted: boolean) {
|
||||
try {
|
||||
await feedbackService.toggleVote(feedbackId, hasVoted);
|
||||
await loadFeedback();
|
||||
} catch (error) {
|
||||
console.error('[FeedbackPage] Error voting:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveTab(tab: 'my' | 'community') {
|
||||
activeTab = tab;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{pageTitle} - {appName}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="feedback-page">
|
||||
<div class="feedback-page__container">
|
||||
<!-- Header -->
|
||||
<div class="feedback-page__header">
|
||||
<div class="feedback-page__icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="feedback-page__title">{pageTitle}</h1>
|
||||
<p class="feedback-page__subtitle">{pageSubtitle}</p>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
{#if successMessage}
|
||||
<div class="feedback-page__success">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
||||
<polyline points="22 4 12 14.01 9 11.01" />
|
||||
</svg>
|
||||
{successMessage}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- New Feedback Button / Form -->
|
||||
<div class="feedback-page__form-section">
|
||||
{#if showForm}
|
||||
<div class="feedback-page__form-card">
|
||||
<h2 class="feedback-page__form-title">Neues Feedback</h2>
|
||||
<FeedbackForm
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => (showForm = false)}
|
||||
{isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<button class="feedback-page__new-button" onclick={() => (showForm = true)}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
Feedback geben
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="feedback-page__tabs">
|
||||
<button
|
||||
class="feedback-page__tab"
|
||||
class:feedback-page__tab--active={activeTab === 'community'}
|
||||
onclick={() => setActiveTab('community')}
|
||||
>
|
||||
{communityLabel}
|
||||
<span class="feedback-page__tab-count">{publicFeedback.length}</span>
|
||||
</button>
|
||||
<button
|
||||
class="feedback-page__tab"
|
||||
class:feedback-page__tab--active={activeTab === 'my'}
|
||||
onclick={() => setActiveTab('my')}
|
||||
>
|
||||
{myFeedbackLabel}
|
||||
<span class="feedback-page__tab-count">{myFeedback.length}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="feedback-page__content">
|
||||
{#if isLoading}
|
||||
<div class="feedback-page__loading">
|
||||
<div class="feedback-page__spinner"></div>
|
||||
<p>Lade Feedback...</p>
|
||||
</div>
|
||||
{:else if activeTab === 'community'}
|
||||
<FeedbackList
|
||||
items={publicFeedback}
|
||||
{currentUserId}
|
||||
onVote={handleVote}
|
||||
emptyMessage={communityEmptyMessage}
|
||||
/>
|
||||
{:else}
|
||||
<FeedbackList
|
||||
items={myFeedback}
|
||||
{currentUserId}
|
||||
onVote={handleVote}
|
||||
votingDisabled={true}
|
||||
emptyMessage={myFeedbackEmptyMessage}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.feedback-page {
|
||||
min-height: 100%;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.feedback-page__container {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.feedback-page__header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.feedback-page__icon {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
margin: 0 auto 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 1rem;
|
||||
background: hsl(var(--color-surface, 0 0% 100%));
|
||||
border: 1px solid hsl(var(--color-border, 0 0% 90%));
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
color: hsl(var(--color-primary, 47 95% 58%));
|
||||
}
|
||||
|
||||
.feedback-page__icon svg {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.feedback-page__title {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-foreground, 0 0% 17%));
|
||||
}
|
||||
|
||||
.feedback-page__subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--color-muted-foreground, 0 0% 40%));
|
||||
}
|
||||
|
||||
.feedback-page__success {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(var(--color-success, 145 63% 42%) / 0.1);
|
||||
color: hsl(var(--color-success, 145 63% 42%));
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.feedback-page__success svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.feedback-page__form-section {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.feedback-page__new-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: 2px dashed hsl(var(--color-border, 0 0% 90%));
|
||||
border-radius: 0.75rem;
|
||||
background: transparent;
|
||||
color: hsl(var(--color-muted-foreground, 0 0% 40%));
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.feedback-page__new-button:hover {
|
||||
border-color: hsl(var(--color-primary, 47 95% 58%));
|
||||
color: hsl(var(--color-primary, 47 95% 58%));
|
||||
background: hsl(var(--color-primary, 47 95% 58%) / 0.05);
|
||||
}
|
||||
|
||||
.feedback-page__new-button svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.feedback-page__form-card {
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
background: hsl(var(--color-surface, 0 0% 100%));
|
||||
border: 1px solid hsl(var(--color-border, 0 0% 90%));
|
||||
}
|
||||
|
||||
.feedback-page__form-title {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-foreground, 0 0% 17%));
|
||||
}
|
||||
|
||||
.feedback-page__tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(var(--color-muted, 0 0% 90%));
|
||||
}
|
||||
|
||||
.feedback-page__tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
background: transparent;
|
||||
color: hsl(var(--color-muted-foreground, 0 0% 40%));
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.feedback-page__tab:hover {
|
||||
color: hsl(var(--color-foreground, 0 0% 17%));
|
||||
}
|
||||
|
||||
.feedback-page__tab--active {
|
||||
background: hsl(var(--color-surface, 0 0% 100%));
|
||||
color: hsl(var(--color-foreground, 0 0% 17%));
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.feedback-page__tab-count {
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
background: hsl(var(--color-muted, 0 0% 90%));
|
||||
}
|
||||
|
||||
.feedback-page__tab--active .feedback-page__tab-count {
|
||||
background: hsl(var(--color-primary, 47 95% 58%) / 0.1);
|
||||
color: hsl(var(--color-primary, 47 95% 58%));
|
||||
}
|
||||
|
||||
.feedback-page__content {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.feedback-page__loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
padding: 3rem;
|
||||
color: hsl(var(--color-muted-foreground, 0 0% 40%));
|
||||
}
|
||||
|
||||
.feedback-page__spinner {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 2px solid hsl(var(--color-border, 0 0% 90%));
|
||||
border-top-color: hsl(var(--color-primary, 47 95% 58%));
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
31
packages/shared-feedback-ui/src/StatusBadge.svelte
Normal file
31
packages/shared-feedback-ui/src/StatusBadge.svelte
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<script lang="ts">
|
||||
import type { FeedbackStatus } from '@manacore/shared-feedback-types';
|
||||
import { FEEDBACK_STATUS_CONFIG } from '@manacore/shared-feedback-types';
|
||||
|
||||
interface Props {
|
||||
status: FeedbackStatus;
|
||||
}
|
||||
|
||||
let { status }: Props = $props();
|
||||
|
||||
const config = $derived(FEEDBACK_STATUS_CONFIG[status]);
|
||||
</script>
|
||||
|
||||
<span class="status-badge" style="--badge-color: {config.color};">
|
||||
{config.label}
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
background: color-mix(in srgb, var(--badge-color) 15%, transparent);
|
||||
color: var(--badge-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
91
packages/shared-feedback-ui/src/VoteButton.svelte
Normal file
91
packages/shared-feedback-ui/src/VoteButton.svelte
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
count: number;
|
||||
hasVoted: boolean;
|
||||
onToggle: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
let { count, hasVoted, onToggle, disabled = false }: Props = $props();
|
||||
|
||||
let isAnimating = $state(false);
|
||||
|
||||
function handleClick() {
|
||||
if (disabled) return;
|
||||
isAnimating = true;
|
||||
onToggle();
|
||||
setTimeout(() => {
|
||||
isAnimating = false;
|
||||
}, 300);
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="vote-button"
|
||||
class:vote-button--voted={hasVoted}
|
||||
class:vote-button--animating={isAnimating}
|
||||
onclick={handleClick}
|
||||
{disabled}
|
||||
type="button"
|
||||
aria-label={hasVoted ? 'Stimme entfernen' : 'Abstimmen'}
|
||||
>
|
||||
<svg
|
||||
class="vote-button__icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M18 15l-6-6-6 6" />
|
||||
</svg>
|
||||
<span class="vote-button__count">{count}</span>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.vote-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid hsl(var(--color-border, 0 0% 90%));
|
||||
border-radius: 0.5rem;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: hsl(var(--color-muted-foreground, 0 0% 40%));
|
||||
min-width: 3rem;
|
||||
}
|
||||
|
||||
.vote-button:hover:not(:disabled) {
|
||||
border-color: hsl(var(--color-primary, 47 95% 58%));
|
||||
color: hsl(var(--color-primary, 47 95% 58%));
|
||||
}
|
||||
|
||||
.vote-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.vote-button--voted {
|
||||
background: hsl(var(--color-primary, 47 95% 58%) / 0.1);
|
||||
border-color: hsl(var(--color-primary, 47 95% 58%));
|
||||
color: hsl(var(--color-primary, 47 95% 58%));
|
||||
}
|
||||
|
||||
.vote-button--animating {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.vote-button__icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.vote-button__count {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
20
packages/shared-feedback-ui/src/index.ts
Normal file
20
packages/shared-feedback-ui/src/index.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* Shared feedback UI components for Manacore monorepo
|
||||
*
|
||||
* This package provides Svelte 5 components for feedback functionality
|
||||
* that can be used across all web apps.
|
||||
*/
|
||||
|
||||
// Page components
|
||||
export { default as FeedbackPage } from './FeedbackPage.svelte';
|
||||
|
||||
// Form components
|
||||
export { default as FeedbackForm } from './FeedbackForm.svelte';
|
||||
|
||||
// List components
|
||||
export { default as FeedbackList } from './FeedbackList.svelte';
|
||||
export { default as FeedbackCard } from './FeedbackCard.svelte';
|
||||
|
||||
// Utility components
|
||||
export { default as VoteButton } from './VoteButton.svelte';
|
||||
export { default as StatusBadge } from './StatusBadge.svelte';
|
||||
16
packages/shared-feedback-ui/tsconfig.json
Normal file
16
packages/shared-feedback-ui/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
568
pnpm-lock.yaml
generated
568
pnpm-lock.yaml
generated
|
|
@ -76,7 +76,7 @@ importers:
|
|||
devDependencies:
|
||||
'@nestjs/cli':
|
||||
specifier: ^10.4.9
|
||||
version: 10.4.9(esbuild@0.19.12)
|
||||
version: 10.4.9(esbuild@0.27.0)
|
||||
'@nestjs/schematics':
|
||||
specifier: ^10.2.3
|
||||
version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3)
|
||||
|
|
@ -109,7 +109,7 @@ importers:
|
|||
version: 0.5.21
|
||||
ts-loader:
|
||||
specifier: ^9.5.1
|
||||
version: 9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12))
|
||||
version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0))
|
||||
ts-node:
|
||||
specifier: ^10.9.2
|
||||
version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3)
|
||||
|
|
@ -1897,7 +1897,7 @@ importers:
|
|||
version: 0.5.21
|
||||
ts-loader:
|
||||
specifier: ^9.5.1
|
||||
version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0))
|
||||
version: 9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.27.0))
|
||||
ts-node:
|
||||
specifier: ^10.9.2
|
||||
version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3)
|
||||
|
|
@ -2841,7 +2841,7 @@ importers:
|
|||
version: 1.57.0
|
||||
jest:
|
||||
specifier: ^29.0.0
|
||||
version: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
|
||||
version: 29.7.0(@types/node@24.10.1)
|
||||
vitest:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
|
||||
|
|
@ -2880,6 +2880,9 @@ importers:
|
|||
|
||||
services/mana-core-auth:
|
||||
dependencies:
|
||||
'@google/generative-ai':
|
||||
specifier: ^0.24.1
|
||||
version: 0.24.1
|
||||
'@nestjs/common':
|
||||
specifier: ^10.4.15
|
||||
version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
|
|
@ -2952,7 +2955,7 @@ importers:
|
|||
devDependencies:
|
||||
'@nestjs/cli':
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.12(@types/node@22.19.1)
|
||||
version: 11.0.12(@types/node@22.19.1)(esbuild@0.19.12)
|
||||
'@nestjs/schematics':
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3)
|
||||
|
|
@ -3006,10 +3009,10 @@ importers:
|
|||
version: 7.1.4
|
||||
ts-jest:
|
||||
specifier: ^29.2.5
|
||||
version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3)
|
||||
version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3)
|
||||
ts-loader:
|
||||
specifier: ^9.5.1
|
||||
version: 9.5.4(typescript@5.9.3)(webpack@5.100.2)
|
||||
version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12))
|
||||
ts-node:
|
||||
specifier: ^10.9.2
|
||||
version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3)
|
||||
|
|
@ -4863,7 +4866,7 @@ packages:
|
|||
|
||||
'@expo/bunyan@4.0.1':
|
||||
resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==}
|
||||
engines: {'0': node >=0.10.0}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
'@expo/cli@0.22.26':
|
||||
resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==}
|
||||
|
|
@ -19093,7 +19096,7 @@ snapshots:
|
|||
wrap-ansi: 7.0.0
|
||||
ws: 8.18.3
|
||||
optionalDependencies:
|
||||
expo-router: 6.0.15(5e7ih2rh6mb55wruwvjljgzihq)
|
||||
expo-router: 6.0.15(jiucxy5ca3jdtbnulaxuc46jdq)
|
||||
react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)
|
||||
transitivePeerDependencies:
|
||||
- '@modelcontextprotocol/sdk'
|
||||
|
|
@ -19170,7 +19173,7 @@ snapshots:
|
|||
wrap-ansi: 7.0.0
|
||||
ws: 8.18.3
|
||||
optionalDependencies:
|
||||
expo-router: 6.0.15(nttrd3tw67nnyhowcwgdzipb5e)
|
||||
expo-router: 6.0.15(ohit2up6tuxb3x34brxduivol4)
|
||||
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)
|
||||
transitivePeerDependencies:
|
||||
- '@modelcontextprotocol/sdk'
|
||||
|
|
@ -20410,6 +20413,41 @@ snapshots:
|
|||
jest-util: 30.2.0
|
||||
slash: 3.0.0
|
||||
|
||||
'@jest/core@29.7.0':
|
||||
dependencies:
|
||||
'@jest/console': 29.7.0
|
||||
'@jest/reporters': 29.7.0
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 22.19.1
|
||||
ansi-escapes: 4.3.2
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-changed-files: 29.7.0
|
||||
jest-config: 29.7.0(@types/node@22.19.1)
|
||||
jest-haste-map: 29.7.0
|
||||
jest-message-util: 29.7.0
|
||||
jest-regex-util: 29.6.3
|
||||
jest-resolve: 29.7.0
|
||||
jest-resolve-dependencies: 29.7.0
|
||||
jest-runner: 29.7.0
|
||||
jest-runtime: 29.7.0
|
||||
jest-snapshot: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
jest-watcher: 29.7.0
|
||||
micromatch: 4.0.8
|
||||
pretty-format: 29.7.0
|
||||
slash: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
'@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@jest/console': 29.7.0
|
||||
|
|
@ -20445,78 +20483,6 @@ snapshots:
|
|||
- supports-color
|
||||
- ts-node
|
||||
|
||||
'@jest/core@29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@jest/console': 29.7.0
|
||||
'@jest/reporters': 29.7.0
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 22.19.1
|
||||
ansi-escapes: 4.3.2
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-changed-files: 29.7.0
|
||||
jest-config: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
|
||||
jest-haste-map: 29.7.0
|
||||
jest-message-util: 29.7.0
|
||||
jest-regex-util: 29.6.3
|
||||
jest-resolve: 29.7.0
|
||||
jest-resolve-dependencies: 29.7.0
|
||||
jest-runner: 29.7.0
|
||||
jest-runtime: 29.7.0
|
||||
jest-snapshot: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
jest-watcher: 29.7.0
|
||||
micromatch: 4.0.8
|
||||
pretty-format: 29.7.0
|
||||
slash: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
'@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@jest/console': 30.2.0
|
||||
'@jest/pattern': 30.0.1
|
||||
'@jest/reporters': 30.2.0
|
||||
'@jest/test-result': 30.2.0
|
||||
'@jest/transform': 30.2.0
|
||||
'@jest/types': 30.2.0
|
||||
'@types/node': 22.19.1
|
||||
ansi-escapes: 4.3.2
|
||||
chalk: 4.1.2
|
||||
ci-info: 4.3.1
|
||||
exit-x: 0.2.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-changed-files: 30.2.0
|
||||
jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||
jest-haste-map: 30.2.0
|
||||
jest-message-util: 30.2.0
|
||||
jest-regex-util: 30.0.1
|
||||
jest-resolve: 30.2.0
|
||||
jest-resolve-dependencies: 30.2.0
|
||||
jest-runner: 30.2.0
|
||||
jest-runtime: 30.2.0
|
||||
jest-snapshot: 30.2.0
|
||||
jest-util: 30.2.0
|
||||
jest-validate: 30.2.0
|
||||
jest-watcher: 30.2.0
|
||||
micromatch: 4.0.8
|
||||
pretty-format: 30.2.0
|
||||
slash: 3.0.0
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- esbuild-register
|
||||
- supports-color
|
||||
- ts-node
|
||||
optional: true
|
||||
|
||||
'@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))':
|
||||
dependencies:
|
||||
'@jest/console': 30.2.0
|
||||
|
|
@ -20943,32 +20909,6 @@ snapshots:
|
|||
- uglify-js
|
||||
- webpack-cli
|
||||
|
||||
'@nestjs/cli@10.4.9(esbuild@0.19.12)':
|
||||
dependencies:
|
||||
'@angular-devkit/core': 17.3.11(chokidar@3.6.0)
|
||||
'@angular-devkit/schematics': 17.3.11(chokidar@3.6.0)
|
||||
'@angular-devkit/schematics-cli': 17.3.11(chokidar@3.6.0)
|
||||
'@nestjs/schematics': 10.2.3(chokidar@3.6.0)(typescript@5.7.2)
|
||||
chalk: 4.1.2
|
||||
chokidar: 3.6.0
|
||||
cli-table3: 0.6.5
|
||||
commander: 4.1.1
|
||||
fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.19.12))
|
||||
glob: 10.4.5
|
||||
inquirer: 8.2.6
|
||||
node-emoji: 1.11.0
|
||||
ora: 5.4.1
|
||||
tree-kill: 1.2.2
|
||||
tsconfig-paths: 4.2.0
|
||||
tsconfig-paths-webpack-plugin: 4.2.0
|
||||
typescript: 5.7.2
|
||||
webpack: 5.97.1(esbuild@0.19.12)
|
||||
webpack-node-externals: 3.0.0
|
||||
transitivePeerDependencies:
|
||||
- esbuild
|
||||
- uglify-js
|
||||
- webpack-cli
|
||||
|
||||
'@nestjs/cli@10.4.9(esbuild@0.27.0)':
|
||||
dependencies:
|
||||
'@angular-devkit/core': 17.3.11(chokidar@3.6.0)
|
||||
|
|
@ -20995,7 +20935,7 @@ snapshots:
|
|||
- uglify-js
|
||||
- webpack-cli
|
||||
|
||||
'@nestjs/cli@11.0.12(@types/node@22.19.1)':
|
||||
'@nestjs/cli@11.0.12(@types/node@22.19.1)(esbuild@0.19.12)':
|
||||
dependencies:
|
||||
'@angular-devkit/core': 19.2.19(chokidar@4.0.3)
|
||||
'@angular-devkit/schematics': 19.2.19(chokidar@4.0.3)
|
||||
|
|
@ -21006,14 +20946,14 @@ snapshots:
|
|||
chokidar: 4.0.3
|
||||
cli-table3: 0.6.5
|
||||
commander: 4.1.1
|
||||
fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.100.2)
|
||||
fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12))
|
||||
glob: 12.0.0
|
||||
node-emoji: 1.11.0
|
||||
ora: 5.4.1
|
||||
tsconfig-paths: 4.2.0
|
||||
tsconfig-paths-webpack-plugin: 4.2.0
|
||||
typescript: 5.9.3
|
||||
webpack: 5.100.2
|
||||
webpack: 5.100.2(esbuild@0.19.12)
|
||||
webpack-node-externals: 3.0.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
|
@ -23447,7 +23387,7 @@ snapshots:
|
|||
react-test-renderer: 19.1.0(react@19.1.0)
|
||||
redent: 3.0.0
|
||||
|
||||
'@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
'@testing-library/react-native@13.3.3(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
jest-matcher-utils: 30.2.0
|
||||
picocolors: 1.1.1
|
||||
|
|
@ -23457,7 +23397,20 @@ snapshots:
|
|||
react-test-renderer: 19.1.0(react@19.1.0)
|
||||
redent: 3.0.0
|
||||
optionalDependencies:
|
||||
jest: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||
jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||
optional: true
|
||||
|
||||
'@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
jest-matcher-utils: 30.2.0
|
||||
picocolors: 1.1.1
|
||||
pretty-format: 30.2.0
|
||||
react: 19.1.0
|
||||
react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)
|
||||
react-test-renderer: 19.1.0(react@19.1.0)
|
||||
redent: 3.0.0
|
||||
optionalDependencies:
|
||||
jest: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))
|
||||
optional: true
|
||||
|
||||
'@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
|
|
@ -24419,11 +24372,11 @@ snapshots:
|
|||
- vite
|
||||
optional: true
|
||||
|
||||
'@vitest/browser@3.2.4(playwright@1.57.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)':
|
||||
'@vitest/browser@3.2.4(playwright@1.57.0)(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)':
|
||||
dependencies:
|
||||
'@testing-library/dom': 10.4.1
|
||||
'@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1)
|
||||
'@vitest/mocker': 3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))
|
||||
'@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))
|
||||
'@vitest/utils': 3.2.4
|
||||
magic-string: 0.30.21
|
||||
sirv: 3.0.2
|
||||
|
|
@ -24463,15 +24416,6 @@ snapshots:
|
|||
optionalDependencies:
|
||||
vite: 6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
|
||||
|
||||
'@vitest/mocker@3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
dependencies:
|
||||
tinyrainbow: 2.0.0
|
||||
|
|
@ -24501,7 +24445,7 @@ snapshots:
|
|||
sirv: 3.0.2
|
||||
tinyglobby: 0.2.15
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
|
||||
|
||||
'@vitest/utils@3.2.4':
|
||||
dependencies:
|
||||
|
|
@ -26183,13 +26127,13 @@ snapshots:
|
|||
- supports-color
|
||||
- ts-node
|
||||
|
||||
create-jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)):
|
||||
create-jest@29.7.0(@types/node@24.10.1):
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-config: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
|
||||
jest-config: 29.7.0(@types/node@24.10.1)
|
||||
jest-util: 29.7.0
|
||||
prompts: 2.4.2
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -27054,9 +26998,9 @@ snapshots:
|
|||
'@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-expo: 1.0.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1))
|
||||
globals: 16.5.0
|
||||
|
|
@ -27071,9 +27015,9 @@ snapshots:
|
|||
'@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3)
|
||||
'@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3)
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-expo: 0.1.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1))
|
||||
globals: 16.5.0
|
||||
|
|
@ -27143,7 +27087,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)):
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
|
|
@ -27154,22 +27098,7 @@ snapshots:
|
|||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
get-tsconfig: 4.13.0
|
||||
is-bun-module: 2.0.0
|
||||
stable-hash: 0.0.5
|
||||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -27193,25 +27122,25 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3)
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -27311,7 +27240,7 @@ snapshots:
|
|||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
|
|
@ -27322,7 +27251,7 @@ snapshots:
|
|||
doctrine: 2.1.0
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
|
|
@ -27340,7 +27269,7 @@ snapshots:
|
|||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
|
|
@ -27351,7 +27280,7 @@ snapshots:
|
|||
doctrine: 2.1.0
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
|
|
@ -28492,7 +28421,54 @@ snapshots:
|
|||
- '@types/react-dom'
|
||||
- supports-color
|
||||
|
||||
expo-router@6.0.15(nttrd3tw67nnyhowcwgdzipb5e):
|
||||
expo-router@6.0.15(jiucxy5ca3jdtbnulaxuc46jdq):
|
||||
dependencies:
|
||||
'@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
'@expo/schema-utils': 0.1.7
|
||||
'@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0)
|
||||
'@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
'@react-navigation/native': 7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
'@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
client-only: 0.0.1
|
||||
debug: 4.4.3
|
||||
escape-string-regexp: 4.0.0
|
||||
expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))
|
||||
expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
expo-server: 1.0.4
|
||||
fast-deep-equal: 3.1.3
|
||||
invariant: 2.2.4
|
||||
nanoid: 3.3.11
|
||||
query-string: 7.1.3
|
||||
react: 19.1.0
|
||||
react-fast-compare: 3.2.2
|
||||
react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)
|
||||
react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
react-native-safe-area-context: 5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
react-native-screens: 4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
semver: 7.6.3
|
||||
server-only: 0.0.1
|
||||
sf-symbols-typescript: 2.1.0
|
||||
shallowequal: 1.1.0
|
||||
use-latest-callback: 0.2.6(react@19.1.0)
|
||||
vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
'@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0))
|
||||
transitivePeerDependencies:
|
||||
- '@react-native-masked-view/masked-view'
|
||||
- '@types/react'
|
||||
- '@types/react-dom'
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
expo-router@6.0.15(ohit2up6tuxb3x34brxduivol4):
|
||||
dependencies:
|
||||
'@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
'@expo/schema-utils': 0.1.7
|
||||
|
|
@ -28526,12 +28502,12 @@ snapshots:
|
|||
vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
'@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@testing-library/react-native': 13.3.3(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.97.1(esbuild@0.19.12))
|
||||
react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.19.12))
|
||||
transitivePeerDependencies:
|
||||
- '@react-native-masked-view/masked-view'
|
||||
- '@types/react'
|
||||
|
|
@ -29389,23 +29365,6 @@ snapshots:
|
|||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.19.12)):
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
chalk: 4.1.2
|
||||
chokidar: 3.6.0
|
||||
cosmiconfig: 8.3.6(typescript@5.7.2)
|
||||
deepmerge: 4.3.1
|
||||
fs-extra: 10.1.0
|
||||
memfs: 3.5.3
|
||||
minimatch: 3.1.2
|
||||
node-abort-controller: 3.1.1
|
||||
schema-utils: 3.3.0
|
||||
semver: 7.7.3
|
||||
tapable: 2.3.0
|
||||
typescript: 5.7.2
|
||||
webpack: 5.97.1(esbuild@0.19.12)
|
||||
|
||||
fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.27.0)):
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
|
|
@ -29440,6 +29399,23 @@ snapshots:
|
|||
typescript: 5.7.2
|
||||
webpack: 5.97.1
|
||||
|
||||
fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)):
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
chalk: 4.1.2
|
||||
chokidar: 4.0.3
|
||||
cosmiconfig: 8.3.6(typescript@5.9.3)
|
||||
deepmerge: 4.3.1
|
||||
fs-extra: 10.1.0
|
||||
memfs: 3.5.3
|
||||
minimatch: 3.1.2
|
||||
node-abort-controller: 3.1.1
|
||||
schema-utils: 3.3.0
|
||||
semver: 7.7.3
|
||||
tapable: 2.3.0
|
||||
typescript: 5.9.3
|
||||
webpack: 5.100.2(esbuild@0.19.12)
|
||||
|
||||
fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)):
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
|
|
@ -29457,23 +29433,6 @@ snapshots:
|
|||
typescript: 5.9.3
|
||||
webpack: 5.100.2(esbuild@0.27.0)
|
||||
|
||||
fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2):
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
chalk: 4.1.2
|
||||
chokidar: 4.0.3
|
||||
cosmiconfig: 8.3.6(typescript@5.9.3)
|
||||
deepmerge: 4.3.1
|
||||
fs-extra: 10.1.0
|
||||
memfs: 3.5.3
|
||||
minimatch: 3.1.2
|
||||
node-abort-controller: 3.1.1
|
||||
schema-utils: 3.3.0
|
||||
semver: 7.7.3
|
||||
tapable: 2.3.0
|
||||
typescript: 5.9.3
|
||||
webpack: 5.100.2
|
||||
|
||||
form-data-encoder@1.7.2: {}
|
||||
|
||||
form-data@3.0.4:
|
||||
|
|
@ -30556,16 +30515,16 @@ snapshots:
|
|||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest-cli@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)):
|
||||
jest-cli@29.7.0(@types/node@24.10.1):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
|
||||
'@jest/core': 29.7.0
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
create-jest: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
|
||||
create-jest: 29.7.0(@types/node@24.10.1)
|
||||
exit: 0.1.2
|
||||
import-local: 3.2.0
|
||||
jest-config: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
|
||||
jest-config: 29.7.0(@types/node@24.10.1)
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
yargs: 17.7.2
|
||||
|
|
@ -30575,15 +30534,15 @@ snapshots:
|
|||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest-cli@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)):
|
||||
jest-cli@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)):
|
||||
dependencies:
|
||||
'@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||
'@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))
|
||||
'@jest/test-result': 30.2.0
|
||||
'@jest/types': 30.2.0
|
||||
chalk: 4.1.2
|
||||
exit-x: 0.2.2
|
||||
import-local: 3.2.0
|
||||
jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||
jest-config: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))
|
||||
jest-util: 30.2.0
|
||||
jest-validate: 30.2.0
|
||||
yargs: 17.7.2
|
||||
|
|
@ -30653,6 +30612,36 @@ snapshots:
|
|||
- ts-node
|
||||
optional: true
|
||||
|
||||
jest-config@29.7.0(@types/node@22.19.1):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@jest/test-sequencer': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
babel-jest: 29.7.0(@babel/core@7.28.5)
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
deepmerge: 4.3.1
|
||||
glob: 7.2.3
|
||||
graceful-fs: 4.2.11
|
||||
jest-circus: 29.7.0
|
||||
jest-environment-node: 29.7.0
|
||||
jest-get-type: 29.6.3
|
||||
jest-regex-util: 29.6.3
|
||||
jest-resolve: 29.7.0
|
||||
jest-runner: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
micromatch: 4.0.8
|
||||
parse-json: 5.2.0
|
||||
pretty-format: 29.7.0
|
||||
slash: 3.0.0
|
||||
strip-json-comments: 3.1.1
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.1
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-config@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
|
|
@ -30684,38 +30673,7 @@ snapshots:
|
|||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-config@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@jest/test-sequencer': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
babel-jest: 29.7.0(@babel/core@7.28.5)
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
deepmerge: 4.3.1
|
||||
glob: 7.2.3
|
||||
graceful-fs: 4.2.11
|
||||
jest-circus: 29.7.0
|
||||
jest-environment-node: 29.7.0
|
||||
jest-get-type: 29.6.3
|
||||
jest-regex-util: 29.6.3
|
||||
jest-resolve: 29.7.0
|
||||
jest-runner: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
micromatch: 4.0.8
|
||||
parse-json: 5.2.0
|
||||
pretty-format: 29.7.0
|
||||
slash: 3.0.0
|
||||
strip-json-comments: 3.1.1
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.1
|
||||
ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-config@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)):
|
||||
jest-config@29.7.0(@types/node@24.10.1):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@jest/test-sequencer': 29.7.0
|
||||
|
|
@ -30741,12 +30699,11 @@ snapshots:
|
|||
strip-json-comments: 3.1.1
|
||||
optionalDependencies:
|
||||
'@types/node': 24.10.1
|
||||
ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)):
|
||||
jest-config@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@jest/get-type': 30.1.0
|
||||
|
|
@ -30773,9 +30730,8 @@ snapshots:
|
|||
slash: 3.0.0
|
||||
strip-json-comments: 3.1.1
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.1
|
||||
esbuild-register: 3.6.0(esbuild@0.19.12)
|
||||
ts-node: 10.9.2(@types/node@22.19.1)(typescript@5.9.3)
|
||||
'@types/node': 20.19.25
|
||||
esbuild-register: 3.6.0(esbuild@0.27.0)
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
|
@ -31424,24 +31380,24 @@ snapshots:
|
|||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)):
|
||||
jest@29.7.0(@types/node@24.10.1):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
|
||||
'@jest/core': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
import-local: 3.2.0
|
||||
jest-cli: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
|
||||
jest-cli: 29.7.0(@types/node@24.10.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)):
|
||||
jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)):
|
||||
dependencies:
|
||||
'@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||
'@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))
|
||||
'@jest/types': 30.2.0
|
||||
import-local: 3.2.0
|
||||
jest-cli: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||
jest-cli: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
|
|
@ -35055,6 +35011,16 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/react': 19.2.7
|
||||
|
||||
react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.19.12)):
|
||||
dependencies:
|
||||
acorn-loose: 8.5.2
|
||||
neo-async: 2.6.2
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
webpack: 5.100.2(esbuild@0.19.12)
|
||||
webpack-sources: 3.3.3
|
||||
optional: true
|
||||
|
||||
react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)):
|
||||
dependencies:
|
||||
acorn-loose: 8.5.2
|
||||
|
|
@ -35064,16 +35030,6 @@ snapshots:
|
|||
webpack: 5.100.2(esbuild@0.27.0)
|
||||
webpack-sources: 3.3.3
|
||||
|
||||
react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.97.1(esbuild@0.19.12)):
|
||||
dependencies:
|
||||
acorn-loose: 8.5.2
|
||||
neo-async: 2.6.2
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
webpack: 5.97.1(esbuild@0.19.12)
|
||||
webpack-sources: 3.3.3
|
||||
optional: true
|
||||
|
||||
react-style-singleton@2.2.3(@types/react@18.3.27)(react@18.3.1):
|
||||
dependencies:
|
||||
get-nonce: 1.0.1
|
||||
|
|
@ -36296,14 +36252,14 @@ snapshots:
|
|||
ansi-escapes: 4.3.2
|
||||
supports-hyperlinks: 2.3.0
|
||||
|
||||
terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)):
|
||||
terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.100.2(esbuild@0.19.12)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 4.3.3
|
||||
serialize-javascript: 6.0.2
|
||||
terser: 5.44.1
|
||||
webpack: 5.97.1(esbuild@0.19.12)
|
||||
webpack: 5.100.2(esbuild@0.19.12)
|
||||
optionalDependencies:
|
||||
esbuild: 0.19.12
|
||||
|
||||
|
|
@ -36329,15 +36285,6 @@ snapshots:
|
|||
optionalDependencies:
|
||||
esbuild: 0.27.0
|
||||
|
||||
terser-webpack-plugin@5.3.14(webpack@5.100.2):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 4.3.3
|
||||
serialize-javascript: 6.0.2
|
||||
terser: 5.44.1
|
||||
webpack: 5.100.2
|
||||
|
||||
terser-webpack-plugin@5.3.14(webpack@5.97.1):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
|
@ -36493,6 +36440,27 @@ snapshots:
|
|||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3):
|
||||
dependencies:
|
||||
bs-logger: 0.2.6
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
handlebars: 4.7.8
|
||||
jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||
json5: 2.2.3
|
||||
lodash.memoize: 4.1.2
|
||||
make-error: 1.3.6
|
||||
semver: 7.7.3
|
||||
type-fest: 4.41.0
|
||||
typescript: 5.9.3
|
||||
yargs-parser: 21.1.1
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@jest/transform': 30.2.0
|
||||
'@jest/types': 30.2.0
|
||||
babel-jest: 30.2.0(@babel/core@7.28.5)
|
||||
esbuild: 0.19.12
|
||||
jest-util: 30.2.0
|
||||
|
||||
ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3):
|
||||
dependencies:
|
||||
bs-logger: 0.2.6
|
||||
|
|
@ -36514,25 +36482,15 @@ snapshots:
|
|||
esbuild: 0.27.0
|
||||
jest-util: 30.2.0
|
||||
|
||||
ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3):
|
||||
ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)):
|
||||
dependencies:
|
||||
bs-logger: 0.2.6
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
handlebars: 4.7.8
|
||||
jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||
json5: 2.2.3
|
||||
lodash.memoize: 4.1.2
|
||||
make-error: 1.3.6
|
||||
chalk: 4.1.2
|
||||
enhanced-resolve: 5.18.3
|
||||
micromatch: 4.0.8
|
||||
semver: 7.7.3
|
||||
type-fest: 4.41.0
|
||||
source-map: 0.7.6
|
||||
typescript: 5.9.3
|
||||
yargs-parser: 21.1.1
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@jest/transform': 30.2.0
|
||||
'@jest/types': 30.2.0
|
||||
babel-jest: 30.2.0(@babel/core@7.28.5)
|
||||
jest-util: 30.2.0
|
||||
webpack: 5.100.2(esbuild@0.19.12)
|
||||
|
||||
ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)):
|
||||
dependencies:
|
||||
|
|
@ -36544,7 +36502,7 @@ snapshots:
|
|||
typescript: 5.9.3
|
||||
webpack: 5.100.2(esbuild@0.27.0)
|
||||
|
||||
ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2):
|
||||
ts-loader@9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.27.0)):
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
enhanced-resolve: 5.18.3
|
||||
|
|
@ -36552,17 +36510,7 @@ snapshots:
|
|||
semver: 7.7.3
|
||||
source-map: 0.7.6
|
||||
typescript: 5.9.3
|
||||
webpack: 5.100.2
|
||||
|
||||
ts-loader@9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)):
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
enhanced-resolve: 5.18.3
|
||||
micromatch: 4.0.8
|
||||
semver: 7.7.3
|
||||
source-map: 0.7.6
|
||||
typescript: 5.9.3
|
||||
webpack: 5.97.1(esbuild@0.19.12)
|
||||
webpack: 5.97.1(esbuild@0.27.0)
|
||||
|
||||
ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3):
|
||||
dependencies:
|
||||
|
|
@ -37322,7 +37270,7 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/debug': 4.1.12
|
||||
'@types/node': 24.10.1
|
||||
'@vitest/browser': 3.2.4(playwright@1.57.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)
|
||||
'@vitest/browser': 3.2.4(playwright@1.57.0)(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)
|
||||
'@vitest/ui': 3.2.4(vitest@3.2.4)
|
||||
jsdom: 27.2.0
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -37485,7 +37433,7 @@ snapshots:
|
|||
|
||||
webpack-sources@3.3.3: {}
|
||||
|
||||
webpack@5.100.2:
|
||||
webpack@5.100.2(esbuild@0.19.12):
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
'@types/estree': 1.0.8
|
||||
|
|
@ -37509,7 +37457,7 @@ snapshots:
|
|||
neo-async: 2.6.2
|
||||
schema-utils: 4.3.3
|
||||
tapable: 2.3.0
|
||||
terser-webpack-plugin: 5.3.14(webpack@5.100.2)
|
||||
terser-webpack-plugin: 5.3.14(esbuild@0.19.12)(webpack@5.100.2(esbuild@0.19.12))
|
||||
watchpack: 2.4.4
|
||||
webpack-sources: 3.3.3
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -37579,36 +37527,6 @@ snapshots:
|
|||
- esbuild
|
||||
- uglify-js
|
||||
|
||||
webpack@5.97.1(esbuild@0.19.12):
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
'@types/estree': 1.0.8
|
||||
'@webassemblyjs/ast': 1.14.1
|
||||
'@webassemblyjs/wasm-edit': 1.14.1
|
||||
'@webassemblyjs/wasm-parser': 1.14.1
|
||||
acorn: 8.15.0
|
||||
browserslist: 4.28.0
|
||||
chrome-trace-event: 1.0.4
|
||||
enhanced-resolve: 5.18.3
|
||||
es-module-lexer: 1.7.0
|
||||
eslint-scope: 5.1.1
|
||||
events: 3.3.0
|
||||
glob-to-regexp: 0.4.1
|
||||
graceful-fs: 4.2.11
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
loader-runner: 4.3.1
|
||||
mime-types: 2.1.35
|
||||
neo-async: 2.6.2
|
||||
schema-utils: 3.3.0
|
||||
tapable: 2.3.0
|
||||
terser-webpack-plugin: 5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12))
|
||||
watchpack: 2.4.4
|
||||
webpack-sources: 3.3.3
|
||||
transitivePeerDependencies:
|
||||
- '@swc/core'
|
||||
- esbuild
|
||||
- uglify-js
|
||||
|
||||
webpack@5.97.1(esbuild@0.27.0):
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"db:studio": "drizzle-kit studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"@nestjs/common": "^10.4.15",
|
||||
"@nestjs/config": "^3.3.0",
|
||||
"@nestjs/core": "^10.4.15",
|
||||
|
|
|
|||
9
services/mana-core-auth/src/ai/ai.module.ts
Normal file
9
services/mana-core-auth/src/ai/ai.module.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Module, Global } from '@nestjs/common';
|
||||
import { AiService } from './ai.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [AiService],
|
||||
exports: [AiService],
|
||||
})
|
||||
export class AiModule {}
|
||||
103
services/mana-core-auth/src/ai/ai.service.ts
Normal file
103
services/mana-core-auth/src/ai/ai.service.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { GoogleGenerativeAI } from '@google/generative-ai';
|
||||
|
||||
export interface FeedbackAnalysis {
|
||||
title: string;
|
||||
category: 'bug' | 'feature' | 'improvement' | 'question' | 'other';
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AiService {
|
||||
private readonly logger = new Logger(AiService.name);
|
||||
private genAI: GoogleGenerativeAI | null = null;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
const apiKey = this.configService.get<string>('ai.geminiApiKey');
|
||||
if (apiKey) {
|
||||
this.genAI = new GoogleGenerativeAI(apiKey);
|
||||
} else {
|
||||
this.logger.warn('GOOGLE_GENAI_API_KEY not configured - AI features disabled');
|
||||
}
|
||||
}
|
||||
|
||||
async analyzeFeedback(feedbackText: string): Promise<FeedbackAnalysis> {
|
||||
// Fallback if AI not available
|
||||
if (!this.genAI) {
|
||||
return this.fallbackAnalysis(feedbackText);
|
||||
}
|
||||
|
||||
try {
|
||||
const model = this.genAI.getGenerativeModel({ model: 'gemini-2.0-flash' });
|
||||
|
||||
const prompt = `Analysiere dieses User-Feedback und generiere:
|
||||
1. Einen kurzen, prägnanten deutschen Titel (max 60 Zeichen) der den Kern des Feedbacks zusammenfasst
|
||||
2. Eine passende Kategorie aus: bug, feature, improvement, question, other
|
||||
|
||||
Feedback: "${feedbackText}"
|
||||
|
||||
Antworte NUR mit validem JSON in diesem Format (keine Markdown-Codeblocks, kein anderer Text):
|
||||
{"title": "...", "category": "..."}`;
|
||||
|
||||
const result = await model.generateContent(prompt);
|
||||
const response = result.response.text().trim();
|
||||
|
||||
// Parse JSON response - handle potential markdown code blocks
|
||||
let jsonStr = response;
|
||||
if (response.includes('```')) {
|
||||
const match = response.match(/```(?:json)?\s*([\s\S]*?)```/);
|
||||
if (match) {
|
||||
jsonStr = match[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonStr) as FeedbackAnalysis;
|
||||
|
||||
// Validate category
|
||||
const validCategories = ['bug', 'feature', 'improvement', 'question', 'other'];
|
||||
if (!validCategories.includes(parsed.category)) {
|
||||
parsed.category = 'other';
|
||||
}
|
||||
|
||||
// Ensure title is not too long
|
||||
if (parsed.title.length > 60) {
|
||||
parsed.title = parsed.title.substring(0, 57) + '...';
|
||||
}
|
||||
|
||||
this.logger.debug(`AI analyzed feedback: ${JSON.stringify(parsed)}`);
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
this.logger.error(`AI analysis failed: ${error}`);
|
||||
return this.fallbackAnalysis(feedbackText);
|
||||
}
|
||||
}
|
||||
|
||||
private fallbackAnalysis(feedbackText: string): FeedbackAnalysis {
|
||||
// Simple fallback: use first 60 chars as title, default category
|
||||
const title =
|
||||
feedbackText.length > 60 ? feedbackText.substring(0, 57) + '...' : feedbackText;
|
||||
|
||||
// Simple keyword-based category detection
|
||||
const lowerText = feedbackText.toLowerCase();
|
||||
let category: FeedbackAnalysis['category'] = 'feature';
|
||||
|
||||
if (
|
||||
lowerText.includes('bug') ||
|
||||
lowerText.includes('fehler') ||
|
||||
lowerText.includes('kaputt') ||
|
||||
lowerText.includes('funktioniert nicht')
|
||||
) {
|
||||
category = 'bug';
|
||||
} else if (lowerText.includes('?') || lowerText.includes('frage') || lowerText.includes('wie')) {
|
||||
category = 'question';
|
||||
} else if (
|
||||
lowerText.includes('besser') ||
|
||||
lowerText.includes('verbessern') ||
|
||||
lowerText.includes('optimieren')
|
||||
) {
|
||||
category = 'improvement';
|
||||
}
|
||||
|
||||
return { title, category };
|
||||
}
|
||||
}
|
||||
2
services/mana-core-auth/src/ai/index.ts
Normal file
2
services/mana-core-auth/src/ai/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './ai.module';
|
||||
export * from './ai.service';
|
||||
|
|
@ -5,6 +5,8 @@ import { APP_FILTER } from '@nestjs/core';
|
|||
import configuration from './config/configuration';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { CreditsModule } from './credits/credits.module';
|
||||
import { FeedbackModule } from './feedback/feedback.module';
|
||||
import { AiModule } from './ai/ai.module';
|
||||
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||||
|
||||
@Module({
|
||||
|
|
@ -19,8 +21,10 @@ import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
|||
limit: 100, // 100 requests per minute
|
||||
},
|
||||
]),
|
||||
AiModule,
|
||||
AuthModule,
|
||||
CreditsModule,
|
||||
FeedbackModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -44,4 +44,8 @@ export default () => ({
|
|||
signupBonus: parseInt(process.env.CREDITS_SIGNUP_BONUS || '150', 10),
|
||||
dailyFreeCredits: parseInt(process.env.CREDITS_DAILY_FREE || '5', 10),
|
||||
},
|
||||
|
||||
ai: {
|
||||
geminiApiKey: process.env.GOOGLE_GENAI_API_KEY || '',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
CREATE SCHEMA "feedback";
|
||||
--> statement-breakpoint
|
||||
CREATE TYPE "public"."feedback_category" AS ENUM('bug', 'feature', 'improvement', 'question', 'other');--> statement-breakpoint
|
||||
CREATE TYPE "public"."feedback_status" AS ENUM('submitted', 'under_review', 'planned', 'in_progress', 'completed', 'declined');--> statement-breakpoint
|
||||
CREATE TABLE "feedback"."feedback_votes" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"feedback_id" uuid NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "feedback"."user_feedback" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"app_id" text NOT NULL,
|
||||
"title" text,
|
||||
"feedback_text" text NOT NULL,
|
||||
"category" "feedback_category" DEFAULT 'feature' NOT NULL,
|
||||
"status" "feedback_status" DEFAULT 'submitted' NOT NULL,
|
||||
"is_public" boolean DEFAULT false NOT NULL,
|
||||
"admin_response" text,
|
||||
"vote_count" integer DEFAULT 0 NOT NULL,
|
||||
"device_info" jsonb,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"published_at" timestamp with time zone,
|
||||
"completed_at" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "feedback"."feedback_votes" ADD CONSTRAINT "feedback_votes_feedback_id_user_feedback_id_fk" FOREIGN KEY ("feedback_id") REFERENCES "feedback"."user_feedback"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "feedback"."feedback_votes" ADD CONSTRAINT "feedback_votes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "feedback"."user_feedback" ADD CONSTRAINT "user_feedback_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "feedback_vote_unique" ON "feedback"."feedback_votes" USING btree ("feedback_id","user_id");--> statement-breakpoint
|
||||
CREATE INDEX "feedback_votes_feedback_idx" ON "feedback"."feedback_votes" USING btree ("feedback_id");--> statement-breakpoint
|
||||
CREATE INDEX "feedback_user_idx" ON "feedback"."user_feedback" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "feedback_app_idx" ON "feedback"."user_feedback" USING btree ("app_id");--> statement-breakpoint
|
||||
CREATE INDEX "feedback_public_idx" ON "feedback"."user_feedback" USING btree ("is_public");--> statement-breakpoint
|
||||
CREATE INDEX "feedback_status_idx" ON "feedback"."user_feedback" USING btree ("status");--> statement-breakpoint
|
||||
CREATE INDEX "feedback_created_at_idx" ON "feedback"."user_feedback" USING btree ("created_at");
|
||||
1600
services/mana-core-auth/src/db/migrations/meta/0001_snapshot.json
Normal file
1600
services/mana-core-auth/src/db/migrations/meta/0001_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,13 +1,20 @@
|
|||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1764089133415,
|
||||
"tag": "0000_lush_ironclad",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1764089133415,
|
||||
"tag": "0000_lush_ironclad",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1764448681401,
|
||||
"tag": "0001_zippy_ma_gnuci",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
94
services/mana-core-auth/src/db/schema/feedback.schema.ts
Normal file
94
services/mana-core-auth/src/db/schema/feedback.schema.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import {
|
||||
pgSchema,
|
||||
uuid,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
jsonb,
|
||||
integer,
|
||||
index,
|
||||
pgEnum,
|
||||
uniqueIndex,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { users } from './auth.schema';
|
||||
|
||||
export const feedbackSchema = pgSchema('feedback');
|
||||
|
||||
// Category enum
|
||||
export const feedbackCategoryEnum = pgEnum('feedback_category', [
|
||||
'bug',
|
||||
'feature',
|
||||
'improvement',
|
||||
'question',
|
||||
'other',
|
||||
]);
|
||||
|
||||
// Status enum
|
||||
export const feedbackStatusEnum = pgEnum('feedback_status', [
|
||||
'submitted',
|
||||
'under_review',
|
||||
'planned',
|
||||
'in_progress',
|
||||
'completed',
|
||||
'declined',
|
||||
]);
|
||||
|
||||
// User feedback table
|
||||
export const userFeedback = feedbackSchema.table(
|
||||
'user_feedback',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
appId: text('app_id').notNull(), // 'chat', 'picture', 'zitare', etc.
|
||||
|
||||
// Content
|
||||
title: text('title'),
|
||||
feedbackText: text('feedback_text').notNull(),
|
||||
category: feedbackCategoryEnum('category').default('feature').notNull(),
|
||||
|
||||
// Status & Publishing
|
||||
status: feedbackStatusEnum('status').default('submitted').notNull(),
|
||||
isPublic: boolean('is_public').default(false).notNull(),
|
||||
adminResponse: text('admin_response'),
|
||||
|
||||
// Voting (denormalized for performance)
|
||||
voteCount: integer('vote_count').default(0).notNull(),
|
||||
|
||||
// Metadata
|
||||
deviceInfo: jsonb('device_info'),
|
||||
|
||||
// Timestamps
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
publishedAt: timestamp('published_at', { withTimezone: true }),
|
||||
completedAt: timestamp('completed_at', { withTimezone: true }),
|
||||
},
|
||||
(table) => ({
|
||||
userIdx: index('feedback_user_idx').on(table.userId),
|
||||
appIdx: index('feedback_app_idx').on(table.appId),
|
||||
publicIdx: index('feedback_public_idx').on(table.isPublic),
|
||||
statusIdx: index('feedback_status_idx').on(table.status),
|
||||
createdAtIdx: index('feedback_created_at_idx').on(table.createdAt),
|
||||
})
|
||||
);
|
||||
|
||||
// Feedback votes table
|
||||
export const feedbackVotes = feedbackSchema.table(
|
||||
'feedback_votes',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
feedbackId: uuid('feedback_id')
|
||||
.references(() => userFeedback.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
userId: uuid('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
uniqueVote: uniqueIndex('feedback_vote_unique').on(table.feedbackId, table.userId),
|
||||
feedbackIdx: index('feedback_votes_feedback_idx').on(table.feedbackId),
|
||||
})
|
||||
);
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
export * from './auth.schema';
|
||||
export * from './credits.schema';
|
||||
export * from './feedback.schema';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import { IsString, IsOptional, MaxLength, MinLength, IsEnum, IsObject } from 'class-validator';
|
||||
|
||||
export class CreateFeedbackDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(100)
|
||||
title?: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(10, { message: 'Feedback must be at least 10 characters long' })
|
||||
@MaxLength(2000, { message: 'Feedback must be at most 2000 characters long' })
|
||||
feedbackText: string;
|
||||
|
||||
@IsEnum(['bug', 'feature', 'improvement', 'question', 'other'])
|
||||
@IsOptional()
|
||||
category?: 'bug' | 'feature' | 'improvement' | 'question' | 'other';
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
deviceInfo?: Record<string, unknown>;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { IsString, IsOptional, IsEnum, IsInt, Min, Max } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export class FeedbackQueryDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
appId?: string;
|
||||
|
||||
@IsEnum(['submitted', 'under_review', 'planned', 'in_progress', 'completed', 'declined'])
|
||||
@IsOptional()
|
||||
status?: string;
|
||||
|
||||
@IsEnum(['bug', 'feature', 'improvement', 'question', 'other'])
|
||||
@IsOptional()
|
||||
category?: string;
|
||||
|
||||
@IsEnum(['votes', 'recent'])
|
||||
@IsOptional()
|
||||
sort?: 'votes' | 'recent' = 'votes';
|
||||
|
||||
@Transform(({ value }) => parseInt(value, 10))
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(50)
|
||||
@IsOptional()
|
||||
limit?: number = 20;
|
||||
|
||||
@Transform(({ value }) => parseInt(value, 10))
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
offset?: number = 0;
|
||||
}
|
||||
2
services/mana-core-auth/src/feedback/dto/index.ts
Normal file
2
services/mana-core-auth/src/feedback/dto/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { CreateFeedbackDto } from './create-feedback.dto';
|
||||
export { FeedbackQueryDto } from './feedback-query.dto';
|
||||
54
services/mana-core-auth/src/feedback/feedback.controller.ts
Normal file
54
services/mana-core-auth/src/feedback/feedback.controller.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
Headers,
|
||||
} from '@nestjs/common';
|
||||
import { FeedbackService } from './feedback.service';
|
||||
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
||||
import { CurrentUser, CurrentUserData } from '../common/decorators/current-user.decorator';
|
||||
import { CreateFeedbackDto, FeedbackQueryDto } from './dto';
|
||||
|
||||
@Controller('feedback')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class FeedbackController {
|
||||
constructor(private readonly feedbackService: FeedbackService) {}
|
||||
|
||||
@Post()
|
||||
async createFeedback(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Body() dto: CreateFeedbackDto,
|
||||
@Headers('x-app-id') appIdHeader?: string
|
||||
) {
|
||||
const appId = appIdHeader || 'unknown';
|
||||
return this.feedbackService.createFeedback(user.userId, appId, dto);
|
||||
}
|
||||
|
||||
@Get('public')
|
||||
async getPublicFeedback(@CurrentUser() user: CurrentUserData, @Query() query: FeedbackQueryDto) {
|
||||
return this.feedbackService.getPublicFeedback(user.userId, query);
|
||||
}
|
||||
|
||||
@Get('my')
|
||||
async getMyFeedback(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Query('appId') appId?: string
|
||||
) {
|
||||
return this.feedbackService.getMyFeedback(user.userId, appId);
|
||||
}
|
||||
|
||||
@Post(':id/vote')
|
||||
async vote(@CurrentUser() user: CurrentUserData, @Param('id') feedbackId: string) {
|
||||
return this.feedbackService.vote(user.userId, feedbackId);
|
||||
}
|
||||
|
||||
@Delete(':id/vote')
|
||||
async unvote(@CurrentUser() user: CurrentUserData, @Param('id') feedbackId: string) {
|
||||
return this.feedbackService.unvote(user.userId, feedbackId);
|
||||
}
|
||||
}
|
||||
10
services/mana-core-auth/src/feedback/feedback.module.ts
Normal file
10
services/mana-core-auth/src/feedback/feedback.module.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { FeedbackController } from './feedback.controller';
|
||||
import { FeedbackService } from './feedback.service';
|
||||
|
||||
@Module({
|
||||
controllers: [FeedbackController],
|
||||
providers: [FeedbackService],
|
||||
exports: [FeedbackService],
|
||||
})
|
||||
export class FeedbackModule {}
|
||||
277
services/mana-core-auth/src/feedback/feedback.service.ts
Normal file
277
services/mana-core-auth/src/feedback/feedback.service.ts
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
import { Injectable, NotFoundException, ConflictException, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { eq, and, desc, sql, count } from 'drizzle-orm';
|
||||
import { getDb } from '../db/connection';
|
||||
import { userFeedback, feedbackVotes } from '../db/schema';
|
||||
import { CreateFeedbackDto, FeedbackQueryDto } from './dto';
|
||||
import { AiService } from '../ai/ai.service';
|
||||
|
||||
@Injectable()
|
||||
export class FeedbackService {
|
||||
private readonly logger = new Logger(FeedbackService.name);
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private aiService: AiService
|
||||
) {}
|
||||
|
||||
private getDb() {
|
||||
const databaseUrl = this.configService.get<string>('database.url');
|
||||
return getDb(databaseUrl!);
|
||||
}
|
||||
|
||||
async createFeedback(userId: string, appId: string, dto: CreateFeedbackDto) {
|
||||
const db = this.getDb();
|
||||
|
||||
// Use AI to generate title and category if not provided
|
||||
let title = dto.title;
|
||||
let category = dto.category;
|
||||
|
||||
if (!title || !category) {
|
||||
this.logger.debug('Analyzing feedback with AI...');
|
||||
const analysis = await this.aiService.analyzeFeedback(dto.feedbackText);
|
||||
|
||||
if (!title) {
|
||||
title = analysis.title;
|
||||
}
|
||||
if (!category) {
|
||||
category = analysis.category;
|
||||
}
|
||||
this.logger.debug(`AI generated: title="${title}", category="${category}"`);
|
||||
}
|
||||
|
||||
const [feedback] = await db
|
||||
.insert(userFeedback)
|
||||
.values({
|
||||
userId,
|
||||
appId,
|
||||
title,
|
||||
feedbackText: dto.feedbackText,
|
||||
category: category || 'feature',
|
||||
deviceInfo: dto.deviceInfo,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
feedback: this.mapFeedback(feedback, false),
|
||||
};
|
||||
}
|
||||
|
||||
async getPublicFeedback(userId: string, query: FeedbackQueryDto) {
|
||||
const db = this.getDb();
|
||||
const { appId, status, category, sort = 'votes', limit = 20, offset = 0 } = query;
|
||||
|
||||
// Build conditions
|
||||
const conditions = [eq(userFeedback.isPublic, true)];
|
||||
|
||||
if (appId) {
|
||||
conditions.push(eq(userFeedback.appId, appId));
|
||||
}
|
||||
if (status) {
|
||||
conditions.push(eq(userFeedback.status, status as any));
|
||||
}
|
||||
if (category) {
|
||||
conditions.push(eq(userFeedback.category, category as any));
|
||||
}
|
||||
|
||||
// Get feedback items
|
||||
const feedbackItems = await db
|
||||
.select()
|
||||
.from(userFeedback)
|
||||
.where(and(...conditions))
|
||||
.orderBy(sort === 'votes' ? desc(userFeedback.voteCount) : desc(userFeedback.createdAt))
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
|
||||
// Get total count
|
||||
const [{ total }] = await db
|
||||
.select({ total: count() })
|
||||
.from(userFeedback)
|
||||
.where(and(...conditions));
|
||||
|
||||
// Get user's votes
|
||||
const feedbackIds = feedbackItems.map((f) => f.id);
|
||||
const userVotes =
|
||||
feedbackIds.length > 0
|
||||
? await db
|
||||
.select({ feedbackId: feedbackVotes.feedbackId })
|
||||
.from(feedbackVotes)
|
||||
.where(
|
||||
and(
|
||||
eq(feedbackVotes.userId, userId),
|
||||
sql`${feedbackVotes.feedbackId} = ANY(${feedbackIds})`
|
||||
)
|
||||
)
|
||||
: [];
|
||||
|
||||
const votedFeedbackIds = new Set(userVotes.map((v) => v.feedbackId));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
items: feedbackItems.map((f) => this.mapFeedback(f, votedFeedbackIds.has(f.id))),
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
async getMyFeedback(userId: string, appId?: string) {
|
||||
const db = this.getDb();
|
||||
|
||||
const conditions = [eq(userFeedback.userId, userId)];
|
||||
if (appId) {
|
||||
conditions.push(eq(userFeedback.appId, appId));
|
||||
}
|
||||
|
||||
const feedbackItems = await db
|
||||
.select()
|
||||
.from(userFeedback)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(userFeedback.createdAt));
|
||||
|
||||
// Get user's votes on their own feedback (for consistency)
|
||||
const feedbackIds = feedbackItems.map((f) => f.id);
|
||||
const userVotes =
|
||||
feedbackIds.length > 0
|
||||
? await db
|
||||
.select({ feedbackId: feedbackVotes.feedbackId })
|
||||
.from(feedbackVotes)
|
||||
.where(
|
||||
and(
|
||||
eq(feedbackVotes.userId, userId),
|
||||
sql`${feedbackVotes.feedbackId} = ANY(${feedbackIds})`
|
||||
)
|
||||
)
|
||||
: [];
|
||||
|
||||
const votedFeedbackIds = new Set(userVotes.map((v) => v.feedbackId));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
items: feedbackItems.map((f) => this.mapFeedback(f, votedFeedbackIds.has(f.id))),
|
||||
total: feedbackItems.length,
|
||||
};
|
||||
}
|
||||
|
||||
async vote(userId: string, feedbackId: string) {
|
||||
const db = this.getDb();
|
||||
|
||||
// Check if feedback exists and is public
|
||||
const [feedback] = await db
|
||||
.select()
|
||||
.from(userFeedback)
|
||||
.where(eq(userFeedback.id, feedbackId))
|
||||
.limit(1);
|
||||
|
||||
if (!feedback) {
|
||||
throw new NotFoundException('Feedback not found');
|
||||
}
|
||||
|
||||
if (!feedback.isPublic) {
|
||||
throw new NotFoundException('Feedback not found or not public');
|
||||
}
|
||||
|
||||
// Check if user already voted
|
||||
const [existingVote] = await db
|
||||
.select()
|
||||
.from(feedbackVotes)
|
||||
.where(and(eq(feedbackVotes.feedbackId, feedbackId), eq(feedbackVotes.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (existingVote) {
|
||||
throw new ConflictException('Already voted');
|
||||
}
|
||||
|
||||
// Add vote
|
||||
await db.insert(feedbackVotes).values({
|
||||
feedbackId,
|
||||
userId,
|
||||
});
|
||||
|
||||
// Increment vote count
|
||||
const [updated] = await db
|
||||
.update(userFeedback)
|
||||
.set({
|
||||
voteCount: sql`${userFeedback.voteCount} + 1`,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(userFeedback.id, feedbackId))
|
||||
.returning();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
newVoteCount: updated.voteCount,
|
||||
userHasVoted: true,
|
||||
};
|
||||
}
|
||||
|
||||
async unvote(userId: string, feedbackId: string) {
|
||||
const db = this.getDb();
|
||||
|
||||
// Check if feedback exists
|
||||
const [feedback] = await db
|
||||
.select()
|
||||
.from(userFeedback)
|
||||
.where(eq(userFeedback.id, feedbackId))
|
||||
.limit(1);
|
||||
|
||||
if (!feedback) {
|
||||
throw new NotFoundException('Feedback not found');
|
||||
}
|
||||
|
||||
// Check if user has voted
|
||||
const [existingVote] = await db
|
||||
.select()
|
||||
.from(feedbackVotes)
|
||||
.where(and(eq(feedbackVotes.feedbackId, feedbackId), eq(feedbackVotes.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (!existingVote) {
|
||||
throw new NotFoundException('Vote not found');
|
||||
}
|
||||
|
||||
// Remove vote
|
||||
await db
|
||||
.delete(feedbackVotes)
|
||||
.where(and(eq(feedbackVotes.feedbackId, feedbackId), eq(feedbackVotes.userId, userId)));
|
||||
|
||||
// Decrement vote count
|
||||
const [updated] = await db
|
||||
.update(userFeedback)
|
||||
.set({
|
||||
voteCount: sql`GREATEST(${userFeedback.voteCount} - 1, 0)`,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(userFeedback.id, feedbackId))
|
||||
.returning();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
newVoteCount: updated.voteCount,
|
||||
userHasVoted: false,
|
||||
};
|
||||
}
|
||||
|
||||
private mapFeedback(
|
||||
feedback: typeof userFeedback.$inferSelect,
|
||||
userHasVoted: boolean
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
id: feedback.id,
|
||||
userId: feedback.userId,
|
||||
appId: feedback.appId,
|
||||
title: feedback.title,
|
||||
feedbackText: feedback.feedbackText,
|
||||
category: feedback.category,
|
||||
status: feedback.status,
|
||||
isPublic: feedback.isPublic,
|
||||
adminResponse: feedback.adminResponse,
|
||||
voteCount: feedback.voteCount,
|
||||
userHasVoted,
|
||||
deviceInfo: feedback.deviceInfo,
|
||||
createdAt: feedback.createdAt.toISOString(),
|
||||
updatedAt: feedback.updatedAt.toISOString(),
|
||||
publishedAt: feedback.publishedAt?.toISOString(),
|
||||
completedAt: feedback.completedAt?.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ async function bootstrap() {
|
|||
origin: corsOrigins,
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'X-App-Id'],
|
||||
});
|
||||
|
||||
// Global validation pipe
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue