From 36a9e3a37c143ee919aecbf9a355798671932e7a Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Sun, 25 Jan 2026 13:25:51 +0100 Subject: [PATCH] feat: restore presi and storage apps from archive Re-activate presi (presentation tool) and storage (cloud storage) apps that were previously archived for context reduction. Co-Authored-By: Claude Opus 4.5 --- apps/presi/.gitignore | 41 + apps/presi/CLAUDE.md | 232 +++ apps/presi/README.md | 50 + apps/presi/apps/backend/drizzle.config.ts | 11 + apps/presi/apps/backend/eslint.config.mjs | 17 + apps/presi/apps/backend/nest-cli.json | 8 + apps/presi/apps/backend/package.json | 43 + apps/presi/apps/backend/src/app.module.ts | 24 + apps/presi/apps/backend/src/db/connection.ts | 39 + .../apps/backend/src/db/database.module.ts | 28 + .../backend/src/db/schema/decks.schema.ts | 25 + .../presi/apps/backend/src/db/schema/index.ts | 4 + .../src/db/schema/shared-decks.schema.ts | 20 + .../backend/src/db/schema/slides.schema.ts | 27 + .../backend/src/db/schema/themes.schema.ts | 24 + .../apps/backend/src/deck/deck.controller.ts | 41 + apps/presi/apps/backend/src/deck/deck.dto.ts | 32 + .../apps/backend/src/deck/deck.module.ts | 10 + .../apps/backend/src/deck/deck.service.ts | 113 ++ .../backend/src/health/health.controller.ts | 13 + .../apps/backend/src/health/health.module.ts | 7 + apps/presi/apps/backend/src/main.ts | 41 + .../backend/src/share/share.controller.ts | 40 + .../presi/apps/backend/src/share/share.dto.ts | 7 + .../apps/backend/src/share/share.module.ts | 12 + .../apps/backend/src/share/share.service.ts | 111 ++ .../backend/src/slide/slide.controller.ts | 40 + .../presi/apps/backend/src/slide/slide.dto.ts | 45 + .../apps/backend/src/slide/slide.module.ts | 11 + .../apps/backend/src/slide/slide.service.ts | 125 ++ .../backend/src/theme/theme.controller.ts | 22 + .../apps/backend/src/theme/theme.module.ts | 10 + .../apps/backend/src/theme/theme.service.ts | 27 + apps/presi/apps/backend/tsconfig.json | 24 + apps/presi/apps/landing/astro.config.mjs | 8 + apps/presi/apps/landing/package.json | 26 + apps/presi/apps/landing/public/favicon.svg | 10 + .../apps/landing/src/components/Footer.astro | 93 + .../landing/src/components/Navigation.astro | 39 + .../apps/landing/src/layouts/Layout.astro | 50 + apps/presi/apps/landing/src/pages/index.astro | 229 +++ apps/presi/apps/landing/src/styles/global.css | 101 ++ apps/presi/apps/landing/tailwind.config.mjs | 36 + apps/presi/apps/landing/tsconfig.json | 9 + apps/presi/apps/landing/wrangler.toml | 3 + apps/presi/apps/mobile/app.json | 49 + apps/presi/apps/mobile/app/(auth)/_layout.tsx | 33 + .../mobile/app/(auth)/forgot-password.tsx | 157 ++ apps/presi/apps/mobile/app/(auth)/login.tsx | 147 ++ .../presi/apps/mobile/app/(auth)/register.tsx | 165 ++ apps/presi/apps/mobile/app/_layout.tsx | 124 ++ apps/presi/apps/mobile/app/deck/[id].tsx | 399 +++++ apps/presi/apps/mobile/app/index.tsx | 418 +++++ apps/presi/apps/mobile/app/profile.tsx | 138 ++ apps/presi/apps/mobile/app/settings.tsx | 163 ++ apps/presi/apps/mobile/app/shared/[id].tsx | 94 + apps/presi/apps/mobile/app/themes.tsx | 127 ++ .../mobile/assets/fonts/SpaceMono-Regular.ttf | Bin 0 -> 93252 bytes .../mobile/assets/images/adaptive-icon.png | Bin 0 -> 17547 bytes .../apps/mobile/assets/images/favicon.png | Bin 0 -> 1466 bytes apps/presi/apps/mobile/assets/images/icon.png | Bin 0 -> 16353 bytes .../assets/images/partial-react-logo.png | Bin 0 -> 5075 bytes .../images/patterns/memo-theme-tile.png | Bin 0 -> 146901 bytes .../images/patterns/nature-theme-tile.png | Bin 0 -> 384735 bytes .../images/patterns/stone-theme-tile.png | Bin 0 -> 390611 bytes .../apps/mobile/assets/images/react-logo.png | Bin 0 -> 6341 bytes .../mobile/assets/images/react-logo@2x.png | Bin 0 -> 14225 bytes .../mobile/assets/images/react-logo@3x.png | Bin 0 -> 21252 bytes .../apps/mobile/assets/images/splash-icon.png | Bin 0 -> 17547 bytes .../apps/mobile/assets/images/splash.png | Bin 0 -> 29638 bytes .../apps/mobile/components/Menu/Header.tsx | 156 ++ .../apps/mobile/components/ThemeProvider.tsx | 162 ++ .../apps/mobile/components/atoms/Button.tsx | 69 + .../apps/mobile/components/atoms/Input.tsx | 59 + .../mobile/components/common/ContextMenu.tsx | 100 ++ .../components/common/CreateItemButton.tsx | 104 ++ .../components/common/ThemeSettings.tsx | 128 ++ .../apps/mobile/components/common/menu.tsx | 8 + .../components/decks/CreateDeckButton.tsx | 92 + .../apps/mobile/components/decks/DeckCard.tsx | 149 ++ .../apps/mobile/components/decks/DeckList.tsx | 443 +++++ .../components/decks/DeckShareSettings.tsx | 272 +++ .../components/forms/CreateDeckForm.tsx | 184 ++ .../presentation/PresentationMode.tsx | 319 ++++ .../mobile/components/slides/SlideEditor.tsx | 336 ++++ .../mobile/components/slides/SlideList.tsx | 426 +++++ .../mobile/components/slides/SlideView.tsx | 232 +++ apps/presi/apps/mobile/constants/theme.ts | 279 +++ apps/presi/apps/mobile/eas.json | 21 + apps/presi/apps/mobile/eslint.config.mjs | 12 + apps/presi/apps/mobile/metro.config.js | 6 + apps/presi/apps/mobile/package.json | 53 + apps/presi/apps/mobile/services/auth.ts | 49 + apps/presi/apps/mobile/services/firestore.ts | 293 +++ apps/presi/apps/mobile/services/storage.ts | 31 + apps/presi/apps/mobile/theme/README.md | 62 + .../presi/apps/mobile/theme/ThemeProvider.tsx | 191 ++ .../presi/apps/mobile/theme/ThemeSettings.tsx | 128 ++ apps/presi/apps/mobile/theme/constants.ts | 52 + apps/presi/apps/mobile/theme/index.ts | 3 + apps/presi/apps/mobile/tsconfig.json | 10 + apps/presi/apps/mobile/types/models.ts | 35 + apps/presi/apps/web/eslint.config.js | 17 + apps/presi/apps/web/package.json | 50 + apps/presi/apps/web/postcss.config.js | 5 + apps/presi/apps/web/src/app.css | 39 + apps/presi/apps/web/src/app.html | 12 + apps/presi/apps/web/src/lib/api/client.ts | 270 +++ apps/presi/apps/web/src/lib/api/feedback.ts | 15 + .../web/src/lib/components/AppSlider.svelte | 33 + .../lib/components/LanguageSelector.svelte | 19 + apps/presi/apps/web/src/lib/i18n/index.ts | 52 + .../apps/web/src/lib/i18n/locales/de.json | 13 + .../apps/web/src/lib/i18n/locales/en.json | 13 + .../apps/web/src/lib/i18n/locales/es.json | 13 + .../apps/web/src/lib/i18n/locales/fr.json | 13 + .../apps/web/src/lib/i18n/locales/it.json | 13 + .../apps/web/src/lib/stores/auth.svelte.ts | 191 ++ .../apps/web/src/lib/stores/decks.svelte.ts | 185 ++ .../apps/web/src/lib/stores/navigation.ts | 4 + apps/presi/apps/web/src/lib/stores/theme.ts | 10 + .../src/lib/stores/user-settings.svelte.ts | 19 + .../apps/web/src/routes/(app)/+layout.svelte | 250 +++ .../apps/web/src/routes/(app)/+page.svelte | 236 +++ .../web/src/routes/(app)/apps/+page.svelte | 14 + .../src/routes/(app)/deck/[id]/+page.svelte | 667 +++++++ .../src/routes/(app)/feedback/+page.svelte | 7 + .../web/src/routes/(app)/mana/+page.svelte | 39 + .../routes/(app)/present/[id]/+page.svelte | 310 ++++ .../web/src/routes/(app)/profile/+page.svelte | 136 ++ .../src/routes/(app)/settings/+page.svelte | 194 ++ .../routes/(app)/shared/[code]/+page.svelte | 314 ++++ .../web/src/routes/(app)/themes/+page.svelte | 19 + .../(auth)/forgot-password/+page.svelte | 41 + .../web/src/routes/(auth)/login/+page.svelte | 49 + .../src/routes/(auth)/register/+page.svelte | 42 + apps/presi/apps/web/src/routes/+layout.svelte | 39 + apps/presi/apps/web/svelte.config.js | 15 + apps/presi/apps/web/tsconfig.json | 14 + apps/presi/apps/web/vite.config.ts | 42 + apps/presi/package.json | 14 + apps/presi/packages/shared/package.json | 13 + apps/presi/packages/shared/src/index.ts | 6 + apps/presi/packages/shared/src/types/index.ts | 88 + apps/presi/packages/shared/tsconfig.json | 15 + apps/presi/pnpm-workspace.yaml | 3 + apps/storage/CLAUDE.md | 256 +++ apps/storage/apps/backend/drizzle.config.ts | 15 + apps/storage/apps/backend/eslint.config.mjs | 17 + apps/storage/apps/backend/nest-cli.json | 10 + apps/storage/apps/backend/package.json | 55 + apps/storage/apps/backend/src/app.module.ts | 29 + .../storage/apps/backend/src/db/connection.ts | 38 + .../apps/backend/src/db/database.module.ts | 30 + .../backend/src/db/schema/file-tags.schema.ts | 33 + .../src/db/schema/file-versions.schema.ts | 36 + .../backend/src/db/schema/files.schema.ts | 56 + .../backend/src/db/schema/folders.schema.ts | 41 + .../apps/backend/src/db/schema/index.ts | 17 + .../backend/src/db/schema/shares.schema.ts | 50 + .../apps/backend/src/db/schema/tags.schema.ts | 20 + .../backend/src/file/dto/create-file.dto.ts | 20 + .../apps/backend/src/file/file.controller.ts | 135 ++ .../apps/backend/src/file/file.module.ts | 18 + .../apps/backend/src/file/file.service.ts | 166 ++ .../src/folder/dto/create-folder.dto.ts | 20 + .../src/folder/dto/update-folder.dto.ts | 23 + .../backend/src/folder/folder.controller.ts | 69 + .../apps/backend/src/folder/folder.module.ts | 10 + .../apps/backend/src/folder/folder.service.ts | 147 ++ .../backend/src/health/health.controller.ts | 13 + .../apps/backend/src/health/health.module.ts | 7 + apps/storage/apps/backend/src/main.ts | 32 + .../backend/src/search/search.controller.ts | 23 + .../apps/backend/src/search/search.module.ts | 10 + .../apps/backend/src/search/search.service.ts | 57 + .../backend/src/share/share.controller.ts | 55 + .../apps/backend/src/share/share.module.ts | 10 + .../apps/backend/src/share/share.service.ts | 94 + .../backend/src/storage/storage.module.ts | 9 + .../backend/src/storage/storage.service.ts | 72 + .../apps/backend/src/tag/tag.controller.ts | 38 + .../apps/backend/src/tag/tag.module.ts | 10 + .../apps/backend/src/tag/tag.service.ts | 64 + .../backend/src/trash/trash.controller.ts | 47 + .../apps/backend/src/trash/trash.module.ts | 10 + .../apps/backend/src/trash/trash.service.ts | 107 ++ apps/storage/apps/backend/tsconfig.json | 25 + apps/storage/apps/web/eslint.config.js | 17 + apps/storage/apps/web/package.json | 47 + apps/storage/apps/web/src/app.css | 9 + apps/storage/apps/web/src/app.html | 13 + apps/storage/apps/web/src/lib/api/client.ts | 274 +++ .../lib/components/LanguageSelector.svelte | 19 + .../src/lib/components/ToastContainer.svelte | 188 ++ .../lib/components/files/Breadcrumb.svelte | 90 + .../src/lib/components/files/FileCard.svelte | 202 +++ .../src/lib/components/files/FileGrid.svelte | 49 + .../src/lib/components/files/FileList.svelte | 97 + .../src/lib/components/files/FileRow.svelte | 200 +++ .../lib/components/files/FolderCard.svelte | 179 ++ .../src/lib/components/files/FolderRow.svelte | 193 ++ .../components/files/NewFolderModal.svelte | 283 +++ .../lib/components/files/UploadZone.svelte | 157 ++ apps/storage/apps/web/src/lib/i18n/index.ts | 49 + .../apps/web/src/lib/i18n/locales/de.json | 77 + .../apps/web/src/lib/i18n/locales/en.json | 77 + .../apps/web/src/lib/stores/auth.svelte.ts | 159 ++ .../apps/web/src/lib/stores/files.svelte.ts | 173 ++ .../apps/web/src/lib/stores/navigation.ts | 8 + .../apps/web/src/lib/stores/theme.svelte.ts | 95 + apps/storage/apps/web/src/lib/stores/toast.ts | 63 + .../src/lib/stores/user-settings.svelte.ts | 19 + .../apps/web/src/routes/+layout.svelte | 276 +++ apps/storage/apps/web/src/routes/+page.svelte | 13 + .../web/src/routes/favorites/+page.svelte | 237 +++ .../apps/web/src/routes/feedback/+page.svelte | 223 +++ .../apps/web/src/routes/files/+page.svelte | 417 +++++ .../src/routes/files/[folderId]/+page.svelte | 457 +++++ .../src/routes/forgot-password/+page.svelte | 36 + .../apps/web/src/routes/login/+page.svelte | 38 + .../apps/web/src/routes/profile/+page.svelte | 132 ++ .../apps/web/src/routes/register/+page.svelte | 37 + .../apps/web/src/routes/search/+page.svelte | 285 +++ .../apps/web/src/routes/settings/+page.svelte | 124 ++ .../apps/web/src/routes/shared/+page.svelte | 337 ++++ .../apps/web/src/routes/themes/+page.svelte | 135 ++ .../apps/web/src/routes/trash/+page.svelte | 359 ++++ apps/storage/apps/web/svelte.config.js | 12 + apps/storage/apps/web/tsconfig.json | 14 + apps/storage/apps/web/vite.config.ts | 44 + apps/storage/package.json | 9 + apps/storage/packages/shared/package.json | 17 + apps/storage/packages/shared/src/index.ts | 1 + .../packages/shared/src/types/index.ts | 72 + apps/storage/packages/shared/tsconfig.json | 16 + pnpm-lock.yaml | 1577 ++++++++++++++++- 237 files changed, 21521 insertions(+), 37 deletions(-) create mode 100644 apps/presi/.gitignore create mode 100644 apps/presi/CLAUDE.md create mode 100644 apps/presi/README.md create mode 100644 apps/presi/apps/backend/drizzle.config.ts create mode 100644 apps/presi/apps/backend/eslint.config.mjs create mode 100644 apps/presi/apps/backend/nest-cli.json create mode 100644 apps/presi/apps/backend/package.json create mode 100644 apps/presi/apps/backend/src/app.module.ts create mode 100644 apps/presi/apps/backend/src/db/connection.ts create mode 100644 apps/presi/apps/backend/src/db/database.module.ts create mode 100644 apps/presi/apps/backend/src/db/schema/decks.schema.ts create mode 100644 apps/presi/apps/backend/src/db/schema/index.ts create mode 100644 apps/presi/apps/backend/src/db/schema/shared-decks.schema.ts create mode 100644 apps/presi/apps/backend/src/db/schema/slides.schema.ts create mode 100644 apps/presi/apps/backend/src/db/schema/themes.schema.ts create mode 100644 apps/presi/apps/backend/src/deck/deck.controller.ts create mode 100644 apps/presi/apps/backend/src/deck/deck.dto.ts create mode 100644 apps/presi/apps/backend/src/deck/deck.module.ts create mode 100644 apps/presi/apps/backend/src/deck/deck.service.ts create mode 100644 apps/presi/apps/backend/src/health/health.controller.ts create mode 100644 apps/presi/apps/backend/src/health/health.module.ts create mode 100644 apps/presi/apps/backend/src/main.ts create mode 100644 apps/presi/apps/backend/src/share/share.controller.ts create mode 100644 apps/presi/apps/backend/src/share/share.dto.ts create mode 100644 apps/presi/apps/backend/src/share/share.module.ts create mode 100644 apps/presi/apps/backend/src/share/share.service.ts create mode 100644 apps/presi/apps/backend/src/slide/slide.controller.ts create mode 100644 apps/presi/apps/backend/src/slide/slide.dto.ts create mode 100644 apps/presi/apps/backend/src/slide/slide.module.ts create mode 100644 apps/presi/apps/backend/src/slide/slide.service.ts create mode 100644 apps/presi/apps/backend/src/theme/theme.controller.ts create mode 100644 apps/presi/apps/backend/src/theme/theme.module.ts create mode 100644 apps/presi/apps/backend/src/theme/theme.service.ts create mode 100644 apps/presi/apps/backend/tsconfig.json create mode 100644 apps/presi/apps/landing/astro.config.mjs create mode 100644 apps/presi/apps/landing/package.json create mode 100644 apps/presi/apps/landing/public/favicon.svg create mode 100644 apps/presi/apps/landing/src/components/Footer.astro create mode 100644 apps/presi/apps/landing/src/components/Navigation.astro create mode 100644 apps/presi/apps/landing/src/layouts/Layout.astro create mode 100644 apps/presi/apps/landing/src/pages/index.astro create mode 100644 apps/presi/apps/landing/src/styles/global.css create mode 100644 apps/presi/apps/landing/tailwind.config.mjs create mode 100644 apps/presi/apps/landing/tsconfig.json create mode 100644 apps/presi/apps/landing/wrangler.toml create mode 100644 apps/presi/apps/mobile/app.json create mode 100644 apps/presi/apps/mobile/app/(auth)/_layout.tsx create mode 100644 apps/presi/apps/mobile/app/(auth)/forgot-password.tsx create mode 100644 apps/presi/apps/mobile/app/(auth)/login.tsx create mode 100644 apps/presi/apps/mobile/app/(auth)/register.tsx create mode 100644 apps/presi/apps/mobile/app/_layout.tsx create mode 100644 apps/presi/apps/mobile/app/deck/[id].tsx create mode 100644 apps/presi/apps/mobile/app/index.tsx create mode 100644 apps/presi/apps/mobile/app/profile.tsx create mode 100644 apps/presi/apps/mobile/app/settings.tsx create mode 100644 apps/presi/apps/mobile/app/shared/[id].tsx create mode 100644 apps/presi/apps/mobile/app/themes.tsx create mode 100755 apps/presi/apps/mobile/assets/fonts/SpaceMono-Regular.ttf create mode 100644 apps/presi/apps/mobile/assets/images/adaptive-icon.png create mode 100644 apps/presi/apps/mobile/assets/images/favicon.png create mode 100644 apps/presi/apps/mobile/assets/images/icon.png create mode 100644 apps/presi/apps/mobile/assets/images/partial-react-logo.png create mode 100644 apps/presi/apps/mobile/assets/images/patterns/memo-theme-tile.png create mode 100644 apps/presi/apps/mobile/assets/images/patterns/nature-theme-tile.png create mode 100644 apps/presi/apps/mobile/assets/images/patterns/stone-theme-tile.png create mode 100644 apps/presi/apps/mobile/assets/images/react-logo.png create mode 100644 apps/presi/apps/mobile/assets/images/react-logo@2x.png create mode 100644 apps/presi/apps/mobile/assets/images/react-logo@3x.png create mode 100644 apps/presi/apps/mobile/assets/images/splash-icon.png create mode 100644 apps/presi/apps/mobile/assets/images/splash.png create mode 100644 apps/presi/apps/mobile/components/Menu/Header.tsx create mode 100644 apps/presi/apps/mobile/components/ThemeProvider.tsx create mode 100644 apps/presi/apps/mobile/components/atoms/Button.tsx create mode 100644 apps/presi/apps/mobile/components/atoms/Input.tsx create mode 100644 apps/presi/apps/mobile/components/common/ContextMenu.tsx create mode 100644 apps/presi/apps/mobile/components/common/CreateItemButton.tsx create mode 100644 apps/presi/apps/mobile/components/common/ThemeSettings.tsx create mode 100644 apps/presi/apps/mobile/components/common/menu.tsx create mode 100644 apps/presi/apps/mobile/components/decks/CreateDeckButton.tsx create mode 100644 apps/presi/apps/mobile/components/decks/DeckCard.tsx create mode 100644 apps/presi/apps/mobile/components/decks/DeckList.tsx create mode 100644 apps/presi/apps/mobile/components/decks/DeckShareSettings.tsx create mode 100644 apps/presi/apps/mobile/components/forms/CreateDeckForm.tsx create mode 100644 apps/presi/apps/mobile/components/presentation/PresentationMode.tsx create mode 100644 apps/presi/apps/mobile/components/slides/SlideEditor.tsx create mode 100644 apps/presi/apps/mobile/components/slides/SlideList.tsx create mode 100644 apps/presi/apps/mobile/components/slides/SlideView.tsx create mode 100644 apps/presi/apps/mobile/constants/theme.ts create mode 100644 apps/presi/apps/mobile/eas.json create mode 100644 apps/presi/apps/mobile/eslint.config.mjs create mode 100644 apps/presi/apps/mobile/metro.config.js create mode 100644 apps/presi/apps/mobile/package.json create mode 100644 apps/presi/apps/mobile/services/auth.ts create mode 100644 apps/presi/apps/mobile/services/firestore.ts create mode 100644 apps/presi/apps/mobile/services/storage.ts create mode 100644 apps/presi/apps/mobile/theme/README.md create mode 100644 apps/presi/apps/mobile/theme/ThemeProvider.tsx create mode 100644 apps/presi/apps/mobile/theme/ThemeSettings.tsx create mode 100644 apps/presi/apps/mobile/theme/constants.ts create mode 100644 apps/presi/apps/mobile/theme/index.ts create mode 100644 apps/presi/apps/mobile/tsconfig.json create mode 100644 apps/presi/apps/mobile/types/models.ts create mode 100644 apps/presi/apps/web/eslint.config.js create mode 100644 apps/presi/apps/web/package.json create mode 100644 apps/presi/apps/web/postcss.config.js create mode 100644 apps/presi/apps/web/src/app.css create mode 100644 apps/presi/apps/web/src/app.html create mode 100644 apps/presi/apps/web/src/lib/api/client.ts create mode 100644 apps/presi/apps/web/src/lib/api/feedback.ts create mode 100644 apps/presi/apps/web/src/lib/components/AppSlider.svelte create mode 100644 apps/presi/apps/web/src/lib/components/LanguageSelector.svelte create mode 100644 apps/presi/apps/web/src/lib/i18n/index.ts create mode 100644 apps/presi/apps/web/src/lib/i18n/locales/de.json create mode 100644 apps/presi/apps/web/src/lib/i18n/locales/en.json create mode 100644 apps/presi/apps/web/src/lib/i18n/locales/es.json create mode 100644 apps/presi/apps/web/src/lib/i18n/locales/fr.json create mode 100644 apps/presi/apps/web/src/lib/i18n/locales/it.json create mode 100644 apps/presi/apps/web/src/lib/stores/auth.svelte.ts create mode 100644 apps/presi/apps/web/src/lib/stores/decks.svelte.ts create mode 100644 apps/presi/apps/web/src/lib/stores/navigation.ts create mode 100644 apps/presi/apps/web/src/lib/stores/theme.ts create mode 100644 apps/presi/apps/web/src/lib/stores/user-settings.svelte.ts create mode 100644 apps/presi/apps/web/src/routes/(app)/+layout.svelte create mode 100644 apps/presi/apps/web/src/routes/(app)/+page.svelte create mode 100644 apps/presi/apps/web/src/routes/(app)/apps/+page.svelte create mode 100644 apps/presi/apps/web/src/routes/(app)/deck/[id]/+page.svelte create mode 100644 apps/presi/apps/web/src/routes/(app)/feedback/+page.svelte create mode 100644 apps/presi/apps/web/src/routes/(app)/mana/+page.svelte create mode 100644 apps/presi/apps/web/src/routes/(app)/present/[id]/+page.svelte create mode 100644 apps/presi/apps/web/src/routes/(app)/profile/+page.svelte create mode 100644 apps/presi/apps/web/src/routes/(app)/settings/+page.svelte create mode 100644 apps/presi/apps/web/src/routes/(app)/shared/[code]/+page.svelte create mode 100644 apps/presi/apps/web/src/routes/(app)/themes/+page.svelte create mode 100644 apps/presi/apps/web/src/routes/(auth)/forgot-password/+page.svelte create mode 100644 apps/presi/apps/web/src/routes/(auth)/login/+page.svelte create mode 100644 apps/presi/apps/web/src/routes/(auth)/register/+page.svelte create mode 100644 apps/presi/apps/web/src/routes/+layout.svelte create mode 100644 apps/presi/apps/web/svelte.config.js create mode 100644 apps/presi/apps/web/tsconfig.json create mode 100644 apps/presi/apps/web/vite.config.ts create mode 100644 apps/presi/package.json create mode 100644 apps/presi/packages/shared/package.json create mode 100644 apps/presi/packages/shared/src/index.ts create mode 100644 apps/presi/packages/shared/src/types/index.ts create mode 100644 apps/presi/packages/shared/tsconfig.json create mode 100644 apps/presi/pnpm-workspace.yaml create mode 100644 apps/storage/CLAUDE.md create mode 100644 apps/storage/apps/backend/drizzle.config.ts create mode 100644 apps/storage/apps/backend/eslint.config.mjs create mode 100644 apps/storage/apps/backend/nest-cli.json create mode 100644 apps/storage/apps/backend/package.json create mode 100644 apps/storage/apps/backend/src/app.module.ts create mode 100644 apps/storage/apps/backend/src/db/connection.ts create mode 100644 apps/storage/apps/backend/src/db/database.module.ts create mode 100644 apps/storage/apps/backend/src/db/schema/file-tags.schema.ts create mode 100644 apps/storage/apps/backend/src/db/schema/file-versions.schema.ts create mode 100644 apps/storage/apps/backend/src/db/schema/files.schema.ts create mode 100644 apps/storage/apps/backend/src/db/schema/folders.schema.ts create mode 100644 apps/storage/apps/backend/src/db/schema/index.ts create mode 100644 apps/storage/apps/backend/src/db/schema/shares.schema.ts create mode 100644 apps/storage/apps/backend/src/db/schema/tags.schema.ts create mode 100644 apps/storage/apps/backend/src/file/dto/create-file.dto.ts create mode 100644 apps/storage/apps/backend/src/file/file.controller.ts create mode 100644 apps/storage/apps/backend/src/file/file.module.ts create mode 100644 apps/storage/apps/backend/src/file/file.service.ts create mode 100644 apps/storage/apps/backend/src/folder/dto/create-folder.dto.ts create mode 100644 apps/storage/apps/backend/src/folder/dto/update-folder.dto.ts create mode 100644 apps/storage/apps/backend/src/folder/folder.controller.ts create mode 100644 apps/storage/apps/backend/src/folder/folder.module.ts create mode 100644 apps/storage/apps/backend/src/folder/folder.service.ts create mode 100644 apps/storage/apps/backend/src/health/health.controller.ts create mode 100644 apps/storage/apps/backend/src/health/health.module.ts create mode 100644 apps/storage/apps/backend/src/main.ts create mode 100644 apps/storage/apps/backend/src/search/search.controller.ts create mode 100644 apps/storage/apps/backend/src/search/search.module.ts create mode 100644 apps/storage/apps/backend/src/search/search.service.ts create mode 100644 apps/storage/apps/backend/src/share/share.controller.ts create mode 100644 apps/storage/apps/backend/src/share/share.module.ts create mode 100644 apps/storage/apps/backend/src/share/share.service.ts create mode 100644 apps/storage/apps/backend/src/storage/storage.module.ts create mode 100644 apps/storage/apps/backend/src/storage/storage.service.ts create mode 100644 apps/storage/apps/backend/src/tag/tag.controller.ts create mode 100644 apps/storage/apps/backend/src/tag/tag.module.ts create mode 100644 apps/storage/apps/backend/src/tag/tag.service.ts create mode 100644 apps/storage/apps/backend/src/trash/trash.controller.ts create mode 100644 apps/storage/apps/backend/src/trash/trash.module.ts create mode 100644 apps/storage/apps/backend/src/trash/trash.service.ts create mode 100644 apps/storage/apps/backend/tsconfig.json create mode 100644 apps/storage/apps/web/eslint.config.js create mode 100644 apps/storage/apps/web/package.json create mode 100644 apps/storage/apps/web/src/app.css create mode 100644 apps/storage/apps/web/src/app.html create mode 100644 apps/storage/apps/web/src/lib/api/client.ts create mode 100644 apps/storage/apps/web/src/lib/components/LanguageSelector.svelte create mode 100644 apps/storage/apps/web/src/lib/components/ToastContainer.svelte create mode 100644 apps/storage/apps/web/src/lib/components/files/Breadcrumb.svelte create mode 100644 apps/storage/apps/web/src/lib/components/files/FileCard.svelte create mode 100644 apps/storage/apps/web/src/lib/components/files/FileGrid.svelte create mode 100644 apps/storage/apps/web/src/lib/components/files/FileList.svelte create mode 100644 apps/storage/apps/web/src/lib/components/files/FileRow.svelte create mode 100644 apps/storage/apps/web/src/lib/components/files/FolderCard.svelte create mode 100644 apps/storage/apps/web/src/lib/components/files/FolderRow.svelte create mode 100644 apps/storage/apps/web/src/lib/components/files/NewFolderModal.svelte create mode 100644 apps/storage/apps/web/src/lib/components/files/UploadZone.svelte create mode 100644 apps/storage/apps/web/src/lib/i18n/index.ts create mode 100644 apps/storage/apps/web/src/lib/i18n/locales/de.json create mode 100644 apps/storage/apps/web/src/lib/i18n/locales/en.json create mode 100644 apps/storage/apps/web/src/lib/stores/auth.svelte.ts create mode 100644 apps/storage/apps/web/src/lib/stores/files.svelte.ts create mode 100644 apps/storage/apps/web/src/lib/stores/navigation.ts create mode 100644 apps/storage/apps/web/src/lib/stores/theme.svelte.ts create mode 100644 apps/storage/apps/web/src/lib/stores/toast.ts create mode 100644 apps/storage/apps/web/src/lib/stores/user-settings.svelte.ts create mode 100644 apps/storage/apps/web/src/routes/+layout.svelte create mode 100644 apps/storage/apps/web/src/routes/+page.svelte create mode 100644 apps/storage/apps/web/src/routes/favorites/+page.svelte create mode 100644 apps/storage/apps/web/src/routes/feedback/+page.svelte create mode 100644 apps/storage/apps/web/src/routes/files/+page.svelte create mode 100644 apps/storage/apps/web/src/routes/files/[folderId]/+page.svelte create mode 100644 apps/storage/apps/web/src/routes/forgot-password/+page.svelte create mode 100644 apps/storage/apps/web/src/routes/login/+page.svelte create mode 100644 apps/storage/apps/web/src/routes/profile/+page.svelte create mode 100644 apps/storage/apps/web/src/routes/register/+page.svelte create mode 100644 apps/storage/apps/web/src/routes/search/+page.svelte create mode 100644 apps/storage/apps/web/src/routes/settings/+page.svelte create mode 100644 apps/storage/apps/web/src/routes/shared/+page.svelte create mode 100644 apps/storage/apps/web/src/routes/themes/+page.svelte create mode 100644 apps/storage/apps/web/src/routes/trash/+page.svelte create mode 100644 apps/storage/apps/web/svelte.config.js create mode 100644 apps/storage/apps/web/tsconfig.json create mode 100644 apps/storage/apps/web/vite.config.ts create mode 100644 apps/storage/package.json create mode 100644 apps/storage/packages/shared/package.json create mode 100644 apps/storage/packages/shared/src/index.ts create mode 100644 apps/storage/packages/shared/src/types/index.ts create mode 100644 apps/storage/packages/shared/tsconfig.json diff --git a/apps/presi/.gitignore b/apps/presi/.gitignore new file mode 100644 index 000000000..7072e8033 --- /dev/null +++ b/apps/presi/.gitignore @@ -0,0 +1,41 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +app-example + +# Local Netlify folder +.netlify diff --git a/apps/presi/CLAUDE.md b/apps/presi/CLAUDE.md new file mode 100644 index 000000000..3218f9250 --- /dev/null +++ b/apps/presi/CLAUDE.md @@ -0,0 +1,232 @@ +# Presi Project Guide + +## Project Structure + +``` +apps/presi/ +├── apps/ +│ ├── backend/ # NestJS API server (@presi/backend) +│ ├── mobile/ # Expo/React Native mobile app (@presi/mobile) +│ ├── web/ # SvelteKit web application (@presi/web) +│ └── landing/ # Astro marketing landing page (@presi/landing) - TODO +├── packages/ +│ └── shared/ # Shared types and utils (@presi/shared) +└── package.json +``` + +## Commands + +### Root Level (from monorepo root) + +```bash +pnpm presi:dev # Run all presi apps +pnpm dev:presi:mobile # Start mobile app +pnpm dev:presi:web # Start web app (port 5178) +pnpm dev:presi:backend # Start backend server +pnpm dev:presi:app # Start web + backend together +pnpm presi:db:push # Push schema to database +pnpm presi:db:studio # Open Drizzle Studio +pnpm presi:db:seed # Seed database with sample data +``` + +### Mobile App (apps/presi/apps/mobile) + +```bash +pnpm dev # Start Expo dev server +pnpm ios # Run on iOS simulator +pnpm android # Run on Android emulator +``` + +### Web App (apps/presi/apps/web) + +```bash +pnpm dev # Start dev server (port 5178) +pnpm build # Build for production +pnpm preview # Preview production build +pnpm check # Run svelte-check +``` + +### Backend (apps/presi/apps/backend) + +```bash +pnpm dev # Start with hot reload +pnpm build # Build for production +pnpm start:prod # Start production server +pnpm db:push # Push schema to database +pnpm db:studio # Open Drizzle Studio +pnpm db:seed # Seed database +``` + +## Technology Stack + +- **Mobile**: React Native 0.76 + Expo SDK 52, Expo Router, Zustand +- **Web**: SvelteKit 2.x, Svelte 5 (runes mode), Tailwind CSS +- **Backend**: NestJS 10, Drizzle ORM, PostgreSQL +- **Types**: TypeScript 5.x + +## Architecture + +### Core Features + +- Create and manage presentation decks +- Add and edit slides with various content types +- Apply themes to presentations +- Share decks via share codes +- Present slides in full-screen mode + +### Backend API Endpoints + +| Endpoint | Method | Auth | Description | +| --------------------------- | ------ | ---- | ------------------------ | +| `/api/health` | GET | No | Health check | +| `/api/decks` | GET | Yes | Get user's decks | +| `/api/decks` | POST | Yes | Create new deck | +| `/api/decks/:id` | GET | Yes | Get deck details | +| `/api/decks/:id` | PUT | Yes | Update deck | +| `/api/decks/:id` | DELETE | Yes | Delete deck | +| `/api/decks/:id/slides` | GET | Yes | Get slides for deck | +| `/api/decks/:id/slides` | POST | Yes | Add slide to deck | +| `/api/slides/:id` | PUT | Yes | Update slide | +| `/api/slides/:id` | DELETE | Yes | Delete slide | +| `/api/slides/reorder` | PUT | Yes | Reorder slides | +| `/api/share/:code` | GET | No | Get shared deck (public) | +| `/api/share/deck/:id` | POST | Yes | Create share link | +| `/api/share/deck/:id/links` | GET | Yes | Get share links for deck | +| `/api/share/:shareId` | DELETE | Yes | Delete share link | + +### Data Models + +**Deck** - Presentation deck + +- `id` (string) - Unique identifier +- `userId` (string) - Owner user ID +- `title` (string) - Deck title +- `description` (string?) - Optional description +- `themeId` (string?) - Theme reference +- `isPublic` (boolean) - Visibility flag +- `createdAt` / `updatedAt` (timestamps) + +**Slide** - Individual slide in a deck + +- `id` (string) - Unique identifier +- `deckId` (string) - Parent deck reference +- `order` (number) - Position in deck +- `content` (SlideContent) - Slide content +- `createdAt` (timestamp) + +**SlideContent** - Content structure + +- `type`: 'title' | 'content' | 'image' | 'split' +- `title`, `subtitle`, `body`, `imageUrl`, `bulletPoints` + +**Theme** - Visual theme + +- `id`, `name`, `colors`, `fonts`, `isDefault` + +**SharedDeck** - Share link for deck + +- `id` (string) - Unique identifier +- `deckId` (string) - Reference to deck +- `shareCode` (string) - Unique share code (12 chars) +- `expiresAt` (timestamp?) - Optional expiration +- `createdAt` (timestamp) + +### Environment Variables + +#### Backend (.env) + +``` +NODE_ENV=development +PORT=3008 +DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/presi +MANA_CORE_AUTH_URL=http://localhost:3001 +CORS_ORIGINS=http://localhost:5173,http://localhost:8081 +``` + +#### Mobile (.env) + +``` +EXPO_PUBLIC_BACKEND_URL=http://localhost:3008 +EXPO_PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001 +``` + +#### Web (.env) + +``` +PUBLIC_BACKEND_URL=http://localhost:3008 +PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001 +``` + +## Shared Package + +### @presi/shared + +Located at `packages/shared/` + +**Types:** + +- `Deck`, `Slide`, `SlideContent` +- `Theme`, `ThemeColors`, `ThemeFonts` +- `SharedDeck` (for sharing feature) + +**DTOs:** + +- `CreateDeckDto`, `UpdateDeckDto` +- `CreateSlideDto`, `UpdateSlideDto` +- `ReorderSlidesDto` + +## Code Style Guidelines + +- **TypeScript**: Strict typing with interfaces +- **Mobile**: Functional components with hooks, Zustand for state +- **Web**: Svelte 5 runes mode (`$state`, `$derived`, `$effect`) +- **Backend**: NestJS modules with controllers and services +- **Styling**: Tailwind CSS (Web), NativeWind (Mobile) +- **Formatting**: Prettier with project config + +## Web App Features + +The SvelteKit web app provides feature parity with the mobile app: + +- **Authentication**: Login/Register/Forgot Password with Mana Core Auth +- **Deck Management**: Create, edit, delete presentation decks +- **Slide Editor**: Create slides with title, body, bullet points, images +- **Presentation Mode**: Fullscreen presentation with keyboard navigation + - Arrow keys / A/D for navigation + - F for fullscreen toggle + - ESC to exit + - Timer with start/pause + - Speaker notes toggle +- **Sharing**: Create share links for decks, public view without auth +- **Profile**: View user info and deck statistics +- **Settings**: Theme switching (light/dark/system), account info + +### Web App Structure + +``` +src/ +├── lib/ +│ ├── api/client.ts # API client with auth +│ └── stores/ +│ ├── auth.svelte.ts # Auth state (Svelte 5 runes) +│ └── decks.svelte.ts # Decks/slides state +├── routes/ +│ ├── +layout.svelte # App layout with header +│ ├── +page.svelte # Deck list (home) +│ ├── login/ # Login page +│ ├── register/ # Register page +│ ├── forgot-password/ # Password reset page +│ ├── deck/[id]/ # Deck editor with slides +│ ├── present/[id]/ # Presentation mode +│ ├── shared/[code]/ # Public shared deck view +│ ├── profile/ # User profile page +│ └── settings/ # Settings page +└── app.css # Global styles +``` + +## Important Notes + +1. **Authentication**: Uses Mana Core Auth (JWT in Authorization header) +2. **Database**: PostgreSQL with Drizzle ORM +3. **Ports**: Backend=3008, Web=5178 +4. **Landing**: Not yet implemented (empty folder) diff --git a/apps/presi/README.md b/apps/presi/README.md new file mode 100644 index 000000000..cd4feb8a3 --- /dev/null +++ b/apps/presi/README.md @@ -0,0 +1,50 @@ +# Welcome to your Expo app 👋 + +This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). + +## Get started + +1. Install dependencies + + ```bash + npm install + ``` + +2. Start the app + + ```bash + npx expo start + ``` + +In the output, you'll find options to open the app in a + +- [development build](https://docs.expo.dev/develop/development-builds/introduction/) +- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) +- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) +- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo + +You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). + +## Get a fresh project + +When you're ready, run: + +```bash +npm run reset-project +``` + +This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. + +## Learn more + +To learn more about developing your project with Expo, look at the following resources: + +- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). +- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. + +## Join the community + +Join our community of developers creating universal apps. + +- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. +- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. diff --git a/apps/presi/apps/backend/drizzle.config.ts b/apps/presi/apps/backend/drizzle.config.ts new file mode 100644 index 000000000..cdfb20e43 --- /dev/null +++ b/apps/presi/apps/backend/drizzle.config.ts @@ -0,0 +1,11 @@ +import 'dotenv/config'; +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/db/schema/index.ts', + out: './src/db/migrations', + dialect: 'postgresql', + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}); diff --git a/apps/presi/apps/backend/eslint.config.mjs b/apps/presi/apps/backend/eslint.config.mjs new file mode 100644 index 000000000..41ef245c0 --- /dev/null +++ b/apps/presi/apps/backend/eslint.config.mjs @@ -0,0 +1,17 @@ +// @ts-check +import { + baseConfig, + typescriptConfig, + nestjsConfig, + prettierConfig, +} from '@manacore/eslint-config'; + +export default [ + { + ignores: ['dist/**', 'node_modules/**'], + }, + ...baseConfig, + ...typescriptConfig, + ...nestjsConfig, + ...prettierConfig, +]; diff --git a/apps/presi/apps/backend/nest-cli.json b/apps/presi/apps/backend/nest-cli.json new file mode 100644 index 000000000..95538fb90 --- /dev/null +++ b/apps/presi/apps/backend/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/apps/presi/apps/backend/package.json b/apps/presi/apps/backend/package.json new file mode 100644 index 000000000..3526aceba --- /dev/null +++ b/apps/presi/apps/backend/package.json @@ -0,0 +1,43 @@ +{ + "name": "@presi/backend", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "nest build", + "start": "nest start", + "dev": "nest start --watch", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "type-check": "tsc --noEmit", + "db:push": "drizzle-kit push", + "db:studio": "drizzle-kit studio", + "db:seed": "tsx src/db/seed.ts" + }, + "dependencies": { + "@manacore/shared-nestjs-auth": "workspace:*", + "@nestjs/common": "^10.4.15", + "@nestjs/config": "^3.3.0", + "@nestjs/core": "^10.4.15", + "@nestjs/platform-express": "^10.4.15", + "@presi/shared": "workspace:*", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "dotenv": "^16.4.7", + "drizzle-kit": "^0.30.2", + "drizzle-orm": "^0.38.3", + "postgres": "^3.4.5", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "nanoid": "^5.0.9" + }, + "devDependencies": { + "@nestjs/cli": "^10.4.9", + "@nestjs/schematics": "^10.2.3", + "@types/express": "^5.0.0", + "@types/node": "^22.10.2", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } +} diff --git a/apps/presi/apps/backend/src/app.module.ts b/apps/presi/apps/backend/src/app.module.ts new file mode 100644 index 000000000..dca08dc88 --- /dev/null +++ b/apps/presi/apps/backend/src/app.module.ts @@ -0,0 +1,24 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { DatabaseModule } from './db/database.module'; +import { DeckModule } from './deck/deck.module'; +import { SlideModule } from './slide/slide.module'; +import { ThemeModule } from './theme/theme.module'; +import { ShareModule } from './share/share.module'; +import { HealthModule } from './health/health.module'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + envFilePath: '.env', + }), + DatabaseModule, + DeckModule, + SlideModule, + ThemeModule, + ShareModule, + HealthModule, + ], +}) +export class AppModule {} diff --git a/apps/presi/apps/backend/src/db/connection.ts b/apps/presi/apps/backend/src/db/connection.ts new file mode 100644 index 000000000..5c4dbb304 --- /dev/null +++ b/apps/presi/apps/backend/src/db/connection.ts @@ -0,0 +1,39 @@ +import { drizzle } from 'drizzle-orm/postgres-js'; +import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import * as schema from './schema'; + +// Use require for postgres to avoid ESM/CommonJS interop issues + +const postgres = require('postgres'); + +let connection: ReturnType | null = null; +let db: PostgresJsDatabase | null = null; + +export function getConnection(databaseUrl: string) { + if (!connection) { + connection = postgres(databaseUrl, { + max: 10, + idle_timeout: 20, + connect_timeout: 10, + }); + } + return connection; +} + +export function getDb(databaseUrl: string): PostgresJsDatabase { + if (!db) { + const conn = getConnection(databaseUrl); + db = drizzle(conn, { schema }); + } + return db; +} + +export async function closeConnection() { + if (connection) { + await connection.end(); + connection = null; + db = null; + } +} + +export type Database = PostgresJsDatabase; diff --git a/apps/presi/apps/backend/src/db/database.module.ts b/apps/presi/apps/backend/src/db/database.module.ts new file mode 100644 index 000000000..b4d1f2af6 --- /dev/null +++ b/apps/presi/apps/backend/src/db/database.module.ts @@ -0,0 +1,28 @@ +import { Module, Global, OnModuleDestroy } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { getDb, closeConnection, type Database } from './connection'; + +export const DATABASE_CONNECTION = 'DATABASE_CONNECTION'; + +@Global() +@Module({ + providers: [ + { + provide: DATABASE_CONNECTION, + useFactory: (configService: ConfigService): Database => { + const databaseUrl = configService.get('DATABASE_URL'); + if (!databaseUrl) { + throw new Error('DATABASE_URL environment variable is not set'); + } + return getDb(databaseUrl); + }, + inject: [ConfigService], + }, + ], + exports: [DATABASE_CONNECTION], +}) +export class DatabaseModule implements OnModuleDestroy { + async onModuleDestroy() { + await closeConnection(); + } +} diff --git a/apps/presi/apps/backend/src/db/schema/decks.schema.ts b/apps/presi/apps/backend/src/db/schema/decks.schema.ts new file mode 100644 index 000000000..8478821a0 --- /dev/null +++ b/apps/presi/apps/backend/src/db/schema/decks.schema.ts @@ -0,0 +1,25 @@ +import { pgTable, uuid, text, boolean, timestamp } from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; +import { slides } from './slides.schema'; +import { themes } from './themes.schema'; +import { sharedDecks } from './shared-decks.schema'; + +export const decks = pgTable('decks', { + id: uuid('id').primaryKey().defaultRandom(), + userId: uuid('user_id').notNull(), + title: text('title').notNull(), + description: text('description'), + themeId: uuid('theme_id').references(() => themes.id), + isPublic: boolean('is_public').default(false).notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}); + +export const decksRelations = relations(decks, ({ many, one }) => ({ + slides: many(slides), + theme: one(themes, { + fields: [decks.themeId], + references: [themes.id], + }), + sharedDecks: many(sharedDecks), +})); diff --git a/apps/presi/apps/backend/src/db/schema/index.ts b/apps/presi/apps/backend/src/db/schema/index.ts new file mode 100644 index 000000000..eff7c7b78 --- /dev/null +++ b/apps/presi/apps/backend/src/db/schema/index.ts @@ -0,0 +1,4 @@ +export * from './decks.schema'; +export * from './slides.schema'; +export * from './themes.schema'; +export * from './shared-decks.schema'; diff --git a/apps/presi/apps/backend/src/db/schema/shared-decks.schema.ts b/apps/presi/apps/backend/src/db/schema/shared-decks.schema.ts new file mode 100644 index 000000000..3a2ed837f --- /dev/null +++ b/apps/presi/apps/backend/src/db/schema/shared-decks.schema.ts @@ -0,0 +1,20 @@ +import { pgTable, uuid, text, timestamp } from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; +import { decks } from './decks.schema'; + +export const sharedDecks = pgTable('shared_decks', { + id: uuid('id').primaryKey().defaultRandom(), + deckId: uuid('deck_id') + .notNull() + .references(() => decks.id, { onDelete: 'cascade' }), + shareCode: text('share_code').notNull().unique(), + expiresAt: timestamp('expires_at'), + createdAt: timestamp('created_at').defaultNow().notNull(), +}); + +export const sharedDecksRelations = relations(sharedDecks, ({ one }) => ({ + deck: one(decks, { + fields: [sharedDecks.deckId], + references: [decks.id], + }), +})); diff --git a/apps/presi/apps/backend/src/db/schema/slides.schema.ts b/apps/presi/apps/backend/src/db/schema/slides.schema.ts new file mode 100644 index 000000000..472a55082 --- /dev/null +++ b/apps/presi/apps/backend/src/db/schema/slides.schema.ts @@ -0,0 +1,27 @@ +import { pgTable, uuid, integer, jsonb, timestamp } from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; +import { decks } from './decks.schema'; + +export const slides = pgTable('slides', { + id: uuid('id').primaryKey().defaultRandom(), + deckId: uuid('deck_id') + .notNull() + .references(() => decks.id, { onDelete: 'cascade' }), + order: integer('order').notNull(), + content: jsonb('content').$type<{ + type: 'title' | 'content' | 'image' | 'split'; + title?: string; + subtitle?: string; + body?: string; + imageUrl?: string; + bulletPoints?: string[]; + }>(), + createdAt: timestamp('created_at').defaultNow().notNull(), +}); + +export const slidesRelations = relations(slides, ({ one }) => ({ + deck: one(decks, { + fields: [slides.deckId], + references: [decks.id], + }), +})); diff --git a/apps/presi/apps/backend/src/db/schema/themes.schema.ts b/apps/presi/apps/backend/src/db/schema/themes.schema.ts new file mode 100644 index 000000000..eac842a28 --- /dev/null +++ b/apps/presi/apps/backend/src/db/schema/themes.schema.ts @@ -0,0 +1,24 @@ +import { pgTable, uuid, text, jsonb, boolean } from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; +import { decks } from './decks.schema'; + +export const themes = pgTable('themes', { + id: uuid('id').primaryKey().defaultRandom(), + name: text('name').notNull(), + colors: jsonb('colors').$type<{ + primary: string; + secondary: string; + background: string; + text: string; + accent: string; + }>(), + fonts: jsonb('fonts').$type<{ + heading: string; + body: string; + }>(), + isDefault: boolean('is_default').default(false).notNull(), +}); + +export const themesRelations = relations(themes, ({ many }) => ({ + decks: many(decks), +})); diff --git a/apps/presi/apps/backend/src/deck/deck.controller.ts b/apps/presi/apps/backend/src/deck/deck.controller.ts new file mode 100644 index 000000000..9d1d6dcb2 --- /dev/null +++ b/apps/presi/apps/backend/src/deck/deck.controller.ts @@ -0,0 +1,41 @@ +import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards } from '@nestjs/common'; +import { DeckService } from './deck.service'; +import { CreateDeckDto } from './deck.dto'; +import type { UpdateDeckDto } from './deck.dto'; +import { JwtAuthGuard, CurrentUser } from '@manacore/shared-nestjs-auth'; +import type { CurrentUserData } from '@manacore/shared-nestjs-auth'; + +@Controller('decks') +@UseGuards(JwtAuthGuard) +export class DeckController { + constructor(private readonly deckService: DeckService) {} + + @Get() + async findAll(@CurrentUser() user: CurrentUserData) { + return this.deckService.findByUser(user.userId); + } + + @Get(':id') + async findOne(@Param('id') id: string, @CurrentUser() user: CurrentUserData) { + return this.deckService.findOneWithSlides(id, user.userId); + } + + @Post() + async create(@Body() createDeckDto: CreateDeckDto, @CurrentUser() user: CurrentUserData) { + return this.deckService.create(user.userId, createDeckDto); + } + + @Put(':id') + async update( + @Param('id') id: string, + @Body() updateDeckDto: UpdateDeckDto, + @CurrentUser() user: CurrentUserData + ) { + return this.deckService.update(id, user.userId, updateDeckDto); + } + + @Delete(':id') + async remove(@Param('id') id: string, @CurrentUser() user: CurrentUserData) { + return this.deckService.remove(id, user.userId); + } +} diff --git a/apps/presi/apps/backend/src/deck/deck.dto.ts b/apps/presi/apps/backend/src/deck/deck.dto.ts new file mode 100644 index 000000000..d5b420c04 --- /dev/null +++ b/apps/presi/apps/backend/src/deck/deck.dto.ts @@ -0,0 +1,32 @@ +import { IsString, IsOptional, IsBoolean, IsUUID } from 'class-validator'; + +export class CreateDeckDto { + @IsString() + title: string; + + @IsString() + @IsOptional() + description?: string; + + @IsUUID() + @IsOptional() + themeId?: string; +} + +export class UpdateDeckDto { + @IsString() + @IsOptional() + title?: string; + + @IsString() + @IsOptional() + description?: string; + + @IsUUID() + @IsOptional() + themeId?: string; + + @IsBoolean() + @IsOptional() + isPublic?: boolean; +} diff --git a/apps/presi/apps/backend/src/deck/deck.module.ts b/apps/presi/apps/backend/src/deck/deck.module.ts new file mode 100644 index 000000000..873e5abb6 --- /dev/null +++ b/apps/presi/apps/backend/src/deck/deck.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { DeckController } from './deck.controller'; +import { DeckService } from './deck.service'; + +@Module({ + controllers: [DeckController], + providers: [DeckService], + exports: [DeckService], +}) +export class DeckModule {} diff --git a/apps/presi/apps/backend/src/deck/deck.service.ts b/apps/presi/apps/backend/src/deck/deck.service.ts new file mode 100644 index 000000000..d09e7c10a --- /dev/null +++ b/apps/presi/apps/backend/src/deck/deck.service.ts @@ -0,0 +1,113 @@ +import { Injectable, Inject, NotFoundException } from '@nestjs/common'; +import { eq, and, desc } from 'drizzle-orm'; +import { DATABASE_CONNECTION } from '../db/database.module'; +import { Database } from '../db/connection'; +import { decks, slides } from '../db/schema'; +import { CreateDeckDto } from './deck.dto'; +import type { UpdateDeckDto } from './deck.dto'; + +@Injectable() +export class DeckService { + constructor( + @Inject(DATABASE_CONNECTION) + private readonly db: Database + ) {} + + async findByUser(userId: string) { + return this.db.query.decks.findMany({ + where: eq(decks.userId, userId), + orderBy: [desc(decks.updatedAt)], + with: { + theme: true, + }, + }); + } + + async findOneWithSlides(id: string, userId: string) { + const deck = await this.db.query.decks.findFirst({ + where: and(eq(decks.id, id), eq(decks.userId, userId)), + with: { + slides: { + orderBy: [slides.order], + }, + theme: true, + }, + }); + + if (!deck) { + throw new NotFoundException('Deck not found'); + } + + return deck; + } + + async findOne(id: string) { + return this.db.query.decks.findFirst({ + where: eq(decks.id, id), + with: { + slides: { + orderBy: [slides.order], + }, + theme: true, + }, + }); + } + + async create(userId: string, dto: CreateDeckDto) { + const [deck] = await this.db + .insert(decks) + .values({ + userId, + title: dto.title, + description: dto.description, + themeId: dto.themeId, + }) + .returning(); + + return deck; + } + + async update(id: string, userId: string, dto: UpdateDeckDto) { + // Verify ownership + const existing = await this.db.query.decks.findFirst({ + where: and(eq(decks.id, id), eq(decks.userId, userId)), + }); + + if (!existing) { + throw new NotFoundException('Deck not found'); + } + + const [updated] = await this.db + .update(decks) + .set({ + ...dto, + updatedAt: new Date(), + }) + .where(eq(decks.id, id)) + .returning(); + + return updated; + } + + async remove(id: string, userId: string) { + // Verify ownership + const existing = await this.db.query.decks.findFirst({ + where: and(eq(decks.id, id), eq(decks.userId, userId)), + }); + + if (!existing) { + throw new NotFoundException('Deck not found'); + } + + await this.db.delete(decks).where(eq(decks.id, id)); + + return { success: true }; + } + + async verifyOwnership(id: string, userId: string): Promise { + const deck = await this.db.query.decks.findFirst({ + where: and(eq(decks.id, id), eq(decks.userId, userId)), + }); + return !!deck; + } +} diff --git a/apps/presi/apps/backend/src/health/health.controller.ts b/apps/presi/apps/backend/src/health/health.controller.ts new file mode 100644 index 000000000..dce48e6e2 --- /dev/null +++ b/apps/presi/apps/backend/src/health/health.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Get } from '@nestjs/common'; + +@Controller('health') +export class HealthController { + @Get() + check() { + return { + status: 'ok', + timestamp: new Date().toISOString(), + service: 'presi-backend', + }; + } +} diff --git a/apps/presi/apps/backend/src/health/health.module.ts b/apps/presi/apps/backend/src/health/health.module.ts new file mode 100644 index 000000000..a61d8b044 --- /dev/null +++ b/apps/presi/apps/backend/src/health/health.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { HealthController } from './health.controller'; + +@Module({ + controllers: [HealthController], +}) +export class HealthModule {} diff --git a/apps/presi/apps/backend/src/main.ts b/apps/presi/apps/backend/src/main.ts new file mode 100644 index 000000000..595a662dc --- /dev/null +++ b/apps/presi/apps/backend/src/main.ts @@ -0,0 +1,41 @@ +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe } from '@nestjs/common'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + // Enable CORS for mobile and web apps + const corsOrigins = process.env.CORS_ORIGINS?.split(',').map((origin) => origin.trim()) || [ + 'http://localhost:3000', + 'http://localhost:5173', + 'http://localhost:5177', + 'http://localhost:5178', + 'http://localhost:8081', + 'exp://localhost:8081', + 'http://localhost:3001', + ]; + + app.enableCors({ + origin: corsOrigins, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + credentials: true, + }); + + // Enable validation + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + transform: true, + forbidNonWhitelisted: true, + }) + ); + + // Set global prefix for API routes + app.setGlobalPrefix('api/v1'); + + const port = process.env.PORT || 3008; + await app.listen(port); + console.log(`Presi backend running on http://localhost:${port}`); +} +bootstrap(); diff --git a/apps/presi/apps/backend/src/share/share.controller.ts b/apps/presi/apps/backend/src/share/share.controller.ts new file mode 100644 index 000000000..3f4bf24a1 --- /dev/null +++ b/apps/presi/apps/backend/src/share/share.controller.ts @@ -0,0 +1,40 @@ +import { Controller, Get, Post, Delete, Body, Param, UseGuards } from '@nestjs/common'; +import { ShareService } from './share.service'; +import { CreateShareDto } from './share.dto'; +import { JwtAuthGuard, CurrentUser } from '@manacore/shared-nestjs-auth'; +import type { CurrentUserData } from '@manacore/shared-nestjs-auth'; + +@Controller('share') +export class ShareController { + constructor(private readonly shareService: ShareService) {} + + // Public endpoint - no auth required + @Get(':code') + async getSharedDeck(@Param('code') code: string) { + return this.shareService.findByShareCode(code); + } + + // Authenticated endpoints + @Post('deck/:deckId') + @UseGuards(JwtAuthGuard) + async createShare( + @Param('deckId') deckId: string, + @Body() createShareDto: CreateShareDto, + @CurrentUser() user: CurrentUserData + ) { + const expiresAt = createShareDto.expiresAt ? new Date(createShareDto.expiresAt) : undefined; + return this.shareService.createShare(deckId, user.userId, expiresAt); + } + + @Get('deck/:deckId/links') + @UseGuards(JwtAuthGuard) + async getSharesForDeck(@Param('deckId') deckId: string, @CurrentUser() user: CurrentUserData) { + return this.shareService.getSharesForDeck(deckId, user.userId); + } + + @Delete(':shareId') + @UseGuards(JwtAuthGuard) + async deleteShare(@Param('shareId') shareId: string, @CurrentUser() user: CurrentUserData) { + return this.shareService.deleteShare(shareId, user.userId); + } +} diff --git a/apps/presi/apps/backend/src/share/share.dto.ts b/apps/presi/apps/backend/src/share/share.dto.ts new file mode 100644 index 000000000..118ca2d98 --- /dev/null +++ b/apps/presi/apps/backend/src/share/share.dto.ts @@ -0,0 +1,7 @@ +import { IsOptional, IsDateString } from 'class-validator'; + +export class CreateShareDto { + @IsOptional() + @IsDateString() + expiresAt?: string; +} diff --git a/apps/presi/apps/backend/src/share/share.module.ts b/apps/presi/apps/backend/src/share/share.module.ts new file mode 100644 index 000000000..0c0ea60a6 --- /dev/null +++ b/apps/presi/apps/backend/src/share/share.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ShareController } from './share.controller'; +import { ShareService } from './share.service'; +import { DeckModule } from '../deck/deck.module'; + +@Module({ + imports: [DeckModule], + controllers: [ShareController], + providers: [ShareService], + exports: [ShareService], +}) +export class ShareModule {} diff --git a/apps/presi/apps/backend/src/share/share.service.ts b/apps/presi/apps/backend/src/share/share.service.ts new file mode 100644 index 000000000..f002ab9bc --- /dev/null +++ b/apps/presi/apps/backend/src/share/share.service.ts @@ -0,0 +1,111 @@ +import { Injectable, Inject, NotFoundException, ForbiddenException } from '@nestjs/common'; +import { eq, and, gt, or, isNull } from 'drizzle-orm'; +import { DATABASE_CONNECTION } from '../db/database.module'; +import { Database } from '../db/connection'; +import { sharedDecks, slides } from '../db/schema'; +import { DeckService } from '../deck/deck.service'; +import { randomBytes } from 'crypto'; + +@Injectable() +export class ShareService { + constructor( + @Inject(DATABASE_CONNECTION) + private readonly db: Database, + private readonly deckService: DeckService + ) {} + + private generateShareCode(): string { + return randomBytes(6).toString('hex'); // 12 character code + } + + async createShare(deckId: string, userId: string, expiresAt?: Date) { + // Verify ownership + const isOwner = await this.deckService.verifyOwnership(deckId, userId); + if (!isOwner) { + throw new ForbiddenException('You do not own this deck'); + } + + // Check if there's already a valid share + const existingShare = await this.db.query.sharedDecks.findFirst({ + where: and( + eq(sharedDecks.deckId, deckId), + or(isNull(sharedDecks.expiresAt), gt(sharedDecks.expiresAt, new Date())) + ), + }); + + if (existingShare) { + return existingShare; + } + + // Create new share + const [share] = await this.db + .insert(sharedDecks) + .values({ + deckId, + shareCode: this.generateShareCode(), + expiresAt: expiresAt || null, + }) + .returning(); + + return share; + } + + async findByShareCode(shareCode: string) { + const share = await this.db.query.sharedDecks.findFirst({ + where: and( + eq(sharedDecks.shareCode, shareCode), + or(isNull(sharedDecks.expiresAt), gt(sharedDecks.expiresAt, new Date())) + ), + with: { + deck: { + with: { + slides: { + orderBy: [slides.order], + }, + theme: true, + }, + }, + }, + }); + + if (!share) { + throw new NotFoundException('Shared deck not found or link has expired'); + } + + return share.deck; + } + + async getSharesForDeck(deckId: string, userId: string) { + // Verify ownership + const isOwner = await this.deckService.verifyOwnership(deckId, userId); + if (!isOwner) { + throw new ForbiddenException('You do not own this deck'); + } + + return this.db.query.sharedDecks.findMany({ + where: eq(sharedDecks.deckId, deckId), + }); + } + + async deleteShare(shareId: string, userId: string) { + const share = await this.db.query.sharedDecks.findFirst({ + where: eq(sharedDecks.id, shareId), + with: { + deck: true, + }, + }); + + if (!share) { + throw new NotFoundException('Share not found'); + } + + // Verify ownership of the deck + if (share.deck.userId !== userId) { + throw new ForbiddenException('You do not own this deck'); + } + + await this.db.delete(sharedDecks).where(eq(sharedDecks.id, shareId)); + + return { success: true }; + } +} diff --git a/apps/presi/apps/backend/src/slide/slide.controller.ts b/apps/presi/apps/backend/src/slide/slide.controller.ts new file mode 100644 index 000000000..5dc6acc57 --- /dev/null +++ b/apps/presi/apps/backend/src/slide/slide.controller.ts @@ -0,0 +1,40 @@ +import { Controller, Post, Put, Delete, Body, Param, UseGuards } from '@nestjs/common'; +import { SlideService } from './slide.service'; +import { CreateSlideDto } from './slide.dto'; +import type { UpdateSlideDto, ReorderSlidesDto } from './slide.dto'; +import { JwtAuthGuard, CurrentUser } from '@manacore/shared-nestjs-auth'; +import type { CurrentUserData } from '@manacore/shared-nestjs-auth'; + +@Controller() +@UseGuards(JwtAuthGuard) +export class SlideController { + constructor(private readonly slideService: SlideService) {} + + @Post('decks/:deckId/slides') + async create( + @Param('deckId') deckId: string, + @Body() createSlideDto: CreateSlideDto, + @CurrentUser() user: CurrentUserData + ) { + return this.slideService.create(deckId, user.userId, createSlideDto); + } + + @Put('slides/:id') + async update( + @Param('id') id: string, + @Body() updateSlideDto: UpdateSlideDto, + @CurrentUser() user: CurrentUserData + ) { + return this.slideService.update(id, user.userId, updateSlideDto); + } + + @Delete('slides/:id') + async remove(@Param('id') id: string, @CurrentUser() user: CurrentUserData) { + return this.slideService.remove(id, user.userId); + } + + @Put('slides/reorder') + async reorder(@Body() reorderDto: ReorderSlidesDto, @CurrentUser() user: CurrentUserData) { + return this.slideService.reorder(user.userId, reorderDto); + } +} diff --git a/apps/presi/apps/backend/src/slide/slide.dto.ts b/apps/presi/apps/backend/src/slide/slide.dto.ts new file mode 100644 index 000000000..5ef28579d --- /dev/null +++ b/apps/presi/apps/backend/src/slide/slide.dto.ts @@ -0,0 +1,45 @@ +import { IsObject, IsOptional, IsNumber, IsArray, ValidateNested, IsUUID } from 'class-validator'; +import { Type } from 'class-transformer'; + +class SlideContent { + type: 'title' | 'content' | 'image' | 'split'; + title?: string; + subtitle?: string; + body?: string; + imageUrl?: string; + bulletPoints?: string[]; +} + +export class CreateSlideDto { + @IsObject() + content: SlideContent; + + @IsNumber() + @IsOptional() + order?: number; +} + +export class UpdateSlideDto { + @IsObject() + @IsOptional() + content?: SlideContent; + + @IsNumber() + @IsOptional() + order?: number; +} + +class SlideOrderItem { + @IsUUID() + id: string; + + @IsNumber() + order: number; +} + +export class ReorderSlidesDto { + @IsArray() + @ValidateNested({ each: true }) + @Type(() => SlideOrderItem) + slides: SlideOrderItem[]; +} diff --git a/apps/presi/apps/backend/src/slide/slide.module.ts b/apps/presi/apps/backend/src/slide/slide.module.ts new file mode 100644 index 000000000..ad785780d --- /dev/null +++ b/apps/presi/apps/backend/src/slide/slide.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { SlideController } from './slide.controller'; +import { SlideService } from './slide.service'; +import { DeckModule } from '../deck/deck.module'; + +@Module({ + imports: [DeckModule], + controllers: [SlideController], + providers: [SlideService], +}) +export class SlideModule {} diff --git a/apps/presi/apps/backend/src/slide/slide.service.ts b/apps/presi/apps/backend/src/slide/slide.service.ts new file mode 100644 index 000000000..0aad844de --- /dev/null +++ b/apps/presi/apps/backend/src/slide/slide.service.ts @@ -0,0 +1,125 @@ +import { Injectable, Inject, NotFoundException, ForbiddenException } from '@nestjs/common'; +import { eq, max } from 'drizzle-orm'; +import { DATABASE_CONNECTION } from '../db/database.module'; +import { Database } from '../db/connection'; +import { slides, decks } from '../db/schema'; +import { DeckService } from '../deck/deck.service'; +import { CreateSlideDto } from './slide.dto'; +import type { UpdateSlideDto, ReorderSlidesDto } from './slide.dto'; + +@Injectable() +export class SlideService { + constructor( + @Inject(DATABASE_CONNECTION) + private readonly db: Database, + private readonly deckService: DeckService + ) {} + + async create(deckId: string, userId: string, dto: CreateSlideDto) { + // Verify deck ownership + const isOwner = await this.deckService.verifyOwnership(deckId, userId); + if (!isOwner) { + throw new ForbiddenException('Not authorized to modify this deck'); + } + + // Get next order number + const result = await this.db + .select({ maxOrder: max(slides.order) }) + .from(slides) + .where(eq(slides.deckId, deckId)); + + const nextOrder = dto.order ?? (result[0]?.maxOrder ?? -1) + 1; + + const [slide] = await this.db + .insert(slides) + .values({ + deckId, + order: nextOrder, + content: dto.content, + }) + .returning(); + + // Update deck's updatedAt + await this.db.update(decks).set({ updatedAt: new Date() }).where(eq(decks.id, deckId)); + + return slide; + } + + async update(id: string, userId: string, dto: UpdateSlideDto) { + // Get slide and verify ownership + const slide = await this.db.query.slides.findFirst({ + where: eq(slides.id, id), + with: { deck: true }, + }); + + if (!slide) { + throw new NotFoundException('Slide not found'); + } + + if (slide.deck.userId !== userId) { + throw new ForbiddenException('Not authorized to modify this slide'); + } + + const [updated] = await this.db + .update(slides) + .set({ + content: dto.content ?? slide.content, + order: dto.order ?? slide.order, + }) + .where(eq(slides.id, id)) + .returning(); + + // Update deck's updatedAt + await this.db.update(decks).set({ updatedAt: new Date() }).where(eq(decks.id, slide.deckId)); + + return updated; + } + + async remove(id: string, userId: string) { + // Get slide and verify ownership + const slide = await this.db.query.slides.findFirst({ + where: eq(slides.id, id), + with: { deck: true }, + }); + + if (!slide) { + throw new NotFoundException('Slide not found'); + } + + if (slide.deck.userId !== userId) { + throw new ForbiddenException('Not authorized to delete this slide'); + } + + await this.db.delete(slides).where(eq(slides.id, id)); + + // Update deck's updatedAt + await this.db.update(decks).set({ updatedAt: new Date() }).where(eq(decks.id, slide.deckId)); + + return { success: true }; + } + + async reorder(userId: string, dto: ReorderSlidesDto) { + // Verify ownership of all slides + for (const item of dto.slides) { + const slide = await this.db.query.slides.findFirst({ + where: eq(slides.id, item.id), + with: { deck: true }, + }); + + if (!slide) { + throw new NotFoundException(`Slide ${item.id} not found`); + } + + if (slide.deck.userId !== userId) { + throw new ForbiddenException('Not authorized to reorder these slides'); + } + } + + // Update orders + for (const item of dto.slides) { + await this.db.update(slides).set({ order: item.order }).where(eq(slides.id, item.id)); + } + + return { success: true }; + } +} diff --git a/apps/presi/apps/backend/src/theme/theme.controller.ts b/apps/presi/apps/backend/src/theme/theme.controller.ts new file mode 100644 index 000000000..0157afa52 --- /dev/null +++ b/apps/presi/apps/backend/src/theme/theme.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Get, Param } from '@nestjs/common'; +import { ThemeService } from './theme.service'; + +@Controller('themes') +export class ThemeController { + constructor(private readonly themeService: ThemeService) {} + + @Get() + async findAll() { + return this.themeService.findAll(); + } + + @Get('default') + async findDefault() { + return this.themeService.findDefault(); + } + + @Get(':id') + async findOne(@Param('id') id: string) { + return this.themeService.findOne(id); + } +} diff --git a/apps/presi/apps/backend/src/theme/theme.module.ts b/apps/presi/apps/backend/src/theme/theme.module.ts new file mode 100644 index 000000000..5deba376f --- /dev/null +++ b/apps/presi/apps/backend/src/theme/theme.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ThemeController } from './theme.controller'; +import { ThemeService } from './theme.service'; + +@Module({ + controllers: [ThemeController], + providers: [ThemeService], + exports: [ThemeService], +}) +export class ThemeModule {} diff --git a/apps/presi/apps/backend/src/theme/theme.service.ts b/apps/presi/apps/backend/src/theme/theme.service.ts new file mode 100644 index 000000000..33083d315 --- /dev/null +++ b/apps/presi/apps/backend/src/theme/theme.service.ts @@ -0,0 +1,27 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { eq } from 'drizzle-orm'; +import { DATABASE_CONNECTION } from '../db/database.module'; +import { Database } from '../db/connection'; +import { themes } from '../db/schema'; + +@Injectable() +export class ThemeService { + constructor( + @Inject(DATABASE_CONNECTION) + private readonly db: Database + ) {} + + async findAll() { + return this.db.select().from(themes); + } + + async findOne(id: string) { + const result = await this.db.select().from(themes).where(eq(themes.id, id)); + return result[0] || null; + } + + async findDefault() { + const result = await this.db.select().from(themes).where(eq(themes.isDefault, true)); + return result[0] || null; + } +} diff --git a/apps/presi/apps/backend/tsconfig.json b/apps/presi/apps/backend/tsconfig.json new file mode 100644 index 000000000..3bf96c993 --- /dev/null +++ b/apps/presi/apps/backend/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/presi/apps/landing/astro.config.mjs b/apps/presi/apps/landing/astro.config.mjs new file mode 100644 index 000000000..9abd76b23 --- /dev/null +++ b/apps/presi/apps/landing/astro.config.mjs @@ -0,0 +1,8 @@ +import { defineConfig } from 'astro/config'; +import tailwind from '@astrojs/tailwind'; +import sitemap from '@astrojs/sitemap'; + +export default defineConfig({ + site: 'https://presi.manacore.app', + integrations: [tailwind(), sitemap()], +}); diff --git a/apps/presi/apps/landing/package.json b/apps/presi/apps/landing/package.json new file mode 100644 index 000000000..412a163e7 --- /dev/null +++ b/apps/presi/apps/landing/package.json @@ -0,0 +1,26 @@ +{ + "name": "@presi/landing", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro check && astro build", + "preview": "astro preview", + "astro": "astro", + "type-check": "astro check" + }, + "dependencies": { + "@astrojs/check": "^0.9.0", + "@astrojs/sitemap": "^3.2.1", + "@manacore/shared-landing-ui": "workspace:*", + "astro": "^5.16.0", + "typescript": "^5.0.0" + }, + "devDependencies": { + "@astrojs/tailwind": "^6.0.0", + "@tailwindcss/typography": "^0.5.16", + "tailwindcss": "^3.4.17" + } +} diff --git a/apps/presi/apps/landing/public/favicon.svg b/apps/presi/apps/landing/public/favicon.svg new file mode 100644 index 000000000..ebea9ecb5 --- /dev/null +++ b/apps/presi/apps/landing/public/favicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/presi/apps/landing/src/components/Footer.astro b/apps/presi/apps/landing/src/components/Footer.astro new file mode 100644 index 000000000..5b8a37ca6 --- /dev/null +++ b/apps/presi/apps/landing/src/components/Footer.astro @@ -0,0 +1,93 @@ +--- +import Container from '@manacore/shared-landing-ui/atoms/Container.astro'; +--- + + diff --git a/apps/presi/apps/landing/src/components/Navigation.astro b/apps/presi/apps/landing/src/components/Navigation.astro new file mode 100644 index 000000000..a27659048 --- /dev/null +++ b/apps/presi/apps/landing/src/components/Navigation.astro @@ -0,0 +1,39 @@ +--- +import Container from '@manacore/shared-landing-ui/atoms/Container.astro'; +import Button from '@manacore/shared-landing-ui/atoms/Button.astro'; +--- + + diff --git a/apps/presi/apps/landing/src/layouts/Layout.astro b/apps/presi/apps/landing/src/layouts/Layout.astro new file mode 100644 index 000000000..97d948dfd --- /dev/null +++ b/apps/presi/apps/landing/src/layouts/Layout.astro @@ -0,0 +1,50 @@ +--- +import '../styles/global.css'; + +interface Props { + title: string; + description?: string; +} + +const { + title, + description = 'Presi - Erstelle beeindruckende Präsentationen mit KI-Unterstützung', +} = Astro.props; +--- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {title} + + + + + diff --git a/apps/presi/apps/landing/src/pages/index.astro b/apps/presi/apps/landing/src/pages/index.astro new file mode 100644 index 000000000..60aa6c2f3 --- /dev/null +++ b/apps/presi/apps/landing/src/pages/index.astro @@ -0,0 +1,229 @@ +--- +import Layout from '../layouts/Layout.astro'; +import Navigation from '../components/Navigation.astro'; +import Footer from '../components/Footer.astro'; + +// Shared components +import HeroSection from '@manacore/shared-landing-ui/sections/HeroSection.astro'; +import FeatureSection from '@manacore/shared-landing-ui/sections/FeatureSection.astro'; +import CTASection from '@manacore/shared-landing-ui/sections/CTASection.astro'; +import FAQSection from '@manacore/shared-landing-ui/sections/FAQSection.astro'; +import Container from '@manacore/shared-landing-ui/atoms/Container.astro'; + +// Feature data +const features = [ + { + icon: '🎨', + title: 'Schöne Themes', + description: 'Wähle aus professionellen Themes oder erstelle dein eigenes Design.', + }, + { + icon: '📱', + title: 'Überall verfügbar', + description: 'Native Apps für iOS, Android und Web - deine Präsentationen sind immer dabei.', + }, + { + icon: '🔗', + title: 'Einfach teilen', + description: 'Teile Präsentationen per Link - ohne Download oder Account nötig.', + }, + { + icon: '🖥️', + title: 'Präsentationsmodus', + description: 'Vollbild-Präsentation mit Tastatursteuerung und Timer.', + }, + { + icon: '📝', + title: 'Verschiedene Layouts', + description: 'Titel, Inhalt, Bilder, Split-Views - für jeden Zweck das richtige Layout.', + }, + { + icon: '☁️', + title: 'Cloud-Sync', + description: 'Automatische Synchronisierung auf allen Geräten.', + }, +]; + +// How it works steps +const steps = [ + { + number: '1', + title: 'Deck erstellen', + description: 'Starte mit einem leeren Deck oder wähle ein Template.', + }, + { + number: '2', + title: 'Slides hinzufügen', + description: 'Füge Titel, Text, Bilder und Bullet Points hinzu.', + }, + { + number: '3', + title: 'Theme wählen', + description: 'Wähle ein professionelles Theme für deine Präsentation.', + }, + { + number: '4', + title: 'Präsentieren & Teilen', + description: 'Präsentiere im Vollbild oder teile per Link.', + }, +]; + +// FAQ data +const faqs = [ + { + question: 'Ist Presi kostenlos?', + answer: + 'Ja, Presi ist kostenlos nutzbar. Du kannst unbegrenzt Präsentationen erstellen, teilen und präsentieren.', + }, + { + question: 'Kann ich Präsentationen offline bearbeiten?', + answer: + 'Mit der mobilen App kannst du deine Präsentationen auch offline bearbeiten. Änderungen werden synchronisiert, sobald du wieder online bist.', + }, + { + question: 'Wie teile ich eine Präsentation?', + answer: + 'Klicke auf "Teilen" und erstelle einen Link. Jeder mit dem Link kann die Präsentation ansehen - ohne Account oder Download.', + }, + { + question: 'Welche Slide-Typen gibt es?', + answer: + 'Presi unterstützt Titel-Slides, Content-Slides mit Text und Bullet Points, Bild-Slides und Split-Views mit Text und Bild nebeneinander.', + }, + { + question: 'Kann ich eigene Themes erstellen?', + answer: + 'Aktuell bieten wir vorgefertigte Themes. Custom Themes sind für zukünftige Versionen geplant.', + }, +]; +--- + + + + +
+ + + + +
+ +
+

So einfach geht's

+

+ In vier Schritten zur perfekten Präsentation +

+
+ +
+ { + steps.map((step, index) => ( +
+
+
+ {step.number} +
+

{step.title}

+

{step.description}

+
+ {index < steps.length - 1 && ( + + )} +
+ )) + } +
+
+
+ + + + + +
+ +
+

+ Präsentiere wie ein Profi +

+

+ Der Präsentationsmodus bietet alles was du brauchst: Vollbild-Ansicht, + Tastaturnavigation mit Pfeiltasten, Timer für perfektes Timing und Speaker Notes für + deine Notizen. +

+
+
+
+ + + Navigation +
+
+ F + Vollbild +
+
+ ESC + Beenden +
+
+
+
+
+
+ + + + + + +
+ +