@weirdfingers/baseboards 0.9.5 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +561 -469
- package/dist/index.js.map +1 -1
- package/package.json +2 -5
- package/templates/README.md +0 -122
- package/templates/api/.env.example +0 -65
- package/templates/api/ARTIFACT_RESOLUTION_GUIDE.md +0 -148
- package/templates/api/Dockerfile +0 -32
- package/templates/api/README.md +0 -264
- package/templates/api/alembic/env.py +0 -114
- package/templates/api/alembic/script.py.mako +0 -28
- package/templates/api/alembic/versions/20250101_000000_initial_schema.py +0 -506
- package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +0 -75
- package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +0 -467
- package/templates/api/alembic/versions/20251202_000000_add_artifact_lineage.py +0 -134
- package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +0 -88
- package/templates/api/alembic.ini +0 -36
- package/templates/api/config/generators.yaml +0 -237
- package/templates/api/config/storage_config.yaml +0 -26
- package/templates/api/docs/ADDING_GENERATORS.md +0 -409
- package/templates/api/docs/GENERATORS_API.md +0 -502
- package/templates/api/docs/MIGRATIONS.md +0 -472
- package/templates/api/docs/TESTING_LIVE_APIS.md +0 -417
- package/templates/api/docs/storage_providers.md +0 -337
- package/templates/api/pyproject.toml +0 -205
- package/templates/api/src/boards/__init__.py +0 -10
- package/templates/api/src/boards/api/app.py +0 -172
- package/templates/api/src/boards/api/auth.py +0 -75
- package/templates/api/src/boards/api/endpoints/__init__.py +0 -3
- package/templates/api/src/boards/api/endpoints/jobs.py +0 -76
- package/templates/api/src/boards/api/endpoints/setup.py +0 -505
- package/templates/api/src/boards/api/endpoints/sse.py +0 -129
- package/templates/api/src/boards/api/endpoints/storage.py +0 -155
- package/templates/api/src/boards/api/endpoints/tenant_registration.py +0 -296
- package/templates/api/src/boards/api/endpoints/uploads.py +0 -149
- package/templates/api/src/boards/api/endpoints/webhooks.py +0 -13
- package/templates/api/src/boards/auth/__init__.py +0 -15
- package/templates/api/src/boards/auth/adapters/__init__.py +0 -27
- package/templates/api/src/boards/auth/adapters/auth0.py +0 -220
- package/templates/api/src/boards/auth/adapters/base.py +0 -73
- package/templates/api/src/boards/auth/adapters/clerk.py +0 -172
- package/templates/api/src/boards/auth/adapters/jwt.py +0 -122
- package/templates/api/src/boards/auth/adapters/none.py +0 -102
- package/templates/api/src/boards/auth/adapters/oidc.py +0 -284
- package/templates/api/src/boards/auth/adapters/supabase.py +0 -110
- package/templates/api/src/boards/auth/context.py +0 -35
- package/templates/api/src/boards/auth/factory.py +0 -129
- package/templates/api/src/boards/auth/middleware.py +0 -221
- package/templates/api/src/boards/auth/provisioning.py +0 -129
- package/templates/api/src/boards/auth/tenant_extraction.py +0 -278
- package/templates/api/src/boards/cli.py +0 -354
- package/templates/api/src/boards/config.py +0 -131
- package/templates/api/src/boards/database/__init__.py +0 -7
- package/templates/api/src/boards/database/cli.py +0 -110
- package/templates/api/src/boards/database/connection.py +0 -292
- package/templates/api/src/boards/database/models.py +0 -19
- package/templates/api/src/boards/database/seed_data.py +0 -182
- package/templates/api/src/boards/dbmodels/__init__.py +0 -441
- package/templates/api/src/boards/generators/__init__.py +0 -57
- package/templates/api/src/boards/generators/artifact_resolution.py +0 -405
- package/templates/api/src/boards/generators/artifacts.py +0 -53
- package/templates/api/src/boards/generators/base.py +0 -144
- package/templates/api/src/boards/generators/implementations/__init__.py +0 -14
- package/templates/api/src/boards/generators/implementations/fal/__init__.py +0 -25
- package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +0 -23
- package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_music_generation.py +0 -171
- package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_sound_effect_generation.py +0 -167
- package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_text_to_speech.py +0 -176
- package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_tts_turbo.py +0 -195
- package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_sound_effects_v2.py +0 -194
- package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_tts_eleven_v3.py +0 -209
- package/templates/api/src/boards/generators/implementations/fal/audio/fal_elevenlabs_tts_turbo_v2_5.py +0 -206
- package/templates/api/src/boards/generators/implementations/fal/audio/fal_minimax_speech_26_hd.py +0 -237
- package/templates/api/src/boards/generators/implementations/fal/audio/minimax_music_v2.py +0 -173
- package/templates/api/src/boards/generators/implementations/fal/audio/minimax_speech_2_6_turbo.py +0 -221
- package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +0 -63
- package/templates/api/src/boards/generators/implementations/fal/image/bytedance_seedream_v45_edit.py +0 -219
- package/templates/api/src/boards/generators/implementations/fal/image/clarity_upscaler.py +0 -220
- package/templates/api/src/boards/generators/implementations/fal/image/crystal_upscaler.py +0 -173
- package/templates/api/src/boards/generators/implementations/fal/image/fal_ideogram_character.py +0 -227
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2.py +0 -203
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_edit.py +0 -230
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro.py +0 -204
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro_edit.py +0 -221
- package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_kontext.py +0 -216
- package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_ultra.py +0 -197
- package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image.py +0 -177
- package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image_edit.py +0 -208
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_15_edit.py +0 -216
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_5.py +0 -177
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_edit_image.py +0 -182
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_mini.py +0 -167
- package/templates/api/src/boards/generators/implementations/fal/image/ideogram_character_edit.py +0 -299
- package/templates/api/src/boards/generators/implementations/fal/image/ideogram_v2.py +0 -190
- package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview.py +0 -191
- package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview_fast.py +0 -179
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana.py +0 -183
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_edit.py +0 -212
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro.py +0 -179
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro_edit.py +0 -226
- package/templates/api/src/boards/generators/implementations/fal/image/qwen_image.py +0 -249
- package/templates/api/src/boards/generators/implementations/fal/image/qwen_image_edit.py +0 -244
- package/templates/api/src/boards/generators/implementations/fal/image/reve_edit.py +0 -178
- package/templates/api/src/boards/generators/implementations/fal/image/reve_text_to_image.py +0 -155
- package/templates/api/src/boards/generators/implementations/fal/image/seedream_v45_text_to_image.py +0 -180
- package/templates/api/src/boards/generators/implementations/fal/utils.py +0 -61
- package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +0 -77
- package/templates/api/src/boards/generators/implementations/fal/video/bytedance_seedance_v1_pro_text_to_video.py +0 -209
- package/templates/api/src/boards/generators/implementations/fal/video/creatify_lipsync.py +0 -161
- package/templates/api/src/boards/generators/implementations/fal/video/fal_bytedance_seedance_v1_pro_image_to_video.py +0 -222
- package/templates/api/src/boards/generators/implementations/fal/video/fal_minimax_hailuo_02_standard_text_to_video.py +0 -152
- package/templates/api/src/boards/generators/implementations/fal/video/fal_pixverse_lipsync.py +0 -197
- package/templates/api/src/boards/generators/implementations/fal/video/fal_sora_2_text_to_video.py +0 -173
- package/templates/api/src/boards/generators/implementations/fal/video/infinitalk.py +0 -221
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_pro.py +0 -168
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_standard.py +0 -159
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_image_to_video.py +0 -175
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_text_to_video.py +0 -168
- package/templates/api/src/boards/generators/implementations/fal/video/minimax_hailuo_2_3_pro_image_to_video.py +0 -153
- package/templates/api/src/boards/generators/implementations/fal/video/sora2_image_to_video.py +0 -172
- package/templates/api/src/boards/generators/implementations/fal/video/sora_2_image_to_video_pro.py +0 -175
- package/templates/api/src/boards/generators/implementations/fal/video/sora_2_text_to_video_pro.py +0 -163
- package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2.py +0 -167
- package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2_pro.py +0 -155
- package/templates/api/src/boards/generators/implementations/fal/video/veed_fabric_1_0.py +0 -180
- package/templates/api/src/boards/generators/implementations/fal/video/veed_lipsync.py +0 -174
- package/templates/api/src/boards/generators/implementations/fal/video/veo3.py +0 -194
- package/templates/api/src/boards/generators/implementations/fal/video/veo31.py +0 -190
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast.py +0 -190
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast_image_to_video.py +0 -191
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +0 -187
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_image_to_video.py +0 -183
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_reference_to_video.py +0 -172
- package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_image_to_video.py +0 -212
- package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_text_to_video.py +0 -208
- package/templates/api/src/boards/generators/implementations/fal/video/wan_pro_image_to_video.py +0 -158
- package/templates/api/src/boards/generators/implementations/kie/__init__.py +0 -11
- package/templates/api/src/boards/generators/implementations/kie/base.py +0 -316
- package/templates/api/src/boards/generators/implementations/kie/image/__init__.py +0 -3
- package/templates/api/src/boards/generators/implementations/kie/image/nano_banana_edit.py +0 -190
- package/templates/api/src/boards/generators/implementations/kie/utils.py +0 -98
- package/templates/api/src/boards/generators/implementations/kie/video/__init__.py +0 -8
- package/templates/api/src/boards/generators/implementations/kie/video/veo3.py +0 -161
- package/templates/api/src/boards/generators/implementations/openai/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/openai/audio/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/openai/audio/whisper.py +0 -69
- package/templates/api/src/boards/generators/implementations/openai/image/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/openai/image/dalle3.py +0 -96
- package/templates/api/src/boards/generators/implementations/replicate/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/replicate/image/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/replicate/image/flux_pro.py +0 -88
- package/templates/api/src/boards/generators/implementations/replicate/video/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/replicate/video/lipsync.py +0 -73
- package/templates/api/src/boards/generators/loader.py +0 -253
- package/templates/api/src/boards/generators/registry.py +0 -114
- package/templates/api/src/boards/generators/resolution.py +0 -632
- package/templates/api/src/boards/generators/testmods/class_gen.py +0 -34
- package/templates/api/src/boards/generators/testmods/import_side_effect.py +0 -35
- package/templates/api/src/boards/graphql/__init__.py +0 -7
- package/templates/api/src/boards/graphql/access_control.py +0 -136
- package/templates/api/src/boards/graphql/mutations/root.py +0 -148
- package/templates/api/src/boards/graphql/queries/root.py +0 -116
- package/templates/api/src/boards/graphql/resolvers/__init__.py +0 -8
- package/templates/api/src/boards/graphql/resolvers/auth.py +0 -12
- package/templates/api/src/boards/graphql/resolvers/board.py +0 -1053
- package/templates/api/src/boards/graphql/resolvers/generation.py +0 -666
- package/templates/api/src/boards/graphql/resolvers/generator.py +0 -50
- package/templates/api/src/boards/graphql/resolvers/lineage.py +0 -381
- package/templates/api/src/boards/graphql/resolvers/upload.py +0 -463
- package/templates/api/src/boards/graphql/resolvers/user.py +0 -25
- package/templates/api/src/boards/graphql/schema.py +0 -81
- package/templates/api/src/boards/graphql/types/board.py +0 -102
- package/templates/api/src/boards/graphql/types/generation.py +0 -166
- package/templates/api/src/boards/graphql/types/generator.py +0 -17
- package/templates/api/src/boards/graphql/types/user.py +0 -47
- package/templates/api/src/boards/jobs/repository.py +0 -153
- package/templates/api/src/boards/logging.py +0 -195
- package/templates/api/src/boards/middleware.py +0 -339
- package/templates/api/src/boards/progress/__init__.py +0 -4
- package/templates/api/src/boards/progress/models.py +0 -25
- package/templates/api/src/boards/progress/publisher.py +0 -64
- package/templates/api/src/boards/py.typed +0 -0
- package/templates/api/src/boards/redis_pool.py +0 -118
- package/templates/api/src/boards/storage/__init__.py +0 -52
- package/templates/api/src/boards/storage/base.py +0 -363
- package/templates/api/src/boards/storage/config.py +0 -187
- package/templates/api/src/boards/storage/factory.py +0 -288
- package/templates/api/src/boards/storage/implementations/__init__.py +0 -27
- package/templates/api/src/boards/storage/implementations/gcs.py +0 -340
- package/templates/api/src/boards/storage/implementations/local.py +0 -201
- package/templates/api/src/boards/storage/implementations/s3.py +0 -294
- package/templates/api/src/boards/storage/implementations/supabase.py +0 -218
- package/templates/api/src/boards/tenant_isolation.py +0 -446
- package/templates/api/src/boards/validation.py +0 -262
- package/templates/api/src/boards/workers/__init__.py +0 -1
- package/templates/api/src/boards/workers/actors.py +0 -274
- package/templates/api/src/boards/workers/cli.py +0 -125
- package/templates/api/src/boards/workers/context.py +0 -348
- package/templates/api/src/boards/workers/middleware.py +0 -58
- package/templates/api/src/py.typed +0 -0
- package/templates/compose.web.yaml +0 -35
- package/templates/compose.yaml +0 -116
- package/templates/docker/env.example +0 -23
- package/templates/web/.env.example +0 -28
- package/templates/web/Dockerfile +0 -51
- package/templates/web/components.json +0 -22
- package/templates/web/imageLoader.js +0 -18
- package/templates/web/next-env.d.ts +0 -5
- package/templates/web/next.config.js +0 -36
- package/templates/web/package.json +0 -41
- package/templates/web/postcss.config.mjs +0 -7
- package/templates/web/public/favicon.ico +0 -0
- package/templates/web/src/app/boards/[boardId]/page.tsx +0 -353
- package/templates/web/src/app/globals.css +0 -123
- package/templates/web/src/app/layout.tsx +0 -31
- package/templates/web/src/app/lineage/[generationId]/page.tsx +0 -235
- package/templates/web/src/app/page.tsx +0 -35
- package/templates/web/src/app/providers.tsx +0 -18
- package/templates/web/src/components/boards/ArtifactInputSlots.tsx +0 -206
- package/templates/web/src/components/boards/ArtifactPreview.tsx +0 -466
- package/templates/web/src/components/boards/GenerationGrid.tsx +0 -282
- package/templates/web/src/components/boards/GenerationInput.tsx +0 -370
- package/templates/web/src/components/boards/GeneratorSelector.tsx +0 -272
- package/templates/web/src/components/boards/UploadArtifact.tsx +0 -563
- package/templates/web/src/components/header.tsx +0 -32
- package/templates/web/src/components/theme-provider.tsx +0 -10
- package/templates/web/src/components/theme-toggle.tsx +0 -75
- package/templates/web/src/components/ui/alert-dialog.tsx +0 -157
- package/templates/web/src/components/ui/button.tsx +0 -58
- package/templates/web/src/components/ui/card.tsx +0 -92
- package/templates/web/src/components/ui/dropdown-menu.tsx +0 -200
- package/templates/web/src/components/ui/navigation-menu.tsx +0 -168
- package/templates/web/src/components/ui/toast.tsx +0 -128
- package/templates/web/src/components/ui/toaster.tsx +0 -35
- package/templates/web/src/components/ui/use-toast.ts +0 -187
- package/templates/web/src/hooks/useGeneratorMRU.ts +0 -57
- package/templates/web/src/lib/utils.ts +0 -6
- package/templates/web/tsconfig.json +0 -41
|
@@ -1,563 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { useCallback, useState, useRef } from "react";
|
|
4
|
-
import {
|
|
5
|
-
useMultiUpload,
|
|
6
|
-
ArtifactType,
|
|
7
|
-
UploadItem,
|
|
8
|
-
MultiUploadRequest,
|
|
9
|
-
} from "@weirdfingers/boards";
|
|
10
|
-
import { toast } from "@/components/ui/use-toast";
|
|
11
|
-
|
|
12
|
-
interface UploadArtifactProps {
|
|
13
|
-
boardId: string;
|
|
14
|
-
onUploadComplete?: (generationId: string) => void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Maximum file size: 100MB
|
|
18
|
-
const MAX_FILE_SIZE = 100 * 1024 * 1024;
|
|
19
|
-
|
|
20
|
-
// Allowed MIME types for each artifact type
|
|
21
|
-
const ALLOWED_MIME_TYPES = {
|
|
22
|
-
image: [
|
|
23
|
-
"image/jpeg",
|
|
24
|
-
"image/png",
|
|
25
|
-
"image/gif",
|
|
26
|
-
"image/webp",
|
|
27
|
-
"image/bmp",
|
|
28
|
-
"image/svg+xml",
|
|
29
|
-
],
|
|
30
|
-
video: [
|
|
31
|
-
"video/mp4",
|
|
32
|
-
"video/quicktime",
|
|
33
|
-
"video/x-msvideo",
|
|
34
|
-
"video/webm",
|
|
35
|
-
"video/mpeg",
|
|
36
|
-
"video/x-matroska",
|
|
37
|
-
],
|
|
38
|
-
audio: [
|
|
39
|
-
"audio/mpeg",
|
|
40
|
-
"audio/wav",
|
|
41
|
-
"audio/ogg",
|
|
42
|
-
"audio/webm",
|
|
43
|
-
"audio/mp4",
|
|
44
|
-
"audio/x-m4a",
|
|
45
|
-
],
|
|
46
|
-
text: [
|
|
47
|
-
"text/plain",
|
|
48
|
-
"text/markdown",
|
|
49
|
-
"application/json",
|
|
50
|
-
"text/html",
|
|
51
|
-
"text/csv",
|
|
52
|
-
],
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
function detectArtifactType(mimeType: string): ArtifactType {
|
|
56
|
-
if (mimeType.startsWith("image/")) {
|
|
57
|
-
return ArtifactType.IMAGE;
|
|
58
|
-
} else if (mimeType.startsWith("video/")) {
|
|
59
|
-
return ArtifactType.VIDEO;
|
|
60
|
-
} else if (mimeType.startsWith("audio/")) {
|
|
61
|
-
return ArtifactType.AUDIO;
|
|
62
|
-
} else if (mimeType.startsWith("text/")) {
|
|
63
|
-
return ArtifactType.TEXT;
|
|
64
|
-
}
|
|
65
|
-
return ArtifactType.IMAGE;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function validateFile(file: File): { valid: boolean; error?: string } {
|
|
69
|
-
// Check file size
|
|
70
|
-
if (file.size > MAX_FILE_SIZE) {
|
|
71
|
-
return {
|
|
72
|
-
valid: false,
|
|
73
|
-
error: `File size exceeds maximum of 100MB (${Math.round(file.size / 1024 / 1024)}MB)`,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Check MIME type
|
|
78
|
-
const mimeType = file.type.toLowerCase();
|
|
79
|
-
const isValidMimeType = Object.values(ALLOWED_MIME_TYPES)
|
|
80
|
-
.flat()
|
|
81
|
-
.some((allowed) => mimeType === allowed.toLowerCase());
|
|
82
|
-
|
|
83
|
-
if (!isValidMimeType) {
|
|
84
|
-
return {
|
|
85
|
-
valid: false,
|
|
86
|
-
error: `Unsupported file type: ${file.type || "unknown"}. Please upload an image, video, audio, or text file.`,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return { valid: true };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function validateUrl(url: string): { valid: boolean; error?: string } {
|
|
94
|
-
try {
|
|
95
|
-
const parsedUrl = new URL(url);
|
|
96
|
-
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
97
|
-
return {
|
|
98
|
-
valid: false,
|
|
99
|
-
error: "URL must use HTTP or HTTPS protocol",
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
return { valid: true };
|
|
103
|
-
} catch {
|
|
104
|
-
return {
|
|
105
|
-
valid: false,
|
|
106
|
-
error: "Invalid URL format",
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function UploadItemRow({
|
|
112
|
-
item,
|
|
113
|
-
onCancel,
|
|
114
|
-
}: {
|
|
115
|
-
item: UploadItem;
|
|
116
|
-
onCancel: () => void;
|
|
117
|
-
}) {
|
|
118
|
-
return (
|
|
119
|
-
<div className="flex items-center gap-3 p-2 bg-muted/50 rounded-lg">
|
|
120
|
-
<div className="flex-1 min-w-0">
|
|
121
|
-
<p className="text-sm font-medium text-foreground truncate">
|
|
122
|
-
{item.fileName}
|
|
123
|
-
</p>
|
|
124
|
-
{item.status === "uploading" && (
|
|
125
|
-
<div className="mt-1 w-full bg-muted rounded-full h-1.5">
|
|
126
|
-
<div
|
|
127
|
-
className="bg-primary h-1.5 rounded-full transition-all duration-300"
|
|
128
|
-
style={{ width: `${item.progress}%` }}
|
|
129
|
-
/>
|
|
130
|
-
</div>
|
|
131
|
-
)}
|
|
132
|
-
{item.status === "failed" && item.error && (
|
|
133
|
-
<p className="text-xs text-red-600 dark:text-red-400 mt-1">{item.error.message}</p>
|
|
134
|
-
)}
|
|
135
|
-
</div>
|
|
136
|
-
<div className="flex-shrink-0">
|
|
137
|
-
{item.status === "pending" && (
|
|
138
|
-
<span className="text-xs text-muted-foreground">Waiting...</span>
|
|
139
|
-
)}
|
|
140
|
-
{item.status === "uploading" && (
|
|
141
|
-
<button
|
|
142
|
-
onClick={onCancel}
|
|
143
|
-
className="text-xs text-muted-foreground hover:text-red-600 dark:hover:text-red-400"
|
|
144
|
-
>
|
|
145
|
-
Cancel
|
|
146
|
-
</button>
|
|
147
|
-
)}
|
|
148
|
-
{item.status === "completed" && (
|
|
149
|
-
<svg
|
|
150
|
-
className="w-5 h-5 text-success"
|
|
151
|
-
fill="none"
|
|
152
|
-
stroke="currentColor"
|
|
153
|
-
viewBox="0 0 24 24"
|
|
154
|
-
>
|
|
155
|
-
<path
|
|
156
|
-
strokeLinecap="round"
|
|
157
|
-
strokeLinejoin="round"
|
|
158
|
-
strokeWidth={2}
|
|
159
|
-
d="M5 13l4 4L19 7"
|
|
160
|
-
/>
|
|
161
|
-
</svg>
|
|
162
|
-
)}
|
|
163
|
-
{item.status === "failed" && (
|
|
164
|
-
<svg
|
|
165
|
-
className="w-5 h-5 text-red-600 dark:text-red-400"
|
|
166
|
-
fill="none"
|
|
167
|
-
stroke="currentColor"
|
|
168
|
-
viewBox="0 0 24 24"
|
|
169
|
-
>
|
|
170
|
-
<path
|
|
171
|
-
strokeLinecap="round"
|
|
172
|
-
strokeLinejoin="round"
|
|
173
|
-
strokeWidth={2}
|
|
174
|
-
d="M6 18L18 6M6 6l12 12"
|
|
175
|
-
/>
|
|
176
|
-
</svg>
|
|
177
|
-
)}
|
|
178
|
-
</div>
|
|
179
|
-
</div>
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export function UploadArtifact({
|
|
184
|
-
boardId,
|
|
185
|
-
onUploadComplete,
|
|
186
|
-
}: UploadArtifactProps) {
|
|
187
|
-
const {
|
|
188
|
-
uploadMultiple,
|
|
189
|
-
uploads,
|
|
190
|
-
isUploading,
|
|
191
|
-
overallProgress,
|
|
192
|
-
clearUploads,
|
|
193
|
-
cancelUpload,
|
|
194
|
-
} = useMultiUpload();
|
|
195
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
196
|
-
const [urlInput, setUrlInput] = useState("");
|
|
197
|
-
const [dragActive, setDragActive] = useState(false);
|
|
198
|
-
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
199
|
-
|
|
200
|
-
const handleFilesUpload = useCallback(
|
|
201
|
-
async (files: File[]) => {
|
|
202
|
-
if (files.length === 0) return;
|
|
203
|
-
|
|
204
|
-
// Validate all files first
|
|
205
|
-
const invalidFiles: string[] = [];
|
|
206
|
-
const validFiles: File[] = [];
|
|
207
|
-
|
|
208
|
-
files.forEach((file) => {
|
|
209
|
-
const validation = validateFile(file);
|
|
210
|
-
if (!validation.valid) {
|
|
211
|
-
invalidFiles.push(`${file.name}: ${validation.error}`);
|
|
212
|
-
} else {
|
|
213
|
-
validFiles.push(file);
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// Show errors for invalid files
|
|
218
|
-
if (invalidFiles.length > 0) {
|
|
219
|
-
toast({
|
|
220
|
-
variant: "destructive",
|
|
221
|
-
title: "Some files were rejected",
|
|
222
|
-
description: invalidFiles.join("; "),
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Upload valid files only
|
|
227
|
-
if (validFiles.length === 0) return;
|
|
228
|
-
|
|
229
|
-
const requests: MultiUploadRequest[] = validFiles.map((file) => ({
|
|
230
|
-
boardId,
|
|
231
|
-
artifactType: detectArtifactType(file.type),
|
|
232
|
-
source: file,
|
|
233
|
-
}));
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
const results = await uploadMultiple(requests);
|
|
237
|
-
results.forEach((result) => {
|
|
238
|
-
onUploadComplete?.(result.id);
|
|
239
|
-
});
|
|
240
|
-
} catch (err) {
|
|
241
|
-
toast({
|
|
242
|
-
variant: "destructive",
|
|
243
|
-
title: "Upload failed",
|
|
244
|
-
description: err instanceof Error ? err.message : "An unknown error occurred",
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
},
|
|
248
|
-
[uploadMultiple, boardId, onUploadComplete]
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
const handleUrlUpload = useCallback(async () => {
|
|
252
|
-
if (!urlInput.trim()) return;
|
|
253
|
-
|
|
254
|
-
// Validate URL
|
|
255
|
-
const validation = validateUrl(urlInput.trim());
|
|
256
|
-
if (!validation.valid) {
|
|
257
|
-
toast({
|
|
258
|
-
variant: "destructive",
|
|
259
|
-
title: "Invalid URL",
|
|
260
|
-
description: validation.error,
|
|
261
|
-
});
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
try {
|
|
266
|
-
const results = await uploadMultiple([
|
|
267
|
-
{
|
|
268
|
-
boardId,
|
|
269
|
-
artifactType: ArtifactType.IMAGE,
|
|
270
|
-
source: urlInput.trim(),
|
|
271
|
-
},
|
|
272
|
-
]);
|
|
273
|
-
if (results.length > 0) {
|
|
274
|
-
onUploadComplete?.(results[0].id);
|
|
275
|
-
}
|
|
276
|
-
setUrlInput("");
|
|
277
|
-
} catch (err) {
|
|
278
|
-
toast({
|
|
279
|
-
variant: "destructive",
|
|
280
|
-
title: "URL upload failed",
|
|
281
|
-
description: err instanceof Error ? err.message : "An unknown error occurred",
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
}, [uploadMultiple, boardId, urlInput, onUploadComplete]);
|
|
285
|
-
|
|
286
|
-
const handleDrop = useCallback(
|
|
287
|
-
(e: React.DragEvent) => {
|
|
288
|
-
e.preventDefault();
|
|
289
|
-
setDragActive(false);
|
|
290
|
-
|
|
291
|
-
const files = Array.from(e.dataTransfer.files);
|
|
292
|
-
if (files.length > 0) {
|
|
293
|
-
handleFilesUpload(files);
|
|
294
|
-
}
|
|
295
|
-
},
|
|
296
|
-
[handleFilesUpload]
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
const handleDragOver = (e: React.DragEvent) => {
|
|
300
|
-
e.preventDefault();
|
|
301
|
-
setDragActive(true);
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
const handleDragLeave = () => {
|
|
305
|
-
setDragActive(false);
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
const handlePaste = useCallback(
|
|
309
|
-
async (e: React.ClipboardEvent) => {
|
|
310
|
-
const items = Array.from(e.clipboardData.items);
|
|
311
|
-
const imageFiles: File[] = [];
|
|
312
|
-
|
|
313
|
-
for (const item of items) {
|
|
314
|
-
if (item.type.startsWith("image/")) {
|
|
315
|
-
const file = item.getAsFile();
|
|
316
|
-
if (file) {
|
|
317
|
-
imageFiles.push(file);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (imageFiles.length > 0) {
|
|
323
|
-
e.preventDefault();
|
|
324
|
-
await handleFilesUpload(imageFiles);
|
|
325
|
-
}
|
|
326
|
-
},
|
|
327
|
-
[handleFilesUpload]
|
|
328
|
-
);
|
|
329
|
-
|
|
330
|
-
const handleClose = useCallback(() => {
|
|
331
|
-
setIsOpen(false);
|
|
332
|
-
// Clear completed/failed uploads when closing
|
|
333
|
-
if (!isUploading) {
|
|
334
|
-
clearUploads();
|
|
335
|
-
}
|
|
336
|
-
}, [isUploading, clearUploads]);
|
|
337
|
-
|
|
338
|
-
// Filter to show only active uploads (not completed ones unless recent)
|
|
339
|
-
const activeUploads = uploads.filter(
|
|
340
|
-
(u) => u.status === "pending" || u.status === "uploading"
|
|
341
|
-
);
|
|
342
|
-
const completedCount = uploads.filter((u) => u.status === "completed").length;
|
|
343
|
-
const failedCount = uploads.filter((u) => u.status === "failed").length;
|
|
344
|
-
|
|
345
|
-
return (
|
|
346
|
-
<div className="relative">
|
|
347
|
-
<button
|
|
348
|
-
onClick={() => setIsOpen(true)}
|
|
349
|
-
className="inline-flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
|
|
350
|
-
>
|
|
351
|
-
<svg
|
|
352
|
-
className="w-5 h-5"
|
|
353
|
-
fill="none"
|
|
354
|
-
stroke="currentColor"
|
|
355
|
-
viewBox="0 0 24 24"
|
|
356
|
-
>
|
|
357
|
-
<path
|
|
358
|
-
strokeLinecap="round"
|
|
359
|
-
strokeLinejoin="round"
|
|
360
|
-
strokeWidth={2}
|
|
361
|
-
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"
|
|
362
|
-
/>
|
|
363
|
-
</svg>
|
|
364
|
-
Upload
|
|
365
|
-
</button>
|
|
366
|
-
|
|
367
|
-
{isOpen && (
|
|
368
|
-
<div className="absolute right-0 top-full mt-2 w-96 bg-background rounded-lg shadow-xl p-6 z-50 border border-border">
|
|
369
|
-
<div className="flex items-center justify-between mb-4">
|
|
370
|
-
<h3 className="text-lg font-semibold text-foreground">
|
|
371
|
-
Upload Artifacts
|
|
372
|
-
</h3>
|
|
373
|
-
<button
|
|
374
|
-
onClick={handleClose}
|
|
375
|
-
className="text-muted-foreground hover:text-foreground"
|
|
376
|
-
aria-label="Close upload dialog"
|
|
377
|
-
>
|
|
378
|
-
<svg
|
|
379
|
-
className="w-6 h-6"
|
|
380
|
-
fill="none"
|
|
381
|
-
stroke="currentColor"
|
|
382
|
-
viewBox="0 0 24 24"
|
|
383
|
-
aria-hidden="true"
|
|
384
|
-
>
|
|
385
|
-
<path
|
|
386
|
-
strokeLinecap="round"
|
|
387
|
-
strokeLinejoin="round"
|
|
388
|
-
strokeWidth={2}
|
|
389
|
-
d="M6 18L18 6M6 6l12 12"
|
|
390
|
-
/>
|
|
391
|
-
</svg>
|
|
392
|
-
</button>
|
|
393
|
-
</div>
|
|
394
|
-
|
|
395
|
-
{/* Drag and drop zone */}
|
|
396
|
-
<div
|
|
397
|
-
onDrop={handleDrop}
|
|
398
|
-
onDragOver={handleDragOver}
|
|
399
|
-
onDragLeave={handleDragLeave}
|
|
400
|
-
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
|
401
|
-
dragActive
|
|
402
|
-
? "border-primary bg-primary/5"
|
|
403
|
-
: "border-border hover:border-border/80"
|
|
404
|
-
}`}
|
|
405
|
-
role="button"
|
|
406
|
-
tabIndex={0}
|
|
407
|
-
aria-label="Upload files by drag and drop or click to select"
|
|
408
|
-
onKeyDown={(e) => {
|
|
409
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
410
|
-
e.preventDefault();
|
|
411
|
-
fileInputRef.current?.click();
|
|
412
|
-
}
|
|
413
|
-
}}
|
|
414
|
-
>
|
|
415
|
-
<input
|
|
416
|
-
id="file-upload"
|
|
417
|
-
ref={fileInputRef}
|
|
418
|
-
type="file"
|
|
419
|
-
multiple
|
|
420
|
-
onChange={(e) => {
|
|
421
|
-
const files = Array.from(e.target.files || []);
|
|
422
|
-
if (files.length > 0) {
|
|
423
|
-
handleFilesUpload(files);
|
|
424
|
-
}
|
|
425
|
-
// Reset input so same files can be selected again
|
|
426
|
-
e.target.value = "";
|
|
427
|
-
}}
|
|
428
|
-
className="hidden"
|
|
429
|
-
accept="image/*,video/*,audio/*,text/*"
|
|
430
|
-
aria-label="Select files to upload"
|
|
431
|
-
aria-describedby="file-upload-description"
|
|
432
|
-
/>
|
|
433
|
-
|
|
434
|
-
<div className="flex flex-col items-center gap-2">
|
|
435
|
-
<svg
|
|
436
|
-
className="w-12 h-12 text-muted-foreground"
|
|
437
|
-
fill="none"
|
|
438
|
-
stroke="currentColor"
|
|
439
|
-
viewBox="0 0 24 24"
|
|
440
|
-
aria-hidden="true"
|
|
441
|
-
>
|
|
442
|
-
<path
|
|
443
|
-
strokeLinecap="round"
|
|
444
|
-
strokeLinejoin="round"
|
|
445
|
-
strokeWidth={2}
|
|
446
|
-
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
|
447
|
-
/>
|
|
448
|
-
</svg>
|
|
449
|
-
|
|
450
|
-
<div>
|
|
451
|
-
<button
|
|
452
|
-
type="button"
|
|
453
|
-
onClick={() => fileInputRef.current?.click()}
|
|
454
|
-
className="text-primary hover:text-primary/90 font-medium"
|
|
455
|
-
aria-label="Choose files to upload"
|
|
456
|
-
>
|
|
457
|
-
Choose files
|
|
458
|
-
</button>
|
|
459
|
-
<span className="text-muted-foreground"> or drag and drop here</span>
|
|
460
|
-
</div>
|
|
461
|
-
|
|
462
|
-
<p id="file-upload-description" className="text-sm text-muted-foreground">
|
|
463
|
-
Images, videos, audio, and text files (max 100MB each)
|
|
464
|
-
</p>
|
|
465
|
-
<p className="text-xs text-muted-foreground/80">
|
|
466
|
-
You can select multiple files at once
|
|
467
|
-
</p>
|
|
468
|
-
</div>
|
|
469
|
-
</div>
|
|
470
|
-
|
|
471
|
-
{/* URL input */}
|
|
472
|
-
<div className="mt-4">
|
|
473
|
-
<label className="block text-sm font-medium text-foreground mb-2">
|
|
474
|
-
Or paste a URL or image
|
|
475
|
-
</label>
|
|
476
|
-
<div className="flex gap-2">
|
|
477
|
-
<input
|
|
478
|
-
type="text"
|
|
479
|
-
value={urlInput}
|
|
480
|
-
onChange={(e) => setUrlInput(e.target.value)}
|
|
481
|
-
onPaste={handlePaste}
|
|
482
|
-
onKeyDown={(e) => {
|
|
483
|
-
if (e.key === "Enter") {
|
|
484
|
-
handleUrlUpload();
|
|
485
|
-
}
|
|
486
|
-
}}
|
|
487
|
-
placeholder="https://example.com/image.jpg or paste an image"
|
|
488
|
-
className="flex-1 px-4 py-2 border border-border rounded-lg bg-background focus:ring-2 focus:ring-primary focus:border-transparent"
|
|
489
|
-
disabled={isUploading}
|
|
490
|
-
/>
|
|
491
|
-
<button
|
|
492
|
-
onClick={handleUrlUpload}
|
|
493
|
-
disabled={!urlInput.trim() || isUploading}
|
|
494
|
-
className="px-6 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground disabled:cursor-not-allowed transition-colors"
|
|
495
|
-
>
|
|
496
|
-
Upload
|
|
497
|
-
</button>
|
|
498
|
-
</div>
|
|
499
|
-
</div>
|
|
500
|
-
|
|
501
|
-
{/* Upload progress list */}
|
|
502
|
-
{activeUploads.length > 0 && (
|
|
503
|
-
<div className="mt-4 space-y-2 max-h-48 overflow-y-auto" role="status" aria-live="polite">
|
|
504
|
-
{activeUploads.map((item) => (
|
|
505
|
-
<UploadItemRow
|
|
506
|
-
key={item.id}
|
|
507
|
-
item={item}
|
|
508
|
-
onCancel={() => cancelUpload(item.id)}
|
|
509
|
-
/>
|
|
510
|
-
))}
|
|
511
|
-
</div>
|
|
512
|
-
)}
|
|
513
|
-
|
|
514
|
-
{/* Overall progress bar */}
|
|
515
|
-
{isUploading && (
|
|
516
|
-
<div className="mt-4" role="status" aria-live="polite" aria-atomic="true">
|
|
517
|
-
<div className="flex items-center justify-between text-sm text-muted-foreground mb-2">
|
|
518
|
-
<span>
|
|
519
|
-
Uploading {activeUploads.length} file
|
|
520
|
-
{activeUploads.length !== 1 ? "s" : ""}...
|
|
521
|
-
</span>
|
|
522
|
-
<span>{Math.round(overallProgress)}%</span>
|
|
523
|
-
</div>
|
|
524
|
-
<div className="w-full bg-muted rounded-full h-2" role="progressbar" aria-valuenow={Math.round(overallProgress)} aria-valuemin={0} aria-valuemax={100}>
|
|
525
|
-
<div
|
|
526
|
-
className="bg-primary h-2 rounded-full transition-all duration-300"
|
|
527
|
-
style={{ width: `${overallProgress}%` }}
|
|
528
|
-
/>
|
|
529
|
-
</div>
|
|
530
|
-
</div>
|
|
531
|
-
)}
|
|
532
|
-
|
|
533
|
-
{/* Summary when uploads finish */}
|
|
534
|
-
{!isUploading && uploads.length > 0 && (
|
|
535
|
-
<div className="mt-4 p-3 bg-muted/50 rounded-lg" role="status" aria-live="polite">
|
|
536
|
-
<div className="flex items-center justify-between text-sm">
|
|
537
|
-
<span className="text-muted-foreground">
|
|
538
|
-
{completedCount > 0 && (
|
|
539
|
-
<span className="text-success">
|
|
540
|
-
{completedCount} completed
|
|
541
|
-
</span>
|
|
542
|
-
)}
|
|
543
|
-
{completedCount > 0 && failedCount > 0 && ", "}
|
|
544
|
-
{failedCount > 0 && (
|
|
545
|
-
<span className="text-red-600 dark:text-red-400">{failedCount} failed</span>
|
|
546
|
-
)}
|
|
547
|
-
</span>
|
|
548
|
-
<button
|
|
549
|
-
type="button"
|
|
550
|
-
onClick={clearUploads}
|
|
551
|
-
className="text-muted-foreground hover:text-foreground text-xs"
|
|
552
|
-
aria-label="Clear upload history"
|
|
553
|
-
>
|
|
554
|
-
Clear
|
|
555
|
-
</button>
|
|
556
|
-
</div>
|
|
557
|
-
</div>
|
|
558
|
-
)}
|
|
559
|
-
</div>
|
|
560
|
-
)}
|
|
561
|
-
</div>
|
|
562
|
-
);
|
|
563
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import Link from "next/link";
|
|
4
|
-
import {
|
|
5
|
-
NavigationMenu,
|
|
6
|
-
NavigationMenuItem,
|
|
7
|
-
NavigationMenuLink,
|
|
8
|
-
NavigationMenuList,
|
|
9
|
-
navigationMenuTriggerStyle,
|
|
10
|
-
} from "@/components/ui/navigation-menu";
|
|
11
|
-
import { ThemeToggle } from "@/components/theme-toggle";
|
|
12
|
-
|
|
13
|
-
export function Header() {
|
|
14
|
-
return (
|
|
15
|
-
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
16
|
-
<div className="container flex h-14 max-w-screen-2xl items-center justify-between">
|
|
17
|
-
<NavigationMenu>
|
|
18
|
-
<NavigationMenuList>
|
|
19
|
-
<NavigationMenuItem>
|
|
20
|
-
<Link href="/" legacyBehavior passHref>
|
|
21
|
-
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
|
|
22
|
-
Boards
|
|
23
|
-
</NavigationMenuLink>
|
|
24
|
-
</Link>
|
|
25
|
-
</NavigationMenuItem>
|
|
26
|
-
</NavigationMenuList>
|
|
27
|
-
</NavigationMenu>
|
|
28
|
-
<ThemeToggle />
|
|
29
|
-
</div>
|
|
30
|
-
</header>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
|
5
|
-
|
|
6
|
-
type ThemeProviderProps = React.ComponentProps<typeof NextThemesProvider>;
|
|
7
|
-
|
|
8
|
-
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
|
9
|
-
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
|
10
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import { Moon, Sun, Monitor } from "lucide-react";
|
|
5
|
-
import { useTheme } from "next-themes";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
DropdownMenu,
|
|
9
|
-
DropdownMenuContent,
|
|
10
|
-
DropdownMenuItem,
|
|
11
|
-
DropdownMenuTrigger,
|
|
12
|
-
} from "@/components/ui/dropdown-menu";
|
|
13
|
-
|
|
14
|
-
export function ThemeToggle() {
|
|
15
|
-
const { theme, setTheme } = useTheme();
|
|
16
|
-
const [mounted, setMounted] = React.useState(false);
|
|
17
|
-
|
|
18
|
-
// useEffect only runs on the client, so now we can safely show the UI
|
|
19
|
-
React.useEffect(() => {
|
|
20
|
-
setMounted(true);
|
|
21
|
-
}, []);
|
|
22
|
-
|
|
23
|
-
if (!mounted) {
|
|
24
|
-
return (
|
|
25
|
-
<button className="inline-flex items-center justify-center w-9 h-9 rounded-md hover:bg-accent hover:text-accent-foreground">
|
|
26
|
-
<Sun className="h-[1.2rem] w-[1.2rem]" />
|
|
27
|
-
<span className="sr-only">Toggle theme</span>
|
|
28
|
-
</button>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<DropdownMenu>
|
|
34
|
-
<DropdownMenuTrigger asChild>
|
|
35
|
-
<button className="inline-flex items-center justify-center w-9 h-9 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
|
36
|
-
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
|
37
|
-
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
|
38
|
-
<span className="sr-only">Toggle theme</span>
|
|
39
|
-
</button>
|
|
40
|
-
</DropdownMenuTrigger>
|
|
41
|
-
<DropdownMenuContent align="end">
|
|
42
|
-
<DropdownMenuItem
|
|
43
|
-
onClick={() => setTheme("light")}
|
|
44
|
-
className="cursor-pointer"
|
|
45
|
-
>
|
|
46
|
-
<Sun className="mr-2 h-4 w-4" />
|
|
47
|
-
<span>Light</span>
|
|
48
|
-
{theme === "light" && (
|
|
49
|
-
<span className="ml-auto text-xs">✓</span>
|
|
50
|
-
)}
|
|
51
|
-
</DropdownMenuItem>
|
|
52
|
-
<DropdownMenuItem
|
|
53
|
-
onClick={() => setTheme("dark")}
|
|
54
|
-
className="cursor-pointer"
|
|
55
|
-
>
|
|
56
|
-
<Moon className="mr-2 h-4 w-4" />
|
|
57
|
-
<span>Dark</span>
|
|
58
|
-
{theme === "dark" && (
|
|
59
|
-
<span className="ml-auto text-xs">✓</span>
|
|
60
|
-
)}
|
|
61
|
-
</DropdownMenuItem>
|
|
62
|
-
<DropdownMenuItem
|
|
63
|
-
onClick={() => setTheme("system")}
|
|
64
|
-
className="cursor-pointer"
|
|
65
|
-
>
|
|
66
|
-
<Monitor className="mr-2 h-4 w-4" />
|
|
67
|
-
<span>System</span>
|
|
68
|
-
{theme === "system" && (
|
|
69
|
-
<span className="ml-auto text-xs">✓</span>
|
|
70
|
-
)}
|
|
71
|
-
</DropdownMenuItem>
|
|
72
|
-
</DropdownMenuContent>
|
|
73
|
-
</DropdownMenu>
|
|
74
|
-
);
|
|
75
|
-
}
|