├── .gitmodules ├── backend ├── services │ ├── tasks │ │ ├── tasks.go │ │ └── mail.go │ ├── error.go │ └── auth.go ├── static │ └── templates │ │ └── mails │ │ ├── text │ │ └── kyc.txt │ │ └── kyc.html ├── utils │ ├── time.go │ ├── commons.go │ └── html.go ├── integrations │ ├── error.go │ └── email │ │ └── email.go ├── adapters │ ├── repository │ │ ├── error.go │ │ ├── db.go │ │ └── member.go │ ├── handler │ │ ├── params.go │ │ ├── auth.go │ │ └── middleware.go │ ├── xhandler │ │ ├── middleware.go │ │ ├── auth.go │ │ └── handler.go │ └── store │ │ └── s3store.go ├── .vscode │ └── settings.json ├── infra │ └── srv │ │ ├── scripts │ │ ├── before.sh │ │ ├── after.sh │ │ ├── validate.sh │ │ ├── stop.sh │ │ └── start.sh │ │ └── appspec.yml ├── srv.DockerFile ├── xsrv.DockerFile ├── models │ ├── member.go │ ├── settings.go │ └── customer.go ├── go.mod ├── cmd │ └── xsrv │ │ └── main.go └── config.go ├── frontend ├── .prettierrc ├── vercel.json ├── postcss.config.js ├── .env.example ├── src │ ├── vite-env.d.ts │ ├── lib │ │ ├── utils.ts │ │ └── search-params.ts │ ├── routes │ │ ├── (auth) │ │ │ ├── recover.tsx │ │ │ └── signout.tsx │ │ ├── _account │ │ │ ├── workspaces │ │ │ │ ├── $workspaceId │ │ │ │ │ ├── setup │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── _workspace │ │ │ │ │ │ ├── threads │ │ │ │ │ │ │ ├── spam.tsx │ │ │ │ │ │ │ ├── route.tsx │ │ │ │ │ │ │ ├── done.tsx │ │ │ │ │ │ │ └── labels.$labelId.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── search.tsx │ │ │ │ │ │ └── route.tsx │ │ │ │ │ └── settings │ │ │ │ │ │ ├── chat.tsx │ │ │ │ │ │ ├── slack.tsx │ │ │ │ │ │ ├── billing.tsx │ │ │ │ │ │ ├── events.tsx │ │ │ │ │ │ ├── github.tsx │ │ │ │ │ │ ├── linear.tsx │ │ │ │ │ │ ├── webhooks.tsx │ │ │ │ │ │ ├── notifications.tsx │ │ │ │ │ │ ├── route.tsx │ │ │ │ │ │ └── ai.tsx │ │ │ │ └── $workspaceId.tsx │ │ │ └── route.tsx │ │ ├── index.tsx │ │ └── __root.tsx │ ├── hooks │ │ └── theme.ts │ ├── components │ │ ├── ui │ │ │ ├── skeleton.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── label.tsx │ │ │ ├── textarea.tsx │ │ │ ├── separator.tsx │ │ │ ├── input.tsx │ │ │ ├── toaster.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── switch.tsx │ │ │ ├── badge.tsx │ │ │ ├── tooltip.tsx │ │ │ ├── popover.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── avatar.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── alert.tsx │ │ │ ├── resizable.tsx │ │ │ ├── button.tsx │ │ │ ├── tabs.tsx │ │ │ ├── card.tsx │ │ │ └── dialog.tsx │ │ ├── workspace │ │ │ ├── thread-list.tsx │ │ │ ├── thread │ │ │ │ ├── sidepanel-thread-list.tsx │ │ │ │ ├── message-form.tsx │ │ │ │ └── threads.tsx │ │ │ ├── sorts.tsx │ │ │ ├── sidenav-mobile-links.tsx │ │ │ ├── settings │ │ │ │ ├── sidenav-mobile-links.tsx │ │ │ │ └── email │ │ │ │ │ └── dns.tsx │ │ │ ├── header.tsx │ │ │ ├── insights │ │ │ │ └── overview.tsx │ │ │ └── thread-list-item.tsx │ │ ├── spinner.tsx │ │ ├── notfound.tsx │ │ ├── theme-toggler.tsx │ │ └── thread │ │ │ └── customer-events.tsx │ ├── db │ │ ├── constants.ts │ │ └── helpers.ts │ ├── main.tsx │ ├── providers.tsx │ └── assets │ │ └── react.svg ├── tsconfig.node.json ├── components.json ├── index.html ├── .vscode │ └── settings.json ├── .eslintrc.cjs ├── tsconfig.json ├── vite.config.ts ├── README.md ├── public │ └── vite.svg ├── tailwind.config.ts └── package.json ├── .dockerignore ├── zyg.code-workspace ├── .vscode └── settings.json ├── .github └── workflows │ └── audit.yml ├── templates ├── package.json └── emails │ └── kyc.tsx ├── .env.example ├── README.md ├── .gitignore └── docker-compose.yml /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/services/tasks/tasks.go: -------------------------------------------------------------------------------- 1 | package tasks 2 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"] 3 | } 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .env.example 4 | .gitignore 5 | 6 | node_modules 7 | dist 8 | -------------------------------------------------------------------------------- /frontend/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/(.*)", "destination": "/" }] 3 | } 4 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | VITE_ZYG_URL= 2 | VITE_SUPABASE_URL= 3 | VITE_SUPABASE_ANON_KEY= 4 | VITE_SENTRY_ENV= 5 | VITE_SENTRY_ENABLED=1 6 | VITE_SENTRY_TELEMETRY_ENABLED=1 -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_SENTRY_ENABLED: string; 5 | readonly VITE_SENTRY_ENV: string; 6 | } 7 | -------------------------------------------------------------------------------- /backend/static/templates/mails/text/kyc.txt: -------------------------------------------------------------------------------- 1 | You started a conversation. 2 | 3 | {{ .PreviewText }} 4 | 5 | Verify your email {{ .MagicLink }} 6 | 7 | ❤️ Zyg ・ Open source, made with love around the world ❤️ -------------------------------------------------------------------------------- /frontend/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/routes/(auth)/recover.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | 3 | export const Route = createFileRoute("/(auth)/recover")({ 4 | component: () =>
Hello /recover!
, 5 | }); 6 | -------------------------------------------------------------------------------- /backend/utils/time.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "time" 4 | 5 | func FromRFC3339OrNow(ts string) time.Time { 6 | t, err := time.Parse(time.RFC3339, ts) 7 | if err != nil { 8 | now := time.Now().UTC() 9 | return now 10 | } 11 | return t 12 | } 13 | -------------------------------------------------------------------------------- /backend/integrations/error.go: -------------------------------------------------------------------------------- 1 | package integrations 2 | 3 | type integrationErr string 4 | 5 | func (err integrationErr) Error() string { 6 | return string(err) 7 | } 8 | 9 | const ( 10 | ErrPostmarkSendMail = integrationErr("postmark send mail error") 11 | ) 12 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /backend/adapters/repository/error.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | type dbErr string 4 | 5 | func (err dbErr) Error() string { 6 | return string(err) 7 | } 8 | 9 | const ( 10 | ErrEmpty = dbErr("got nothing") 11 | ErrQuery = dbErr("db query failed") 12 | ErrTxQuery = dbErr("db tx query failed") 13 | ) 14 | -------------------------------------------------------------------------------- /backend/adapters/handler/params.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | //var StatusParams = map[string]bool{ 4 | // "todo": true, 5 | // "snoozed": true, 6 | // "done": true, 7 | // "unsnoozed": true, 8 | //} 9 | // 10 | //var ReasonParams = map[string]bool{ 11 | // "replied": true, 12 | // "unreplied": true, 13 | //} 14 | -------------------------------------------------------------------------------- /backend/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.fontFamily": "'Cascadia Mono', 'SauceCodePro Nerd Font', Consolas, monospace", 3 | "cSpell.words": [ 4 | "addedby", 5 | "inbc", 6 | "oubm", 7 | "segmentio", 8 | "sess", 9 | "templ", 10 | "xhandler" 11 | ], 12 | "cSpell.ignoreWords": [ 13 | "mpim" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/hooks/theme.ts: -------------------------------------------------------------------------------- 1 | import { ThemeProviderContext } from "@/providers"; 2 | import React from "react"; 3 | 4 | export const useTheme = () => { 5 | const context = React.useContext(ThemeProviderContext); 6 | 7 | if (context === undefined) 8 | throw new Error("useTheme must be used within a ThemeProvider"); 9 | 10 | return context; 11 | }; 12 | -------------------------------------------------------------------------------- /frontend/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /frontend/src/routes/_account/workspaces/$workspaceId/setup/index.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | 3 | export const Route = createFileRoute("/_account/workspaces/$workspaceId/setup/")({ 4 | component: WorkspaceSetup, 5 | }); 6 | 7 | function WorkspaceSetup() { 8 | return
...
; 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 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 | -------------------------------------------------------------------------------- /frontend/src/routes/_account/workspaces/$workspaceId/_workspace/threads/spam.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | 3 | export const Route = createFileRoute( 4 | "/_account/workspaces/$workspaceId/_workspace/threads/spam" 5 | )({ 6 | component: () => ( 7 |
Hello /_account/workspaces/$workspaceId/_workspace/threads/spam!
8 | ), 9 | }); 10 | -------------------------------------------------------------------------------- /backend/utils/commons.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "encoding/json" 4 | 5 | func StructToMap(obj interface{}) (map[string]interface{}, error) { 6 | data, err := json.Marshal(obj) 7 | if err != nil { 8 | return nil, err 9 | } 10 | 11 | var mapData map[string]interface{} 12 | err = json.Unmarshal(data, &mapData) 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | return mapData, nil 18 | } 19 | -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/routes/_account/workspaces/$workspaceId/_workspace/index.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute, redirect } from "@tanstack/react-router"; 2 | 3 | export const Route = createFileRoute( 4 | "/_account/workspaces/$workspaceId/_workspace/", 5 | )({ 6 | component: () => null, 7 | loader: ({ params }) => { 8 | throw redirect({ 9 | params, 10 | to: "/workspaces/$workspaceId/threads/todo", 11 | }); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /zyg.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "ai", 5 | "name": "ai" 6 | }, 7 | { 8 | "path": "backend", 9 | "name": "backend" 10 | }, 11 | { 12 | "path": "frontend", 13 | "name": "frontend" 14 | }, 15 | { 16 | "path": ".", 17 | "name": "zyg" 18 | }, 19 | { 20 | "path": "templates", 21 | "name": "templates" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vite + React + TS 9 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute, redirect } from "@tanstack/react-router"; 2 | 3 | // TODO: redirect to last used workspace. 4 | export const Route = createFileRoute("/")({ 5 | beforeLoad: async ({ context }) => { 6 | const { supabaseClient } = context; 7 | const { data, error } = await supabaseClient.auth.getSession(); 8 | const isAuthenticated = !error && data?.session; 9 | if (!isAuthenticated) { 10 | throw redirect({ to: "/signin" }); 11 | } 12 | }, 13 | component: () =>
Index Root at /
, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/src/components/workspace/thread-list.tsx: -------------------------------------------------------------------------------- 1 | import { ThreadLinkItem } from "@/components/workspace/thread-list-item"; 2 | import { Thread } from "@/db/models"; 3 | import { Virtuoso } from "react-virtuoso"; 4 | 5 | export function ThreadList({ 6 | threads, 7 | workspaceId, 8 | }: { 9 | threads: Thread[]; 10 | workspaceId: string; 11 | }) { 12 | return ( 13 | ( 15 | 16 | )} 17 | totalCount={threads.length} 18 | useWindowScroll 19 | /> 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/db/constants.ts: -------------------------------------------------------------------------------- 1 | // Top level thread status. 2 | export const STATUS_TODO = "todo"; 3 | // export const STATUS_DONE = "done"; 4 | 5 | // in TODO workflow stages. 6 | export const NEEDS_NEXT_RESPONSE = "needs_next_response"; 7 | export const NEEDS_FIRST_RESPONSE = "needs_first_response"; 8 | export const HOLD = "hold"; 9 | export const WAITING_ON_CUSTOMER = "waiting_on_customer"; 10 | 11 | // in DONE workflow stages. 12 | export const RESOLVED = "resolved"; 13 | 14 | // in other workflow, reserved for later usage. 15 | // for other status+stages, haven't decided yet. 16 | export const SPAM = "spam"; 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true, 8 | "**/Thumbs.db": true, 9 | "**/node_modules": true, 10 | "**/dist": true, 11 | "**/.next": true, 12 | "**/out": true, 13 | "**/yarn.lock": true, 14 | "**/package-lock.json": true, 15 | "**/coverage": true, 16 | "deprecated": true, 17 | "ai": false, 18 | "frontend": false, 19 | "widget": false, 20 | "backend": false 21 | }, 22 | "cSpell.words": [ 23 | "appspec", 24 | "XSRV" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /backend/infra/srv/scripts/before.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Create necessary directories if they don't exist 5 | sudo mkdir -p /usr/local/bin 6 | 7 | # Set ownership to ubuntu user 8 | sudo chown ubuntu:ubuntu /usr/local/bin/app || true 9 | 10 | # Set proper permissions 11 | sudo chmod 755 /usr/local/bin/app || true 12 | 13 | # Ensure systemd directory exists and has correct permissions 14 | sudo mkdir -p /etc/systemd/system 15 | sudo chmod 755 /etc/systemd/system 16 | 17 | # Set ownership to ubuntu user 18 | sudo chown ubuntu:ubuntu /etc/systemd/system/srv.service || true 19 | 20 | # Stop the service if it's running (ignore if it fails) 21 | sudo systemctl stop srv.service || true 22 | -------------------------------------------------------------------------------- /frontend/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.fontFamily": "'Cascadia Mono', 'SauceCodePro Nerd Font', Consolas, monospace", 3 | "editor.formatOnSave": true, 4 | "files.associations": { 5 | "*.css": "tailwindcss" 6 | }, 7 | "tailwindCSS.includeLanguages": { 8 | "plaintext": "html" 9 | }, 10 | "tailwindCSS.colorDecorators": true, 11 | "editor.quickSuggestions": { 12 | "strings": "on" 13 | }, 14 | "files.autoSave": "afterDelay", 15 | "files.autoSaveDelay": 1000, 16 | "cSpell.enableFiletypes": ["typescriptreact"], 17 | "cSpell.words": ["hookform", "overscan", "sidenav", "signin", "signout"], 18 | "editor.codeActionsOnSave": { 19 | "source.fixAll": "explicit" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/infra/srv/scripts/after.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Log all commands for debugging 5 | exec 1> >(logger -s -t $(basename $0)) 2>&1 6 | 7 | echo "Starting after-install script execution..." 8 | 9 | # Reload systemd to recognize new or modified service file 10 | echo "Reloading systemd daemon..." 11 | sudo systemctl daemon-reload 12 | 13 | # Enable the service to start on boot 14 | echo "Enabling srv service..." 15 | sudo systemctl enable srv.service 16 | 17 | # Verify application binary exists and is executable 18 | if [ ! -x "/usr/local/bin/app" ]; then 19 | echo "ERROR: Application binary is missing or not executable" 20 | exit 1 21 | fi 22 | 23 | echo "After-install script completed successfully" 24 | exit 0 25 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Audit 2 | 3 | on: 4 | workflow_dispatch: {} 5 | push: 6 | branches: [main] 7 | paths: 8 | - 'backend/**' 9 | pull_request: 10 | branches: [main] 11 | paths: 12 | - 'backend/**' 13 | 14 | jobs: 15 | audit: 16 | name: Audit 17 | runs-on: ubuntu-latest 18 | defaults: 19 | run: 20 | working-directory: ./backend 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: "1.23" 28 | check-latest: true 29 | cache: true 30 | 31 | - name: Verify 32 | run: go mod verify 33 | 34 | - name: Vet 35 | run: go vet ./... 36 | 37 | -------------------------------------------------------------------------------- /backend/infra/srv/scripts/validate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | SERVICE_NAME="srv.service" 5 | 6 | # Function to check if service is running 7 | check_service() { 8 | local service_name=$1 9 | 10 | echo "Checking if service $service_name is running..." 11 | if ! systemctl is-active --quiet "$service_name"; then 12 | echo "ERROR: Service $service_name is not running" 13 | systemctl status "$service_name" 14 | return 1 15 | fi 16 | echo "Service $service_name is running" 17 | return 0 18 | } 19 | 20 | main() { 21 | local service=${3:-$SERVICE_NAME} 22 | 23 | check_service "$service" || exit 1 24 | 25 | echo "All validations passed successfully" 26 | exit 0 27 | } 28 | 29 | # Run main with command line arguments 30 | main "$@" 31 | -------------------------------------------------------------------------------- /templates/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "templates", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "email dev", 8 | "export:html": "email export --pretty --outDir ../backend/static/templates/mails", 9 | "export:text": "email export --pretty --plainText --outDir ../backend/static/templates/mails/text" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@types/react": "^18.3.11", 16 | "@types/react-dom": "^18.3.0", 17 | "react-email": "3.0.1" 18 | }, 19 | "dependencies": { 20 | "@react-email/components": "0.0.25", 21 | "@react-email/render": "1.0.1", 22 | "react": "^18.3.1", 23 | "react-dom": "^18.3.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:react-hooks/recommended", 8 | "plugin:perfectionist/recommended-natural-legacy", 9 | ], 10 | ignorePatterns: [ 11 | "dist", 12 | ".eslintrc.cjs", 13 | "**/components/ui/**", 14 | "postcss.config.js", 15 | "tailwind.config.js", 16 | ], 17 | parser: "@typescript-eslint/parser", 18 | plugins: ["react-refresh", "perfectionist"], 19 | rules: { 20 | // "react-refresh/only-export-components": [ 21 | // "warn", 22 | // { allowConstantExport: true }, 23 | // ], 24 | "@typescript-eslint/no-explicit-any": "off", 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /frontend/src/components/spinner.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export interface SpinnerProps extends React.SVGProps { 4 | size?: number; 5 | } 6 | 7 | export const Spinner = React.forwardRef( 8 | ({ size = 32, ...props }, ref) => { 9 | return ( 10 | 23 | 24 | 25 | ); 26 | }, 27 | ); 28 | 29 | Spinner.displayName = "Spinner"; 30 | -------------------------------------------------------------------------------- /frontend/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | /* shadcn/ui */ 24 | "baseUrl": ".", 25 | "paths": { 26 | "@/*": ["./src/*"] 27 | } 28 | }, 29 | "include": ["src"], 30 | "references": [{ "path": "./tsconfig.node.json" }] 31 | } 32 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | POSTGRES_USER=zygdev 2 | POSTGRES_PASSWORD=VeryS3cure 3 | POSTGRES_DB=zygdb 4 | DATABASE_HOST=database:5432 5 | DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DATABASE_HOST}/${POSTGRES_DB} 6 | 7 | REDIS_ADDR=redis:6379 8 | REDIS_USERNAME=zygdev 9 | REDIS_PASSWORD=redispass 10 | REDIS_TLS_ENABLED=0 11 | 12 | # Get these from Supabase console. 13 | SUPABASE_JWT_SECRET= 14 | 15 | # Resend for Email APIs. 16 | RESEND_API_KEY= 17 | 18 | # Cloudflare AccountID 19 | CF_ACCOUNT_ID= 20 | 21 | # Access key for S3 compat R2 storage 22 | R2_ACCESS_KEY_ID= 23 | 24 | # Access secret for S3 compat R2 storage 25 | R2_ACCESS_SECRET_KEY= 26 | 27 | # Set this to 1 if you want db queries in logs. 28 | ZYG_DB_QUERY_DEBUG=0 29 | 30 | # Main API server port 31 | ZYG_SRV_PORT=8080 32 | 33 | # External API server port 34 | ZYG_XSRV_PORT=8000 35 | 36 | -------------------------------------------------------------------------------- /frontend/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |