├── docs ├── static │ └── img │ │ ├── favicon.ico │ │ ├── logo.svg │ │ ├── editor.jpg │ │ ├── mobile.jpg │ │ ├── gallery.jpg │ │ └── screenshots.jpg ├── tsconfig.json ├── .gitignore ├── docs │ ├── deployment │ │ └── docker.md │ ├── getting-started │ │ └── quick-start.md │ └── intro.md ├── sidebars.ts └── package.json ├── web ├── src │ ├── generated │ │ └── index.ts │ ├── vite-env.d.ts │ ├── lib │ │ ├── config-storage │ │ │ ├── config-storage.ts │ │ │ ├── memory-config-storage.ts │ │ │ ├── local-config-storage.ts │ │ │ ├── session-config-storage.ts │ │ │ └── user-registry-config-storage.ts │ │ ├── token.ts │ │ ├── preload-image.ts │ │ ├── graphql-client.ts │ │ ├── utils.ts │ │ ├── api-utils.ts │ │ ├── editor-open-sections-storage.ts │ │ ├── file-extensions.ts │ │ ├── browser-utils.ts │ │ └── editor-state-url.ts │ ├── components │ │ ├── ui │ │ │ ├── skeleton.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── label.tsx │ │ │ ├── button-with-loading.tsx │ │ │ ├── progress.tsx │ │ │ ├── textarea.tsx │ │ │ ├── separator.tsx │ │ │ ├── rectangle-stepper.tsx │ │ │ ├── sonner.tsx │ │ │ ├── input.tsx │ │ │ ├── progress-bar.tsx │ │ │ ├── slider.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── badge.tsx │ │ │ ├── tooltip.tsx │ │ │ ├── avatar.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── toggle.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── tabs.tsx │ │ │ ├── copy-url-dialog.tsx │ │ │ ├── error-page.tsx │ │ │ └── breadcrumb.tsx │ │ ├── imagor │ │ │ └── embedded-imagor-form.tsx │ │ ├── image-gallery │ │ │ ├── image-view-info.tsx │ │ │ ├── empty-gallery-state.tsx │ │ │ ├── delete-image-dialog.tsx │ │ │ ├── image-context-menu.tsx │ │ │ └── folder-grid.tsx │ │ ├── upload │ │ │ └── drop-zone-overlay.tsx │ │ ├── mode-toggle.tsx │ │ ├── license-badge.tsx │ │ ├── loading-bar.tsx │ │ ├── slideshow-timer.tsx │ │ ├── folder-tree │ │ │ └── folder-tree-sidebar.tsx │ │ └── storage │ │ │ └── storage-type-selector.tsx │ ├── main.tsx │ ├── loaders │ │ ├── admin-setup-loader.ts │ │ ├── root-loader.ts │ │ ├── embedded-loader.ts │ │ └── account-loader.ts │ ├── layouts │ │ ├── content-layout.tsx │ │ ├── sidebar-layout.tsx │ │ └── account-layout.tsx │ ├── i18n.ts │ ├── hooks │ │ ├── use-title.ts │ │ ├── use-breakpoint.ts │ │ ├── use-width-handler.ts │ │ ├── use-resize-handler.ts │ │ ├── use-breadcrumb.ts │ │ └── use-scroll-handler.ts │ ├── scrollbar.css │ ├── stores │ │ └── locale-store.ts │ ├── graphql │ │ ├── imagor.gql.ts │ │ ├── user.gql.ts │ │ └── registry.gql.ts │ └── api │ │ ├── imagor-api.ts │ │ └── license-api.ts ├── .env.production ├── public │ └── icon.png ├── postcss.config.js ├── tsconfig.json ├── components.json ├── vite.config.ts ├── .prettierrc ├── index.html ├── tsconfig.node.json ├── tsconfig.app.json ├── eslint.config.js ├── .env.example ├── codegen.ts └── package.json ├── assets └── screenshots.jpg ├── server ├── embed.go ├── tools.go ├── internal │ ├── noop │ │ ├── noop.go │ │ ├── registrystore.go │ │ └── userstore.go │ ├── migrations │ │ ├── migrations.go │ │ ├── 20250923_add_video_extensions.go │ │ ├── 20250831_add_is_encrypted_to_registry.go │ │ ├── 20250504_create_metadata_table.go │ │ └── 20250816_create_users_table.go │ ├── uuid │ │ ├── uuid.go │ │ └── uuid_test.go │ ├── model │ │ ├── registry.go │ │ └── user.go │ ├── license │ │ └── types.go │ ├── middleware │ │ ├── error.go │ │ ├── error_test.go │ │ ├── cors.go │ │ └── jwt.go │ ├── httphandler │ │ ├── static.go │ │ ├── handle.go │ │ └── license.go │ ├── auth │ │ └── password.go │ ├── storage │ │ └── noopstorage │ │ │ └── noopstorage.go │ ├── validation │ │ └── validation.go │ ├── registrystore │ │ ├── upsert.go │ │ └── namespace.go │ └── resolver │ │ └── resolver.go ├── gqlgen.yml ├── cmd │ ├── imagor-studio-migrate │ │ └── main.go │ └── imagor-studio │ │ └── main.go └── Makefile ├── .dockerignore ├── docker-entrypoint.sh ├── LICENSE ├── graphql ├── user.graphql ├── registry.graphql ├── imagor.graphql └── storage.graphql ├── .gitignore ├── .github └── workflows │ ├── docker.yml │ ├── test.yml │ ├── docker-variants.yml │ └── builder.yml ├── README.md └── Dockerfile.builder /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from './gql' 2 | -------------------------------------------------------------------------------- /web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web/.env.production: -------------------------------------------------------------------------------- 1 | VITE_API_BASE_URL= 2 | VITE_NODE_ENV=production 3 | -------------------------------------------------------------------------------- /web/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cshum/imagor-studio/HEAD/web/public/icon.png -------------------------------------------------------------------------------- /assets/screenshots.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cshum/imagor-studio/HEAD/assets/screenshots.jpg -------------------------------------------------------------------------------- /docs/static/img/editor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cshum/imagor-studio/HEAD/docs/static/img/editor.jpg -------------------------------------------------------------------------------- /docs/static/img/mobile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cshum/imagor-studio/HEAD/docs/static/img/mobile.jpg -------------------------------------------------------------------------------- /docs/static/img/gallery.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cshum/imagor-studio/HEAD/docs/static/img/gallery.jpg -------------------------------------------------------------------------------- /docs/static/img/screenshots.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cshum/imagor-studio/HEAD/docs/static/img/screenshots.jpg -------------------------------------------------------------------------------- /server/embed.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | //go:embed static/* 8 | var EmbedFS embed.FS 9 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@docusaurus/tsconfig", 3 | "compilerOptions": { 4 | "baseUrl": "." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /web/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /server/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | package tools 4 | 5 | import ( 6 | _ "github.com/99designs/gqlgen" 7 | _ "github.com/99designs/gqlgen/graphql/introspection" 8 | ) 9 | -------------------------------------------------------------------------------- /web/src/lib/config-storage/config-storage.ts: -------------------------------------------------------------------------------- 1 | export interface ConfigStorage { 2 | get(): Promise 3 | 4 | set(value: string): Promise 5 | 6 | remove(): Promise 7 | } 8 | -------------------------------------------------------------------------------- /server/internal/noop/noop.go: -------------------------------------------------------------------------------- 1 | package noop 2 | 3 | import "errors" 4 | 5 | // Common error messages for embedded mode operations 6 | var ( 7 | ErrEmbeddedMode = errors.New("operation not available in embedded mode") 8 | ) 9 | -------------------------------------------------------------------------------- /web/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | 3 | function Skeleton({ className, ...props }: React.HTMLAttributes) { 4 | return
5 | } 6 | 7 | export { Skeleton } 8 | -------------------------------------------------------------------------------- /web/src/lib/token.ts: -------------------------------------------------------------------------------- 1 | const TOKEN = 'auth_token' 2 | 3 | export const setToken = (token: string) => window.localStorage.setItem(TOKEN, token) 4 | 5 | export const getToken = () => window.localStorage.getItem(TOKEN) 6 | 7 | export const removeToken = () => window.localStorage.removeItem(TOKEN) 8 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Production 5 | build/ 6 | .docusaurus/ 7 | .cache-loader/ 8 | 9 | # Generated files 10 | .DS_Store 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # IDE 17 | .idea/ 18 | .vscode/ 19 | *.swp 20 | *.swo 21 | *~ 22 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ], 11 | "compilerOptions": { 12 | "baseUrl": ".", 13 | "paths": { 14 | "@/*": ["./src/*"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /web/src/lib/preload-image.ts: -------------------------------------------------------------------------------- 1 | // preload an image 2 | export const preloadImage = (src: string): Promise => { 3 | return new Promise((resolve, reject) => { 4 | const img = new Image() 5 | img.onload = () => resolve(img) 6 | img.onerror = reject 7 | img.onabort = reject 8 | img.src = src 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /web/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | 4 | import './i18n' 5 | import 'non.geist' 6 | import './index.css' 7 | 8 | import { AppRouter } from '@/router.tsx' 9 | 10 | createRoot(document.getElementById('root')!).render( 11 | 12 | 13 | , 14 | ) 15 | -------------------------------------------------------------------------------- /docs/docs/deployment/docker.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Docker Deployment 6 | 7 | See [Docker Deployment Guide](../getting-started/docker-deployment) for comprehensive Docker deployment instructions. 8 | 9 | This page covers the same content as the Getting Started guide but is included here for easy navigation within the Deployment section. 10 | -------------------------------------------------------------------------------- /server/internal/migrations/migrations.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/uptrace/bun/migrate" 7 | ) 8 | 9 | //go:embed *.go 10 | var migrationsFS embed.FS 11 | 12 | var Migrations = migrate.NewMigrations() 13 | 14 | func init() { 15 | if err := Migrations.Discover(migrationsFS); err != nil { 16 | panic(err) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /web/src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import { Collapsible as CollapsiblePrimitive } from 'radix-ui' 2 | 3 | const Collapsible = CollapsiblePrimitive.Root 4 | 5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 6 | 7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 8 | 9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 10 | -------------------------------------------------------------------------------- /server/internal/uuid/uuid.go: -------------------------------------------------------------------------------- 1 | package uuid 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | ) 6 | 7 | // GenerateUUID generates a new UUID v4 string 8 | func GenerateUUID() string { 9 | return uuid.New().String() 10 | } 11 | 12 | // IsValidUUID checks if a string is a valid UUID 13 | func IsValidUUID(u string) bool { 14 | _, err := uuid.Parse(u) 15 | return err == nil 16 | } 17 | -------------------------------------------------------------------------------- /web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import react from '@vitejs/plugin-react-swc' 3 | import { defineConfig } from 'vite' 4 | 5 | export default defineConfig({ 6 | plugins: [react()], 7 | resolve: { 8 | alias: { 9 | '@': path.resolve(__dirname, './src'), 10 | }, 11 | }, 12 | // Configure environment file loading 13 | envDir: './', 14 | envPrefix: 'VITE_', 15 | build: { 16 | outDir: '../server/static', 17 | emptyOutDir: true, 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /web/src/lib/config-storage/memory-config-storage.ts: -------------------------------------------------------------------------------- 1 | import { ConfigStorage } from '@/lib/config-storage/config-storage.ts' 2 | 3 | export class MemoryConfigStorage implements ConfigStorage { 4 | private value: string | null = null 5 | 6 | async get(): Promise { 7 | return this.value 8 | } 9 | 10 | async set(value: string): Promise { 11 | this.value = value 12 | } 13 | 14 | async remove(): Promise { 15 | this.value = null 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "jsxSingleQuote": true, 8 | "bracketSpacing": true, 9 | "bracketSameLine": false, 10 | "importOrder": ["^(react)", "", "", "^@/(.*)$", "", "^[./]"], 11 | "importOrderSeparation": true, 12 | "importOrderSortSpecifiers": true, 13 | "plugins": ["@ianvs/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"], 14 | "pluginSearchDirs": false 15 | } 16 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | Imagor Studio 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /web/src/loaders/admin-setup-loader.ts: -------------------------------------------------------------------------------- 1 | import { getStorageStatus } from '@/api/storage-api' 2 | import type { StorageStatusQuery } from '@/generated/graphql' 3 | import { getAuth } from '@/stores/auth-store.ts' 4 | 5 | export interface AdminSetupLoaderData { 6 | storageStatus?: StorageStatusQuery['storageStatus'] 7 | } 8 | 9 | export const adminSetupLoader = async (): Promise => { 10 | if (!getAuth().accessToken) { 11 | return {} 12 | } 13 | const storageStatus = await getStorageStatus() 14 | return { storageStatus } 15 | } 16 | -------------------------------------------------------------------------------- /web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | /* Bundler mode */ 8 | "moduleResolution": "bundler", 9 | "allowImportingTsExtensions": true, 10 | "isolatedModules": true, 11 | "moduleDetection": "force", 12 | "noEmit": true, 13 | /* Linting */ 14 | "strict": true, 15 | "noImplicitAny": false, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noFallthroughCasesInSwitch": true 19 | }, 20 | "include": ["vite.config.ts"] 21 | } 22 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Git 2 | .git 3 | .gitignore 4 | 5 | # Documentation 6 | README.md 7 | TESTING_GUIDE.md 8 | *.md 9 | 10 | # Development files 11 | .env 12 | .env.local 13 | .env.development 14 | .env.test 15 | 16 | # Node modules (will be installed in container) 17 | web/node_modules 18 | web/.next 19 | web/dist 20 | 21 | # Go build artifacts 22 | server/bin 23 | server/dist 24 | 25 | # IDE files 26 | .vscode 27 | .idea 28 | *.swp 29 | *.swo 30 | 31 | # OS files 32 | .DS_Store 33 | Thumbs.db 34 | 35 | # Test files 36 | *_test.go 37 | test-data 38 | 39 | # Logs 40 | *.log 41 | logs 42 | 43 | # Temporary files 44 | tmp 45 | temp 46 | -------------------------------------------------------------------------------- /web/src/lib/config-storage/local-config-storage.ts: -------------------------------------------------------------------------------- 1 | import { ConfigStorage } from '@/lib/config-storage/config-storage.ts' 2 | 3 | export class LocalConfigStorage implements ConfigStorage { 4 | storageKey: string 5 | 6 | constructor(storageKey: string) { 7 | this.storageKey = storageKey 8 | } 9 | 10 | async get(): Promise { 11 | return localStorage.getItem(this.storageKey) 12 | } 13 | 14 | async set(value: string): Promise { 15 | localStorage.setItem(this.storageKey, value) 16 | } 17 | 18 | async remove(): Promise { 19 | localStorage.removeItem(this.storageKey) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /web/src/lib/config-storage/session-config-storage.ts: -------------------------------------------------------------------------------- 1 | import { ConfigStorage } from '@/lib/config-storage/config-storage.ts' 2 | 3 | export class SessionConfigStorage implements ConfigStorage { 4 | storageKey: string 5 | 6 | constructor(storageKey: string) { 7 | this.storageKey = storageKey 8 | } 9 | 10 | async get(): Promise { 11 | return sessionStorage.getItem(this.storageKey) 12 | } 13 | 14 | async set(value: string): Promise { 15 | sessionStorage.setItem(this.storageKey, value) 16 | } 17 | 18 | async remove(): Promise { 19 | sessionStorage.removeItem(this.storageKey) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /server/internal/model/registry.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/uptrace/bun" 7 | ) 8 | 9 | type Registry struct { 10 | bun.BaseModel `bun:"table:registry,alias:r"` 11 | 12 | ID string `bun:"id,pk,type:text"` 13 | OwnerID string `bun:"owner_id,notnull,type:text"` 14 | Key string `bun:"key,notnull"` 15 | Value string `bun:"value,notnull"` 16 | IsEncrypted bool `bun:"is_encrypted,notnull,default:false"` 17 | CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"` 18 | UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"` 19 | } 20 | -------------------------------------------------------------------------------- /web/src/layouts/content-layout.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react' 2 | 3 | import { LicenseBadge } from '@/components/license-badge.tsx' 4 | 5 | interface ContentLayoutProps { 6 | title: string 7 | isBounded?: boolean 8 | className?: string 9 | } 10 | 11 | export function ContentLayout({ 12 | children, 13 | isBounded, 14 | className, 15 | }: PropsWithChildren) { 16 | return ( 17 |
18 |
21 | 22 | {children} 23 |
24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /web/src/lib/graphql-client.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLClient } from 'graphql-request' 2 | 3 | import { getBaseUrl } from '@/lib/api-utils' 4 | import { getAuth } from '@/stores/auth-store.ts' 5 | 6 | const endpoint = `${getBaseUrl()}/api/query` 7 | 8 | export const createGraphQLClient = (token?: string) => { 9 | const headers: Record = { 10 | 'Content-Type': 'application/json', 11 | } 12 | if (token) { 13 | headers.Authorization = `Bearer ${token}` 14 | } 15 | return new GraphQLClient(endpoint, { headers }) 16 | } 17 | 18 | export const getGraphQLClient = (token?: string) => { 19 | return createGraphQLClient(token || getAuth().accessToken || undefined) 20 | } 21 | -------------------------------------------------------------------------------- /web/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as LabelPrimitive from '@radix-ui/react-label' 3 | 4 | import { cn } from '@/lib/utils' 5 | 6 | function Label({ className, ...props }: React.ComponentProps) { 7 | return ( 8 | 16 | ) 17 | } 18 | 19 | export { Label } 20 | -------------------------------------------------------------------------------- /web/src/i18n.ts: -------------------------------------------------------------------------------- 1 | import { initReactI18next } from 'react-i18next' 2 | import i18n from 'i18next' 3 | import LanguageDetector from 'i18next-browser-languagedetector' 4 | 5 | import enTranslations from '@/locales/en.json' 6 | 7 | const resources = { 8 | en: { 9 | translation: enTranslations, 10 | }, 11 | } 12 | 13 | i18n 14 | .use(LanguageDetector) 15 | .use(initReactI18next) 16 | .init({ 17 | resources, 18 | fallbackLng: 'en', 19 | detection: { 20 | order: ['localStorage', 'navigator'], 21 | caches: ['localStorage'], 22 | }, 23 | interpolation: { 24 | escapeValue: false, // React already safes from XSS 25 | }, 26 | }) 27 | 28 | export default i18n 29 | -------------------------------------------------------------------------------- /server/internal/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/uptrace/bun" 7 | ) 8 | 9 | type User struct { 10 | bun.BaseModel `bun:"table:users,alias:u"` 11 | 12 | ID string `bun:"id,pk,type:text"` 13 | DisplayName string `bun:"display_name,notnull"` 14 | Username string `bun:"username,notnull,unique"` 15 | HashedPassword string `bun:"hashed_password,notnull"` 16 | Role string `bun:"role,notnull,default:'user'"` 17 | IsActive bool `bun:"is_active,notnull,default:true"` 18 | CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"` 19 | UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"` 20 | } 21 | -------------------------------------------------------------------------------- /web/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | /* Linting */ 16 | "strict": true, 17 | "noImplicitAny": false, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["src"] 27 | } 28 | -------------------------------------------------------------------------------- /web/src/components/ui/button-with-loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderCircle } from 'lucide-react' 2 | 3 | import { Button, ButtonProps } from '@/components/ui/button' 4 | 5 | interface ButtonWithLoadingProps extends ButtonProps { 6 | isLoading?: boolean 7 | } 8 | 9 | export const ButtonWithLoading = ({ 10 | children, 11 | isLoading = false, 12 | disabled, 13 | ...props 14 | }: ButtonWithLoadingProps) => { 15 | return ( 16 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /web/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | 8 | /** 9 | * Creates a debounced function that delays invoking func until after wait milliseconds 10 | * have elapsed since the last time the debounced function was invoked. 11 | */ 12 | export function debounce any>( 13 | func: T, 14 | wait: number, 15 | ): (...args: Parameters) => void { 16 | let timeout: NodeJS.Timeout | null = null 17 | 18 | return (...args: Parameters) => { 19 | if (timeout) { 20 | clearTimeout(timeout) 21 | } 22 | timeout = setTimeout(() => { 23 | func(...args) 24 | }, wait) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/gqlgen.yml: -------------------------------------------------------------------------------- 1 | schema: 2 | - ../graphql/*.graphql 3 | exec: 4 | filename: internal/generated/gql/generated.go 5 | package: gql 6 | model: 7 | filename: internal/generated/gql/models_gen.go 8 | package: gql 9 | #resolver: 10 | # layout: follow-schema 11 | # dir: internal/gql 12 | # package: gql 13 | # filename_template: "{name}.resolvers.go" 14 | autobind: [] 15 | models: 16 | ID: 17 | model: 18 | - github.com/99designs/gqlgen/graphql.ID 19 | - github.com/99designs/gqlgen/graphql.Int 20 | - github.com/99designs/gqlgen/graphql.Int64 21 | - github.com/99designs/gqlgen/graphql.Int32 22 | Int: 23 | model: 24 | - github.com/99designs/gqlgen/graphql.Int 25 | - github.com/99designs/gqlgen/graphql.Int64 26 | - github.com/99designs/gqlgen/graphql.Int32 27 | -------------------------------------------------------------------------------- /web/src/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | export interface ProgressProps extends React.HTMLAttributes { 6 | value?: number 7 | max?: number 8 | className?: string 9 | } 10 | 11 | export function Progress({ value = 0, max = 100, className, ...props }: ProgressProps) { 12 | const percentage = Math.min(Math.max((value / max) * 100, 0), 100) 13 | 14 | return ( 15 |
19 |
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /web/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | const Textarea = React.forwardRef>( 6 | ({ className, ...props }, ref) => { 7 | return ( 8 |