mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 06:26:41 +02:00
fix(ci): build shared packages before tests and fix formatting
- Add build:packages step to all test.yml jobs (fixes @manacore/shared-nestjs-auth not found) - Handle missing coverage artifacts gracefully in test-coverage.yml - Update .prettierignore to exclude apps-archived/ and problematic files - Format all source files to pass CI checks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5282f5545b
commit
0ebfde0851
163 changed files with 15247 additions and 14677 deletions
|
|
@ -114,11 +114,11 @@ EXPO_PUBLIC_BACKEND_URL=http://localhost:3001
|
|||
|
||||
## AI Models Available
|
||||
|
||||
| Model ID | Name | Description | Default |
|
||||
| ------------------------------------ | ----------------- | ------------------------------ | ------- |
|
||||
| 550e8400-e29b-41d4-a716-446655440101 | Gemini 2.5 Flash | Fast, efficient responses | Yes |
|
||||
| 550e8400-e29b-41d4-a716-446655440102 | Gemini 2.0 Flash-Lite | Ultra-lightweight model | No |
|
||||
| 550e8400-e29b-41d4-a716-446655440103 | Gemini 2.5 Pro | Most capable model | No |
|
||||
| Model ID | Name | Description | Default |
|
||||
| ------------------------------------ | --------------------- | ------------------------- | ------- |
|
||||
| 550e8400-e29b-41d4-a716-446655440101 | Gemini 2.5 Flash | Fast, efficient responses | Yes |
|
||||
| 550e8400-e29b-41d4-a716-446655440102 | Gemini 2.0 Flash-Lite | Ultra-lightweight model | No |
|
||||
| 550e8400-e29b-41d4-a716-446655440103 | Gemini 2.5 Pro | Most capable model | No |
|
||||
|
||||
## Important Notes
|
||||
|
||||
|
|
|
|||
|
|
@ -134,8 +134,7 @@ export default function SettingsScreen() {
|
|||
style={[
|
||||
styles.themeOption,
|
||||
{
|
||||
borderColor:
|
||||
selectedTheme === theme.id ? colors.primary : colors.border,
|
||||
borderColor: selectedTheme === theme.id ? colors.primary : colors.border,
|
||||
backgroundColor:
|
||||
selectedTheme === theme.id ? colors.primary + '10' : 'transparent',
|
||||
},
|
||||
|
|
@ -272,9 +271,7 @@ export default function SettingsScreen() {
|
|||
style={styles.settingIcon}
|
||||
/>
|
||||
<View>
|
||||
<Text style={[styles.settingLabel, { color: colors.text }]}>
|
||||
Passwort ändern
|
||||
</Text>
|
||||
<Text style={[styles.settingLabel, { color: colors.text }]}>Passwort ändern</Text>
|
||||
<Text style={[styles.settingDescription, { color: colors.text + '70' }]}>
|
||||
Aktualisiere dein Passwort regelmäßig
|
||||
</Text>
|
||||
|
|
@ -285,12 +282,7 @@ export default function SettingsScreen() {
|
|||
|
||||
<TouchableOpacity style={styles.actionRowLast} onPress={handleDeleteChatHistory}>
|
||||
<View style={styles.settingInfo}>
|
||||
<Ionicons
|
||||
name="trash-outline"
|
||||
size={22}
|
||||
color="#FF3B30"
|
||||
style={styles.settingIcon}
|
||||
/>
|
||||
<Ionicons name="trash-outline" size={22} color="#FF3B30" style={styles.settingIcon} />
|
||||
<View>
|
||||
<Text style={[styles.settingLabel, { color: '#FF3B30' }]}>
|
||||
Chat-Verlauf löschen
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@
|
|||
{#each toasts as toast (toast.id)}
|
||||
{@const Icon = icons[toast.type]}
|
||||
<div
|
||||
class="flex items-start gap-3 px-4 py-3 rounded-xl shadow-lg backdrop-blur-xl border border-white/20 animate-slide-in {colors[toast.type]}"
|
||||
class="flex items-start gap-3 px-4 py-3 rounded-xl shadow-lg backdrop-blur-xl border border-white/20 animate-slide-in {colors[
|
||||
toast.type
|
||||
]}"
|
||||
role="alert"
|
||||
>
|
||||
<Icon size={20} weight="fill" class="flex-shrink-0 mt-0.5" />
|
||||
|
|
|
|||
|
|
@ -4,7 +4,15 @@
|
|||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { isSidebarMode, isNavCollapsed } from '$lib/stores/navigation';
|
||||
import { MagnifyingGlass, X, Plus, ChatCircle, Archive, Trash, PushPin } from '@manacore/shared-icons';
|
||||
import {
|
||||
MagnifyingGlass,
|
||||
X,
|
||||
Plus,
|
||||
ChatCircle,
|
||||
Archive,
|
||||
Trash,
|
||||
PushPin,
|
||||
} from '@manacore/shared-icons';
|
||||
import { ConfirmationModal } from '@manacore/shared-ui';
|
||||
import { goto } from '$app/navigation';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
|
@ -72,7 +80,7 @@
|
|||
yesterday: 'Gestern',
|
||||
thisWeek: 'Diese Woche',
|
||||
thisMonth: 'Dieser Monat',
|
||||
older: 'Älter'
|
||||
older: 'Älter',
|
||||
};
|
||||
|
||||
function getDateSection(dateString: string): DateSection {
|
||||
|
|
@ -117,7 +125,7 @@
|
|||
yesterday: [],
|
||||
thisWeek: [],
|
||||
thisMonth: [],
|
||||
older: []
|
||||
older: [],
|
||||
};
|
||||
|
||||
for (const conv of unpinnedConversations) {
|
||||
|
|
@ -331,7 +339,9 @@
|
|||
<!-- Pinned Section -->
|
||||
{#if pinnedConversations.length > 0}
|
||||
<div class="mb-5">
|
||||
<h4 class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2 flex items-center gap-1.5">
|
||||
<h4
|
||||
class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2 flex items-center gap-1.5"
|
||||
>
|
||||
<PushPin size={12} weight="fill" class="text-primary" />
|
||||
Angepinnt
|
||||
</h4>
|
||||
|
|
@ -339,17 +349,11 @@
|
|||
<a
|
||||
href="/chat/{conv.id}"
|
||||
class="group block w-full rounded-xl bg-white/60 dark:bg-white/5 backdrop-blur-sm border border-black/10 dark:border-white/20 p-4 text-left transition-all mb-3 hover:shadow-md hover:bg-white/80 dark:hover:bg-white/10
|
||||
{isActive(conv.id)
|
||||
? 'bg-white/90 dark:bg-white/15 shadow-md border-primary/30'
|
||||
: ''}"
|
||||
{isActive(conv.id) ? 'bg-white/90 dark:bg-white/15 shadow-md border-primary/30' : ''}"
|
||||
>
|
||||
<!-- Title Row -->
|
||||
<div class="mb-1.5 flex items-center gap-2">
|
||||
<PushPin
|
||||
size={16}
|
||||
weight="fill"
|
||||
class="flex-shrink-0 text-primary"
|
||||
/>
|
||||
<PushPin size={16} weight="fill" class="flex-shrink-0 text-primary" />
|
||||
<h3 class="text-sm font-semibold line-clamp-1 text-foreground flex-1">
|
||||
{conv.title || 'Neue Konversation'}
|
||||
</h3>
|
||||
|
|
@ -367,14 +371,14 @@
|
|||
</span>
|
||||
<div class="flex items-center gap-1">
|
||||
{#if conv.documentMode}
|
||||
<span
|
||||
class="text-xs text-primary bg-primary/10 px-2 py-0.5 rounded-full"
|
||||
>
|
||||
<span class="text-xs text-primary bg-primary/10 px-2 py-0.5 rounded-full">
|
||||
Dokument
|
||||
</span>
|
||||
{/if}
|
||||
<!-- Action Buttons (visible on hover) -->
|
||||
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div
|
||||
class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<button
|
||||
onclick={(e) => handleTogglePin(e, conv.id, true)}
|
||||
class="p-1.5 text-primary hover:text-primary hover:bg-primary/10 rounded-lg transition-colors"
|
||||
|
|
@ -409,16 +413,16 @@
|
|||
{@const convs = groupedConversations()[section]}
|
||||
{#if convs.length > 0}
|
||||
<div class="mb-5">
|
||||
<h4 class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
|
||||
<h4
|
||||
class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2"
|
||||
>
|
||||
{sectionLabels[section]}
|
||||
</h4>
|
||||
{#each convs as conv (conv.id)}
|
||||
<a
|
||||
href="/chat/{conv.id}"
|
||||
class="group block w-full rounded-xl bg-white/60 dark:bg-white/5 backdrop-blur-sm border border-black/10 dark:border-white/20 p-4 text-left transition-all mb-3 hover:shadow-md hover:bg-white/80 dark:hover:bg-white/10
|
||||
{isActive(conv.id)
|
||||
? 'bg-white/90 dark:bg-white/15 shadow-md border-primary/30'
|
||||
: ''}"
|
||||
{isActive(conv.id) ? 'bg-white/90 dark:bg-white/15 shadow-md border-primary/30' : ''}"
|
||||
>
|
||||
<!-- Title Row -->
|
||||
<div class="mb-1.5 flex items-center gap-2">
|
||||
|
|
@ -446,14 +450,14 @@
|
|||
</span>
|
||||
<div class="flex items-center gap-1">
|
||||
{#if conv.documentMode}
|
||||
<span
|
||||
class="text-xs text-primary bg-primary/10 px-2 py-0.5 rounded-full"
|
||||
>
|
||||
<span class="text-xs text-primary bg-primary/10 px-2 py-0.5 rounded-full">
|
||||
Dokument
|
||||
</span>
|
||||
{/if}
|
||||
<!-- Action Buttons (visible on hover) -->
|
||||
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div
|
||||
class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<button
|
||||
onclick={(e) => handleTogglePin(e, conv.id, false)}
|
||||
class="p-1.5 text-muted-foreground hover:text-primary hover:bg-primary/10 rounded-lg transition-colors"
|
||||
|
|
|
|||
|
|
@ -138,9 +138,7 @@
|
|||
<a
|
||||
href="/chat/{conv.id}"
|
||||
class="block px-3 py-2 mx-2 rounded-lg transition-colors
|
||||
{isActive
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'hover:bg-muted text-foreground'}"
|
||||
{isActive ? 'bg-primary/10 text-primary' : 'hover:bg-muted text-foreground'}"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-sm font-medium truncate pr-6">
|
||||
|
|
|
|||
|
|
@ -52,9 +52,7 @@
|
|||
{space.name}
|
||||
</h3>
|
||||
{#if isOwner}
|
||||
<span
|
||||
class="px-2 py-0.5 text-xs font-medium bg-primary/10 text-primary rounded"
|
||||
>
|
||||
<span class="px-2 py-0.5 text-xs font-medium bg-primary/10 text-primary rounded">
|
||||
Besitzer
|
||||
</span>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -50,9 +50,7 @@
|
|||
>
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-foreground mb-1">
|
||||
Name *
|
||||
</label>
|
||||
<label for="name" class="block text-sm font-medium text-foreground mb-1"> Name * </label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
|
|
@ -71,10 +69,7 @@
|
|||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label
|
||||
for="description"
|
||||
class="block text-sm font-medium text-foreground mb-1"
|
||||
>
|
||||
<label for="description" class="block text-sm font-medium text-foreground mb-1">
|
||||
Beschreibung (optional)
|
||||
</label>
|
||||
<textarea
|
||||
|
|
|
|||
|
|
@ -20,9 +20,7 @@
|
|||
|
||||
<div
|
||||
class="group relative flex rounded-xl overflow-hidden bg-surface shadow-sm hover:shadow-md transition-all
|
||||
{template.isDefault
|
||||
? 'ring-2 ring-primary'
|
||||
: 'border border-border'}"
|
||||
{template.isDefault ? 'ring-2 ring-primary' : 'border border-border'}"
|
||||
>
|
||||
<!-- Color Indicator -->
|
||||
<div class="w-2 flex-shrink-0" style="background-color: {template.color}"></div>
|
||||
|
|
@ -36,7 +34,9 @@
|
|||
{template.name}
|
||||
</h3>
|
||||
{#if template.isDefault}
|
||||
<span class="px-2 py-0.5 text-xs font-medium bg-primary text-primary-foreground rounded">
|
||||
<span
|
||||
class="px-2 py-0.5 text-xs font-medium bg-primary text-primary-foreground rounded"
|
||||
>
|
||||
Standard
|
||||
</span>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -90,9 +90,7 @@
|
|||
>
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-foreground mb-1">
|
||||
Name *
|
||||
</label>
|
||||
<label for="name" class="block text-sm font-medium text-foreground mb-1"> Name * </label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
|
|
@ -111,10 +109,7 @@
|
|||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label
|
||||
for="description"
|
||||
class="block text-sm font-medium text-foreground mb-1"
|
||||
>
|
||||
<label for="description" class="block text-sm font-medium text-foreground mb-1">
|
||||
Beschreibung (optional)
|
||||
</label>
|
||||
<textarea
|
||||
|
|
@ -131,10 +126,7 @@
|
|||
|
||||
<!-- System Prompt -->
|
||||
<div>
|
||||
<label
|
||||
for="systemPrompt"
|
||||
class="block text-sm font-medium text-foreground mb-1"
|
||||
>
|
||||
<label for="systemPrompt" class="block text-sm font-medium text-foreground mb-1">
|
||||
System-Prompt *
|
||||
</label>
|
||||
<textarea
|
||||
|
|
@ -158,10 +150,7 @@
|
|||
|
||||
<!-- Initial Question -->
|
||||
<div>
|
||||
<label
|
||||
for="initialQuestion"
|
||||
class="block text-sm font-medium text-foreground mb-1"
|
||||
>
|
||||
<label for="initialQuestion" class="block text-sm font-medium text-foreground mb-1">
|
||||
Beispielfrage (optional)
|
||||
</label>
|
||||
<textarea
|
||||
|
|
@ -236,9 +225,7 @@
|
|||
type="button"
|
||||
onclick={() => (documentMode = !documentMode)}
|
||||
class="w-full flex items-center justify-between p-4 border rounded-lg transition-colors
|
||||
{documentMode
|
||||
? 'border-primary bg-primary/10'
|
||||
: 'border-border bg-muted'}"
|
||||
{documentMode ? 'border-primary bg-primary/10' : 'border-border bg-muted'}"
|
||||
>
|
||||
<div class="text-left">
|
||||
<p class="font-medium text-foreground">Dokumentmodus aktivieren</p>
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ export const conversationsStore = {
|
|||
try {
|
||||
conversations = await conversationService.getConversations(spaceId);
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : 'Konversationen konnten nicht geladen werden';
|
||||
const message =
|
||||
e instanceof Error ? e.message : 'Konversationen konnten nicht geladen werden';
|
||||
error = message;
|
||||
toastStore.error(message);
|
||||
conversations = [];
|
||||
|
|
@ -67,7 +68,8 @@ export const conversationsStore = {
|
|||
try {
|
||||
archivedConversations = await conversationService.getArchivedConversations();
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : 'Archivierte Konversationen konnten nicht geladen werden';
|
||||
const message =
|
||||
e instanceof Error ? e.message : 'Archivierte Konversationen konnten nicht geladen werden';
|
||||
error = message;
|
||||
toastStore.error(message);
|
||||
archivedConversations = [];
|
||||
|
|
@ -131,7 +133,10 @@ export const conversationsStore = {
|
|||
const conversation = archivedConversations.find((c) => c.id === conversationId);
|
||||
if (conversation) {
|
||||
archivedConversations = archivedConversations.filter((c) => c.id !== conversationId);
|
||||
conversations = sortConversations([{ ...conversation, isArchived: false }, ...conversations]);
|
||||
conversations = sortConversations([
|
||||
{ ...conversation, isArchived: false },
|
||||
...conversations,
|
||||
]);
|
||||
}
|
||||
toastStore.success('Konversation wiederhergestellt');
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -93,7 +93,10 @@ export const toastStore = {
|
|||
* Helper function for API error handling
|
||||
* Use this in services/stores to show user-friendly error messages
|
||||
*/
|
||||
export function handleApiError(error: unknown, fallbackMessage: string = 'Ein Fehler ist aufgetreten'): string {
|
||||
export function handleApiError(
|
||||
error: unknown,
|
||||
fallbackMessage: string = 'Ein Fehler ist aufgetreten'
|
||||
): string {
|
||||
const message = error instanceof Error ? error.message : fallbackMessage;
|
||||
toastStore.error(message);
|
||||
return message;
|
||||
|
|
|
|||
|
|
@ -81,9 +81,7 @@
|
|||
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-foreground mb-1">
|
||||
Keine archivierten Konversationen
|
||||
</h3>
|
||||
<h3 class="text-lg font-medium text-foreground mb-1">Keine archivierten Konversationen</h3>
|
||||
<p class="text-muted-foreground">Archivierte Gespräche erscheinen hier.</p>
|
||||
</div>
|
||||
{:else}
|
||||
|
|
@ -128,9 +126,7 @@
|
|||
</button>
|
||||
|
||||
<!-- Actions -->
|
||||
<div
|
||||
class="flex justify-end gap-2 px-4 py-2 border-t border-border bg-muted/50"
|
||||
>
|
||||
<div class="flex justify-end gap-2 px-4 py-2 border-t border-border bg-muted/50">
|
||||
<button
|
||||
onclick={() => handleUnarchive(conv.id)}
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 text-sm text-muted-foreground
|
||||
|
|
|
|||
|
|
@ -148,7 +148,9 @@
|
|||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<h3 class="text-2xl font-semibold text-foreground mb-3">Worüber möchtest du reden?</h3>
|
||||
<h3 class="text-2xl font-semibold text-foreground mb-3">
|
||||
Worüber möchtest du reden?
|
||||
</h3>
|
||||
<p class="text-muted-foreground mb-8">
|
||||
Stelle eine Frage, bitte um Hilfe bei einem Projekt oder starte einfach eine
|
||||
Unterhaltung.
|
||||
|
|
|
|||
|
|
@ -10,12 +10,7 @@
|
|||
import ChatInput from '$lib/components/chat/ChatInput.svelte';
|
||||
import ChatLayout from '$lib/components/chat/ChatLayout.svelte';
|
||||
import type { Conversation, Message, AIModel, Document } from '@chat/types';
|
||||
import {
|
||||
FileText,
|
||||
ClockCounterClockwise,
|
||||
X,
|
||||
FloppyDisk,
|
||||
} from '@manacore/shared-icons';
|
||||
import { FileText, ClockCounterClockwise, X, FloppyDisk } from '@manacore/shared-icons';
|
||||
|
||||
let conversation = $state<Conversation | null>(null);
|
||||
let messages = $state<Message[]>([]);
|
||||
|
|
@ -247,9 +242,7 @@
|
|||
<FileText size={18} weight="bold" class="text-primary" />
|
||||
<span class="font-medium text-foreground">Dokument</span>
|
||||
{#if document}
|
||||
<span
|
||||
class="text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded-lg"
|
||||
>
|
||||
<span class="text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded-lg">
|
||||
v{document.version}
|
||||
</span>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -127,9 +127,7 @@
|
|||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-foreground mb-1">
|
||||
Keine Dokumente gefunden
|
||||
</h3>
|
||||
<h3 class="text-lg font-medium text-foreground mb-1">Keine Dokumente gefunden</h3>
|
||||
<p class="text-muted-foreground max-w-sm mx-auto">
|
||||
Erstelle ein neues Dokument in einer Konversation mit aktiviertem Dokumentmodus.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,4 @@
|
|||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
</script>
|
||||
|
||||
<FeedbackPage
|
||||
{feedbackService}
|
||||
appName="ManaChat"
|
||||
currentUserId={authStore.user?.id}
|
||||
/>
|
||||
<FeedbackPage {feedbackService} appName="ManaChat" currentUserId={authStore.user?.id} />
|
||||
|
|
|
|||
|
|
@ -31,7 +31,11 @@
|
|||
}
|
||||
|
||||
function handleDeleteAccount() {
|
||||
if (confirm('Bist du sicher, dass du dein Konto löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.')) {
|
||||
if (
|
||||
confirm(
|
||||
'Bist du sicher, dass du dein Konto löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.'
|
||||
)
|
||||
) {
|
||||
alert('Konto löschen wird noch implementiert.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,9 +136,7 @@
|
|||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-foreground mb-1">
|
||||
Keine Spaces gefunden
|
||||
</h3>
|
||||
<h3 class="text-lg font-medium text-foreground mb-1">Keine Spaces gefunden</h3>
|
||||
<p class="text-muted-foreground mb-4">
|
||||
Erstelle einen neuen Space oder frage nach einer Einladung
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -98,17 +98,10 @@
|
|||
{:else if space}
|
||||
<div class="min-h-[calc(100vh-4rem)] bg-background py-8">
|
||||
<div class="max-w-4xl mx-auto px-4">
|
||||
<PageHeader
|
||||
title={space.name}
|
||||
description={space.description}
|
||||
backHref="/spaces"
|
||||
size="lg"
|
||||
/>
|
||||
<PageHeader title={space.name} description={space.description} backHref="/spaces" size="lg" />
|
||||
|
||||
<!-- New Chat Section -->
|
||||
<div
|
||||
class="mb-8 p-4 bg-surface rounded-xl border border-border"
|
||||
>
|
||||
<div class="mb-8 p-4 bg-surface rounded-xl border border-border">
|
||||
<h2 class="text-lg font-semibold text-foreground mb-3">Neuen Chat starten</h2>
|
||||
<div class="flex items-center gap-3">
|
||||
<select
|
||||
|
|
@ -133,14 +126,10 @@
|
|||
|
||||
<!-- Conversations List -->
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-foreground mb-4">
|
||||
Konversationen in diesem Space
|
||||
</h2>
|
||||
<h2 class="text-lg font-semibold text-foreground mb-4">Konversationen in diesem Space</h2>
|
||||
|
||||
{#if conversations.length === 0}
|
||||
<div
|
||||
class="text-center py-12 bg-surface rounded-xl border border-border"
|
||||
>
|
||||
<div class="text-center py-12 bg-surface rounded-xl border border-border">
|
||||
<svg
|
||||
class="w-12 h-12 text-muted-foreground mx-auto mb-3"
|
||||
fill="none"
|
||||
|
|
@ -154,9 +143,7 @@
|
|||
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>
|
||||
<p class="text-muted-foreground">
|
||||
Noch keine Konversationen in diesem Space.
|
||||
</p>
|
||||
<p class="text-muted-foreground">Noch keine Konversationen in diesem Space.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
|
|
|
|||
|
|
@ -154,12 +154,8 @@
|
|||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-foreground mb-1">
|
||||
Keine Vorlagen vorhanden
|
||||
</h3>
|
||||
<p class="text-muted-foreground mb-4">
|
||||
Erstelle deine erste Vorlage, um loszulegen
|
||||
</p>
|
||||
<h3 class="text-lg font-medium text-foreground mb-1">Keine Vorlagen vorhanden</h3>
|
||||
<p class="text-muted-foreground mb-4">Erstelle deine erste Vorlage, um loszulegen</p>
|
||||
<button
|
||||
onclick={handleCreateNew}
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg font-medium
|
||||
|
|
|
|||
|
|
@ -83,9 +83,7 @@ export interface Template {
|
|||
}
|
||||
|
||||
export type TemplateCreate = Omit<Template, 'id' | 'createdAt' | 'updatedAt'>;
|
||||
export type TemplateUpdate = Partial<
|
||||
Omit<Template, 'id' | 'userId' | 'createdAt' | 'updatedAt'>
|
||||
>;
|
||||
export type TemplateUpdate = Partial<Omit<Template, 'id' | 'userId' | 'createdAt' | 'updatedAt'>>;
|
||||
|
||||
// Space Types
|
||||
export interface Space {
|
||||
|
|
|
|||
|
|
@ -92,20 +92,20 @@
|
|||
|
||||
function handleBuyPackage(pkg: CreditPackage) {
|
||||
// TODO: Integrate with Stripe
|
||||
alert(`Paket "${pkg.name}" kaufen\n\n${formatCredits(pkg.credits)} Credits für ${formatPrice(pkg.priceEuroCents)}\n\nStripe-Integration kommt bald!`);
|
||||
alert(
|
||||
`Paket "${pkg.name}" kaufen\n\n${formatCredits(pkg.credits)} Credits für ${formatPrice(pkg.priceEuroCents)}\n\nStripe-Integration kommt bald!`
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<PageHeader
|
||||
title="Credits"
|
||||
description="Verwalte deine Mana Credits"
|
||||
size="lg"
|
||||
/>
|
||||
<PageHeader title="Credits" description="Verwalte deine Mana Credits" size="lg" />
|
||||
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<div class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
||||
<div
|
||||
class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"
|
||||
></div>
|
||||
</div>
|
||||
{:else if error}
|
||||
<Card>
|
||||
|
|
@ -195,7 +195,9 @@
|
|||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each transactions.slice(0, 5) as tx}
|
||||
<div class="flex items-center justify-between py-2 border-b border-border last:border-0">
|
||||
<div
|
||||
class="flex items-center justify-between py-2 border-b border-border last:border-0"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xl">{getTransactionIcon(tx.type)}</span>
|
||||
<div>
|
||||
|
|
@ -232,7 +234,9 @@
|
|||
>
|
||||
<div class="text-left">
|
||||
<p class="font-medium">{pkg.name}</p>
|
||||
<p class="text-sm text-muted-foreground">{formatCredits(pkg.credits)} Credits</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{formatCredits(pkg.credits)} Credits
|
||||
</p>
|
||||
</div>
|
||||
<span class="font-semibold text-primary">{formatPrice(pkg.priceEuroCents)}</span>
|
||||
</button>
|
||||
|
|
@ -247,7 +251,6 @@
|
|||
{/if}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{:else if activeTab === 'transactions'}
|
||||
<Card>
|
||||
<h3 class="text-lg font-semibold mb-4">Transaktionsverlauf</h3>
|
||||
|
|
@ -288,7 +291,6 @@
|
|||
</div>
|
||||
{/if}
|
||||
</Card>
|
||||
|
||||
{:else if activeTab === 'packages'}
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each packages as pkg}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@
|
|||
},
|
||||
{
|
||||
name: 'Gratis-Credits heute',
|
||||
value: creditBalance ? `${creditBalance.freeCreditsRemaining}/${creditBalance.dailyFreeCredits}` : '...',
|
||||
value: creditBalance
|
||||
? `${creditBalance.freeCreditsRemaining}/${creditBalance.dailyFreeCredits}`
|
||||
: '...',
|
||||
icon: '🎁',
|
||||
href: '/credits',
|
||||
},
|
||||
|
|
@ -56,10 +58,14 @@
|
|||
|
||||
function getTransactionIcon(type: string): string {
|
||||
switch (type) {
|
||||
case 'purchase': return '💳';
|
||||
case 'usage': return '⚡';
|
||||
case 'bonus': return '🎁';
|
||||
default: return '📝';
|
||||
case 'purchase':
|
||||
return '💳';
|
||||
case 'usage':
|
||||
return '⚡';
|
||||
case 'bonus':
|
||||
return '🎁';
|
||||
default:
|
||||
return '📝';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -99,10 +105,7 @@
|
|||
<Card>
|
||||
<h2 class="mb-4 text-lg font-semibold">Schnellzugriff</h2>
|
||||
<div class="space-y-2">
|
||||
<a
|
||||
href="/credits"
|
||||
class="block rounded-lg p-3 hover:bg-surface-hover transition-colors"
|
||||
>
|
||||
<a href="/credits" class="block rounded-lg p-3 hover:bg-surface-hover transition-colors">
|
||||
<div class="flex items-center">
|
||||
<span class="text-2xl">💰</span>
|
||||
<div class="ml-3">
|
||||
|
|
@ -111,10 +114,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="/feedback"
|
||||
class="block rounded-lg p-3 hover:bg-surface-hover transition-colors"
|
||||
>
|
||||
<a href="/feedback" class="block rounded-lg p-3 hover:bg-surface-hover transition-colors">
|
||||
<div class="flex items-center">
|
||||
<span class="text-2xl">💬</span>
|
||||
<div class="ml-3">
|
||||
|
|
@ -123,10 +123,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="/profile"
|
||||
class="block rounded-lg p-3 hover:bg-surface-hover transition-colors"
|
||||
>
|
||||
<a href="/profile" class="block rounded-lg p-3 hover:bg-surface-hover transition-colors">
|
||||
<div class="flex items-center">
|
||||
<span class="text-2xl">👤</span>
|
||||
<div class="ml-3">
|
||||
|
|
@ -163,7 +160,9 @@
|
|||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each recentTransactions as tx}
|
||||
<div class="flex items-center justify-between py-2 border-b border-border last:border-0">
|
||||
<div
|
||||
class="flex items-center justify-between py-2 border-b border-border last:border-0"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xl">{getTransactionIcon(tx.type)}</span>
|
||||
<div>
|
||||
|
|
@ -173,7 +172,11 @@
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="font-semibold {tx.amount > 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}">
|
||||
<span
|
||||
class="font-semibold {tx.amount > 0
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
: 'text-red-600 dark:text-red-400'}"
|
||||
>
|
||||
{tx.amount > 0 ? '+' : ''}{formatCredits(tx.amount)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,4 @@
|
|||
import { authStore } from '$lib/stores/authStore.svelte';
|
||||
</script>
|
||||
|
||||
<FeedbackPage
|
||||
{feedbackService}
|
||||
appName="ManaCore"
|
||||
currentUserId={authStore.user?.id}
|
||||
/>
|
||||
<FeedbackPage {feedbackService} appName="ManaCore" currentUserId={authStore.user?.id} />
|
||||
|
|
|
|||
|
|
@ -31,7 +31,12 @@
|
|||
{#snippet actions()}
|
||||
<Button variant="primary">
|
||||
<svg class="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Create Organization
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,9 @@
|
|||
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<div class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
||||
<div
|
||||
class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"
|
||||
></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-6">
|
||||
|
|
@ -65,7 +67,9 @@
|
|||
<Card>
|
||||
<div class="p-6">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-primary"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
|
|
@ -82,13 +86,17 @@
|
|||
</div>
|
||||
|
||||
{#if profileSuccess}
|
||||
<div class="mb-4 rounded-lg bg-green-50 p-4 text-sm text-green-800 dark:bg-green-900/20 dark:text-green-400">
|
||||
<div
|
||||
class="mb-4 rounded-lg bg-green-50 p-4 text-sm text-green-800 dark:bg-green-900/20 dark:text-green-400"
|
||||
>
|
||||
Profil erfolgreich aktualisiert!
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if profileError}
|
||||
<div class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400">
|
||||
<div
|
||||
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
|
||||
>
|
||||
{profileError}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -109,30 +117,16 @@
|
|||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="firstName" class="mb-2 block text-sm font-medium">Vorname</label>
|
||||
<Input
|
||||
type="text"
|
||||
id="firstName"
|
||||
bind:value={firstName}
|
||||
placeholder="Max"
|
||||
/>
|
||||
<Input type="text" id="firstName" bind:value={firstName} placeholder="Max" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="lastName" class="mb-2 block text-sm font-medium">Nachname</label>
|
||||
<Input
|
||||
type="text"
|
||||
id="lastName"
|
||||
bind:value={lastName}
|
||||
placeholder="Mustermann"
|
||||
/>
|
||||
<Input type="text" id="lastName" bind:value={lastName} placeholder="Mustermann" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onclick={handleUpdateProfile}
|
||||
loading={savingProfile}
|
||||
class="w-full sm:w-auto"
|
||||
>
|
||||
<Button onclick={handleUpdateProfile} loading={savingProfile} class="w-full sm:w-auto">
|
||||
{savingProfile ? 'Speichern...' : 'Änderungen speichern'}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -144,7 +138,9 @@
|
|||
<div class="p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-yellow-100 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-full bg-yellow-100 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
|
|
@ -172,7 +168,9 @@
|
|||
<div class="rounded-lg bg-surface-hover p-4 text-center">
|
||||
<p class="text-sm text-muted-foreground">Gratis heute</p>
|
||||
<p class="text-2xl font-bold text-green-600 dark:text-green-400">
|
||||
{creditBalance ? `${creditBalance.freeCreditsRemaining}/${creditBalance.dailyFreeCredits}` : '...'}
|
||||
{creditBalance
|
||||
? `${creditBalance.freeCreditsRemaining}/${creditBalance.dailyFreeCredits}`
|
||||
: '...'}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg bg-surface-hover p-4 text-center">
|
||||
|
|
@ -204,7 +202,9 @@
|
|||
<Card>
|
||||
<div class="p-6">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
|
|
@ -226,7 +226,9 @@
|
|||
<p class="font-medium">Konto-Status</p>
|
||||
<p class="text-sm text-muted-foreground">Dein aktueller Kontostatus</p>
|
||||
</div>
|
||||
<span class="rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-800 dark:bg-green-900/20 dark:text-green-400">
|
||||
<span
|
||||
class="rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-800 dark:bg-green-900/20 dark:text-green-400"
|
||||
>
|
||||
Aktiv
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -236,7 +238,9 @@
|
|||
<p class="font-medium">Rolle</p>
|
||||
<p class="text-sm text-muted-foreground">Deine Berechtigungsstufe</p>
|
||||
</div>
|
||||
<span class="rounded-full bg-blue-100 px-3 py-1 text-xs font-medium text-blue-800 dark:bg-blue-900/20 dark:text-blue-400">
|
||||
<span
|
||||
class="rounded-full bg-blue-100 px-3 py-1 text-xs font-medium text-blue-800 dark:bg-blue-900/20 dark:text-blue-400"
|
||||
>
|
||||
{authStore.user?.role || 'user'}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -258,7 +262,9 @@
|
|||
<Card>
|
||||
<div class="p-6 border-red-200 dark:border-red-800">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/20 text-red-600 dark:text-red-400">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/20 text-red-600 dark:text-red-400"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
|
|
@ -282,11 +288,7 @@
|
|||
Das Löschen deines Kontos kann nicht rückgängig gemacht werden.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled
|
||||
class="bg-red-600 hover:bg-red-700 text-white"
|
||||
>
|
||||
<Button variant="destructive" disabled class="bg-red-600 hover:bg-red-700 text-white">
|
||||
Konto löschen
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,15 +19,16 @@
|
|||
</script>
|
||||
|
||||
<div>
|
||||
<PageHeader
|
||||
title="Teams"
|
||||
description="Manage your teams and collaborate with members"
|
||||
size="lg"
|
||||
>
|
||||
<PageHeader title="Teams" description="Manage your teams and collaborate with members" size="lg">
|
||||
{#snippet actions()}
|
||||
<Button variant="primary">
|
||||
<svg class="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Create Team
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,4 @@
|
|||
import { authStore } from '$lib/stores/authStore.svelte';
|
||||
</script>
|
||||
|
||||
<FeedbackPage
|
||||
{feedbackService}
|
||||
appName="ManaDeck"
|
||||
currentUserId={authStore.user?.id}
|
||||
/>
|
||||
<FeedbackPage {feedbackService} appName="ManaDeck" currentUserId={authStore.user?.id} />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import { Controller, Get, Patch, Body, UseGuards } from '@nestjs/common';
|
||||
import { ProfileService } from './profile.service';
|
||||
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
|
||||
import { UpdateProfileDto, ProfileResponse, UserStatsResponse, RateLimitsResponse } from './dto/profile.dto';
|
||||
import {
|
||||
UpdateProfileDto,
|
||||
ProfileResponse,
|
||||
UserStatsResponse,
|
||||
RateLimitsResponse,
|
||||
} from './dto/profile.dto';
|
||||
|
||||
@Controller('profiles')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,12 @@ import { eq, and, isNull, isNotNull, sql, gte, inArray } from 'drizzle-orm';
|
|||
import { DATABASE_CONNECTION } from '../db/database.module';
|
||||
import { type Database } from '../db/connection';
|
||||
import { profiles, images, imageGenerations, type Profile } from '../db/schema';
|
||||
import { UpdateProfileDto, ProfileResponse, UserStatsResponse, RateLimitsResponse } from './dto/profile.dto';
|
||||
import {
|
||||
UpdateProfileDto,
|
||||
ProfileResponse,
|
||||
UserStatsResponse,
|
||||
RateLimitsResponse,
|
||||
} from './dto/profile.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ProfileService {
|
||||
|
|
@ -156,13 +161,17 @@ export class ProfileService {
|
|||
const dailyResult = await this.db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(imageGenerations)
|
||||
.where(and(eq(imageGenerations.userId, userId), gte(imageGenerations.createdAt, startOfDay)));
|
||||
.where(
|
||||
and(eq(imageGenerations.userId, userId), gte(imageGenerations.createdAt, startOfDay))
|
||||
);
|
||||
|
||||
// Count hourly generations
|
||||
const hourlyResult = await this.db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(imageGenerations)
|
||||
.where(and(eq(imageGenerations.userId, userId), gte(imageGenerations.createdAt, startOfHour)));
|
||||
.where(
|
||||
and(eq(imageGenerations.userId, userId), gte(imageGenerations.createdAt, startOfHour))
|
||||
);
|
||||
|
||||
// Count active generations (pending, queued, processing)
|
||||
const activeResult = await this.db
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@
|
|||
|
||||
const selectedItem = $derived($selectedItems[0] || null);
|
||||
const hasMultipleSelected = $derived($selectedItems.length > 1);
|
||||
const selectedImageItem = $derived(selectedItem && isImageItem(selectedItem) ? selectedItem : null);
|
||||
const selectedImageItem = $derived(
|
||||
selectedItem && isImageItem(selectedItem) ? selectedItem : null
|
||||
);
|
||||
|
||||
// Local state for inputs (synced with selected item)
|
||||
let positionX = $state(0);
|
||||
|
|
|
|||
|
|
@ -93,7 +93,9 @@
|
|||
class="absolute right-0 mt-2 w-48 rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="border-b border-gray-100 px-4 py-2 dark:border-gray-700">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">{authStore.user?.email}</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{authStore.user?.email}
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="/app/profile"
|
||||
|
|
|
|||
|
|
@ -173,9 +173,9 @@
|
|||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'}"
|
||||
>
|
||||
<span
|
||||
class="{active
|
||||
class={active
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-400 group-hover:text-gray-600 dark:text-gray-500 dark:group-hover:text-gray-300'}"
|
||||
: 'text-gray-400 group-hover:text-gray-600 dark:text-gray-500 dark:group-hover:text-gray-300'}
|
||||
>
|
||||
{#if item.iconName === 'gallery'}
|
||||
<Image size={20} />
|
||||
|
|
@ -193,7 +193,9 @@
|
|||
<Archive size={20} />
|
||||
{:else if item.iconName === 'mana'}
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12.3 1c.03.05 7.3 9.67 7.3 13.7 0 4.03-3.27 7.3-7.3 7.3S5 18.73 5 14.7C5 10.66 12.3 1 12.3 1zm0 6.4c-.02.03-3.65 4.83-3.65 6.84 0 2.02 1.64 3.65 3.65 3.65s3.65-1.64 3.65-3.65c0-2.01-3.62-6.81-3.65-6.84z" />
|
||||
<path
|
||||
d="M12.3 1c.03.05 7.3 9.67 7.3 13.7 0 4.03-3.27 7.3-7.3 7.3S5 18.73 5 14.7C5 10.66 12.3 1 12.3 1zm0 6.4c-.02.03-3.65 4.83-3.65 6.84 0 2.02 1.64 3.65 3.65 3.65s3.65-1.64 3.65-3.65c0-2.01-3.62-6.81-3.65-6.84z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</span>
|
||||
|
|
@ -209,7 +211,9 @@
|
|||
onclick={() => showKeyboardShortcuts.set(true)}
|
||||
class="group flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-gray-700 transition-all hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
<span class="text-gray-400 group-hover:text-gray-600 dark:text-gray-500 dark:group-hover:text-gray-300">
|
||||
<span
|
||||
class="text-gray-400 group-hover:text-gray-600 dark:text-gray-500 dark:group-hover:text-gray-300"
|
||||
>
|
||||
<Question size={20} />
|
||||
</span>
|
||||
<span>Tastaturkürzel</span>
|
||||
|
|
@ -472,7 +476,7 @@
|
|||
? 'bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400'
|
||||
: 'text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800'}"
|
||||
>
|
||||
<span class="{active ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400'}">
|
||||
<span class={active ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400'}>
|
||||
{#if item.iconName === 'gallery'}
|
||||
<Image size={20} />
|
||||
{:else if item.iconName === 'board'}
|
||||
|
|
@ -489,7 +493,9 @@
|
|||
<Archive size={20} />
|
||||
{:else if item.iconName === 'mana'}
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12.3 1c.03.05 7.3 9.67 7.3 13.7 0 4.03-3.27 7.3-7.3 7.3S5 18.73 5 14.7C5 10.66 12.3 1 12.3 1zm0 6.4c-.02.03-3.65 4.83-3.65 6.84 0 2.02 1.64 3.65 3.65 3.65s3.65-1.64 3.65-3.65c0-2.01-3.62-6.81-3.65-6.84z" />
|
||||
<path
|
||||
d="M12.3 1c.03.05 7.3 9.67 7.3 13.7 0 4.03-3.27 7.3-7.3 7.3S5 18.73 5 14.7C5 10.66 12.3 1 12.3 1zm0 6.4c-.02.03-3.65 4.83-3.65 6.84 0 2.02 1.64 3.65 3.65 3.65s3.65-1.64 3.65-3.65c0-2.01-3.62-6.81-3.65-6.84z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</span>
|
||||
|
|
@ -547,7 +553,9 @@
|
|||
<Archive size={24} />
|
||||
{:else if item.iconName === 'mana'}
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12.3 1c.03.05 7.3 9.67 7.3 13.7 0 4.03-3.27 7.3-7.3 7.3S5 18.73 5 14.7C5 10.66 12.3 1 12.3 1zm0 6.4c-.02.03-3.65 4.83-3.65 6.84 0 2.02 1.64 3.65 3.65 3.65s3.65-1.64 3.65-3.65c0-2.01-3.62-6.81-3.65-6.84z" />
|
||||
<path
|
||||
d="M12.3 1c.03.05 7.3 9.67 7.3 13.7 0 4.03-3.27 7.3-7.3 7.3S5 18.73 5 14.7C5 10.66 12.3 1 12.3 1zm0 6.4c-.02.03-3.65 4.83-3.65 6.84 0 2.02 1.64 3.65 3.65 3.65s3.65-1.64 3.65-3.65c0-2.01-3.62-6.81-3.65-6.84z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
<span class="text-xs font-medium">{item.label}</span>
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@
|
|||
options: viewModeOptions,
|
||||
value: $viewMode === 'grid5' ? 'gridSmall' : $viewMode,
|
||||
onChange: (id: string) => {
|
||||
const mode = id === 'gridSmall' ? 'grid5' : id as ViewMode;
|
||||
const mode = id === 'gridSmall' ? 'grid5' : (id as ViewMode);
|
||||
setViewMode(mode);
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -178,7 +178,10 @@
|
|||
placeholder="Bilder suchen..."
|
||||
class="w-full rounded-full border border-gray-300/50 bg-white/80 px-4 py-2 pl-10 text-sm text-gray-900 placeholder-gray-500 backdrop-blur-xl transition-all focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600/50 dark:bg-gray-800/80 dark:text-gray-100 dark:placeholder-gray-400"
|
||||
/>
|
||||
<MagnifyingGlass size={16} class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500" />
|
||||
<MagnifyingGlass
|
||||
size={16}
|
||||
class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500"
|
||||
/>
|
||||
{#if searchInput}
|
||||
<button
|
||||
onclick={() => {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,4 @@
|
|||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
</script>
|
||||
|
||||
<FeedbackPage
|
||||
{feedbackService}
|
||||
appName="Picture"
|
||||
currentUserId={authStore.user?.id}
|
||||
/>
|
||||
<FeedbackPage {feedbackService} appName="Picture" currentUserId={authStore.user?.id} />
|
||||
|
|
|
|||
|
|
@ -137,7 +137,9 @@
|
|||
<!-- Tags -->
|
||||
{#if $tags.length > 0}
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Tags:</span>
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
||||
>Tags:</span
|
||||
>
|
||||
<TagPills />
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -949,4 +949,13 @@ declare function isValidThemeVariant(variant: string): variant is ThemeVariant;
|
|||
*/
|
||||
type NativeTheme = ReturnType<typeof createNativeTheme>;
|
||||
|
||||
export { type ColorMode, type NativeTheme, type SemanticColors, type ThemeVariant, createNativeTheme, getThemeColors, getThemeVariants, isValidThemeVariant };
|
||||
export {
|
||||
type ColorMode,
|
||||
type NativeTheme,
|
||||
type SemanticColors,
|
||||
type ThemeVariant,
|
||||
createNativeTheme,
|
||||
getThemeColors,
|
||||
getThemeVariants,
|
||||
isValidThemeVariant,
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,13 +1,4 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards } from '@nestjs/common';
|
||||
import { DeckService } from './deck.service';
|
||||
import { CreateDeckDto, UpdateDeckDto } from './deck.dto';
|
||||
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
|
||||
|
|
|
|||
|
|
@ -27,10 +27,7 @@ export class ShareController {
|
|||
|
||||
@Get('deck/:deckId/links')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async getSharesForDeck(
|
||||
@Param('deckId') deckId: string,
|
||||
@CurrentUser() user: CurrentUserData
|
||||
) {
|
||||
async getSharesForDeck(@Param('deckId') deckId: string, @CurrentUser() user: CurrentUserData) {
|
||||
return this.shareService.getSharesForDeck(deckId, user.userId);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,11 +61,7 @@
|
|||
</svelte:head>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<PageHeader
|
||||
title="My Presentations"
|
||||
description="Create and manage your slide decks"
|
||||
size="lg"
|
||||
>
|
||||
<PageHeader title="My Presentations" description="Create and manage your slide decks" size="lg">
|
||||
{#snippet actions()}
|
||||
<button
|
||||
onclick={() => (showCreateModal = true)}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,4 @@
|
|||
import { auth } from '$lib/stores/auth.svelte';
|
||||
</script>
|
||||
|
||||
<FeedbackPage
|
||||
{feedbackService}
|
||||
appName="Presi"
|
||||
currentUserId={auth.user?.id}
|
||||
/>
|
||||
<FeedbackPage {feedbackService} appName="Presi" currentUserId={auth.user?.id} />
|
||||
|
|
|
|||
|
|
@ -110,9 +110,7 @@
|
|||
class="flex items-center justify-between p-4 hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<div class="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center">
|
||||
<FolderOpen class="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -64,7 +64,9 @@
|
|||
/>
|
||||
</svg>
|
||||
{/snippet}
|
||||
<span class="font-mono text-xs text-[hsl(var(--muted-foreground))]">{auth.user?.id || '-'}</span>
|
||||
<span class="font-mono text-xs text-[hsl(var(--muted-foreground))]"
|
||||
>{auth.user?.id || '-'}</span
|
||||
>
|
||||
</SettingsRow>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
|
|
@ -85,9 +87,7 @@
|
|||
<SettingsCard>
|
||||
<div class="px-5 py-4">
|
||||
<p class="font-medium text-[hsl(var(--foreground))] mb-2">Theme</p>
|
||||
<p class="text-sm text-[hsl(var(--muted-foreground))] mb-4">
|
||||
Choose your preferred theme
|
||||
</p>
|
||||
<p class="text-sm text-[hsl(var(--muted-foreground))] mb-4">Choose your preferred theme</p>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<button
|
||||
onclick={() => setThemeMode('light')}
|
||||
|
|
@ -96,7 +96,12 @@
|
|||
? 'border-[hsl(var(--primary))] bg-[hsl(var(--primary)/0.1)]'
|
||||
: 'border-[hsl(var(--border))]'}"
|
||||
>
|
||||
<svg class="w-6 h-6 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg
|
||||
class="w-6 h-6 text-amber-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
|
|
@ -113,7 +118,12 @@
|
|||
? 'border-[hsl(var(--primary))] bg-[hsl(var(--primary)/0.1)]'
|
||||
: 'border-[hsl(var(--border))]'}"
|
||||
>
|
||||
<svg class="w-6 h-6 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg
|
||||
class="w-6 h-6 text-indigo-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
|
|
@ -130,7 +140,12 @@
|
|||
? 'border-[hsl(var(--primary))] bg-[hsl(var(--primary)/0.1)]'
|
||||
: 'border-[hsl(var(--border))]'}"
|
||||
>
|
||||
<svg class="w-6 h-6 text-[hsl(var(--muted-foreground))]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg
|
||||
class="w-6 h-6 text-[hsl(var(--muted-foreground))]"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,4 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
|
||||
import { ListService } from './list.service';
|
||||
import { IsString, IsNotEmpty, IsOptional, IsArray } from 'class-validator';
|
||||
|
|
|
|||
|
|
@ -43,7 +43,9 @@
|
|||
favorites = new Set(favorites);
|
||||
|
||||
// Update quote's favorite status
|
||||
quotes = quotes.map((q) => (q.id === quoteId ? { ...q, isFavorite: favorites.has(quoteId) } : q));
|
||||
quotes = quotes.map((q) =>
|
||||
q.id === quoteId ? { ...q, isFavorite: favorites.has(quoteId) } : q
|
||||
);
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('favorites', JSON.stringify([...favorites]));
|
||||
|
|
@ -85,7 +87,17 @@
|
|||
|
||||
{#if quotes.length > 0}
|
||||
<div class="scroll-hint">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
<span>Scrollen für mehr</span>
|
||||
|
|
@ -144,7 +156,11 @@
|
|||
}
|
||||
|
||||
@keyframes bounce-arrow {
|
||||
0%, 20%, 50%, 80%, 100% {
|
||||
0%,
|
||||
20%,
|
||||
50%,
|
||||
80%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,4 @@
|
|||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
</script>
|
||||
|
||||
<FeedbackPage
|
||||
{feedbackService}
|
||||
appName="Zitare"
|
||||
currentUserId={authStore.user?.id}
|
||||
/>
|
||||
<FeedbackPage {feedbackService} appName="Zitare" currentUserId={authStore.user?.id} />
|
||||
|
|
|
|||
|
|
@ -131,8 +131,20 @@
|
|||
<div class="search-header">
|
||||
<h2>Suche</h2>
|
||||
<div class="search-input-wrapper">
|
||||
<svg class="search-icon" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
<svg
|
||||
class="search-icon"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
|
|
@ -144,7 +156,12 @@
|
|||
{#if searchTerm}
|
||||
<button class="clear-btn" onclick={() => (searchTerm = '')} aria-label="Clear search">
|
||||
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
|
|
@ -157,10 +174,18 @@
|
|||
<button class="tab" class:active={activeTab === 'all'} onclick={() => (activeTab = 'all')}>
|
||||
Alle ({filteredQuotes.length + filteredAuthors.length})
|
||||
</button>
|
||||
<button class="tab" class:active={activeTab === 'quotes'} onclick={() => (activeTab = 'quotes')}>
|
||||
<button
|
||||
class="tab"
|
||||
class:active={activeTab === 'quotes'}
|
||||
onclick={() => (activeTab = 'quotes')}
|
||||
>
|
||||
Zitate ({filteredQuotes.length})
|
||||
</button>
|
||||
<button class="tab" class:active={activeTab === 'authors'} onclick={() => (activeTab = 'authors')}>
|
||||
<button
|
||||
class="tab"
|
||||
class:active={activeTab === 'authors'}
|
||||
onclick={() => (activeTab = 'authors')}
|
||||
>
|
||||
Autoren ({filteredAuthors.length})
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -168,7 +193,15 @@
|
|||
{#if totalResults === 0}
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.35-4.35"></path>
|
||||
</svg>
|
||||
|
|
@ -234,7 +267,15 @@
|
|||
{:else}
|
||||
<div class="hint">
|
||||
<div class="hint-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.35-4.35"></path>
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -139,7 +139,9 @@
|
|||
/>
|
||||
</svg>
|
||||
{/snippet}
|
||||
<span class="px-3 py-1.5 text-sm font-medium bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))] rounded-lg">
|
||||
<span
|
||||
class="px-3 py-1.5 text-sm font-medium bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))] rounded-lg"
|
||||
>
|
||||
Themes wählen
|
||||
</span>
|
||||
</SettingsRow>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue