more boilerplate and dev environment
This commit is contained in:
+8
-2
@@ -20,8 +20,14 @@ func InitDB() (*sql.DB, error) {
|
|||||||
password = "secret"
|
password = "secret"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the DSN. The host "db" matches the service name in docker-compose.yml.
|
host := os.Getenv("DB_HOST")
|
||||||
dsn := fmt.Sprintf("oworganizer:%s@tcp(db:3306)/?parseTime=true", password)
|
if host == "" {
|
||||||
|
// Default to the Docker Compose service name.
|
||||||
|
host = "db"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the DSN. The default host "db" matches the service name in docker-compose.yml.
|
||||||
|
dsn := fmt.Sprintf("oworganizer:%s@tcp(%s:3306)/?parseTime=true", password, host)
|
||||||
|
|
||||||
db, err := sql.Open("mysql", dsn)
|
db, err := sql.Open("mysql", dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ require (
|
|||||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.7 // indirect
|
github.com/cloudwego/base64x v0.1.7 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
|
github.com/gin-contrib/cors v1.7.7 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.1 // indirect
|
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/gin-contrib/cors v1.7.7 h1:Oh9joP463x7Mw72vhvJ61YQm8ODh9b04YR7vsOErD0Q=
|
||||||
|
github.com/gin-contrib/cors v1.7.7/go.mod h1:K5tW0RkzJtWSiOdikXloy8VEZlgdVNpHNw8FpjUPNrE=
|
||||||
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
|
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
|
||||||
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
|
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
|
||||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package main
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,6 +19,20 @@ func main() {
|
|||||||
|
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
|
|
||||||
|
// Configure CORS. In production, set CORS_ALLOWED_ORIGINS to your public domain.
|
||||||
|
allowedOrigins := []string{"http://localhost:5173"}
|
||||||
|
if envOrigins := os.Getenv("CORS_ALLOWED_ORIGINS"); envOrigins != "" {
|
||||||
|
allowedOrigins = strings.Split(envOrigins, ",")
|
||||||
|
}
|
||||||
|
router.Use(cors.New(cors.Config{
|
||||||
|
AllowOrigins: allowedOrigins,
|
||||||
|
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||||
|
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
|
||||||
|
ExposeHeaders: []string{"Content-Length"},
|
||||||
|
AllowCredentials: true,
|
||||||
|
MaxAge: 12 * 60 * 60,
|
||||||
|
}))
|
||||||
|
|
||||||
router.GET("/ping", func(c *gin.Context) {
|
router.GET("/ping", func(c *gin.Context) {
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"message": "pong",
|
"message": "pong",
|
||||||
|
|||||||
+9
-2
@@ -1,12 +1,19 @@
|
|||||||
services:
|
services:
|
||||||
|
frontend:
|
||||||
|
build: ./frontend
|
||||||
|
container_name: ${SERVICE_NAME}-oworganizer-frontend
|
||||||
|
ports:
|
||||||
|
- "3000:80"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
build: ./backend
|
build: ./backend
|
||||||
container_name: ${SERVICE_NAME}-oworganizer-backend
|
container_name: ${SERVICE_NAME}-oworganizer-backend
|
||||||
environment:
|
environment:
|
||||||
DB_PASSWORD: ${DB_PASSWORD}
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
# volumes:
|
# volumes:
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
db:
|
db:
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Development API base URL.
|
||||||
|
# In production, the app uses the relative /api path and nginx proxies it to the backend.
|
||||||
|
VITE_API_BASE_URL=http://localhost:8080
|
||||||
|
|
||||||
|
# Vite dev server port. The dev script expects the frontend at http://localhost:5173.
|
||||||
|
# PORT=5173
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy dependency files first for better layer caching
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy source code and build the static SPA
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy the built static files to nginx's default document root
|
||||||
|
COPY --from=builder /app/build /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy nginx configuration
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Expose the port nginx listens on
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Run nginx in the foreground
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Gzip compression for static assets
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|otf)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proxy API requests to the backend container
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://backend:8080/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Serve the SPA and fall back to index.html for client-side routes
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+5
-5
@@ -8,7 +8,7 @@
|
|||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^7.0.1",
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
"@sveltejs/kit": "^2.63.0",
|
"@sveltejs/kit": "^2.63.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||||
"svelte": "^5.56.1",
|
"svelte": "^5.56.1",
|
||||||
@@ -434,10 +434,10 @@
|
|||||||
"acorn": "^8.9.0"
|
"acorn": "^8.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sveltejs/adapter-auto": {
|
"node_modules/@sveltejs/adapter-static": {
|
||||||
"version": "7.0.1",
|
"version": "3.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.8.tgz",
|
||||||
"integrity": "sha512-dvuPm1E7M9NI/+canIQ6KKQDU2AkEefEZ2Dp7cY6uKoPq9Z/PhOXABe526UdW2mN986gjVkuSLkOYIBnS/M2LQ==",
|
"integrity": "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"prepare": "svelte-kit sync || echo ''"
|
"prepare": "svelte-kit sync || echo ''"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^7.0.1",
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
"@sveltejs/kit": "^2.63.0",
|
"@sveltejs/kit": "^2.63.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||||
"svelte": "^5.56.1",
|
"svelte": "^5.56.1",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
let { message = 'Loading...' } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p>{message}</p>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {Object} Note
|
||||||
|
* @property {string} objectId
|
||||||
|
* @property {'note'} type
|
||||||
|
* @property {string} text
|
||||||
|
* @property {string[]} childIds
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Note object.
|
||||||
|
*
|
||||||
|
* @param {string} objectId
|
||||||
|
* @param {string} text
|
||||||
|
* @param {string[]} [childIds=[]]
|
||||||
|
* @returns {Note}
|
||||||
|
*/
|
||||||
|
export function createNoteModel(objectId, text, childIds = []) {
|
||||||
|
return { objectId, type: 'note', text, childIds };
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {Object} ObjectRecord
|
||||||
|
* @property {string} objectId
|
||||||
|
* @property {string} userId
|
||||||
|
* @property {string} objectData
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an ObjectRecord object.
|
||||||
|
*
|
||||||
|
* @param {string} objectId
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {string} objectData
|
||||||
|
* @returns {ObjectRecord}
|
||||||
|
*/
|
||||||
|
export function createObjectModel(objectId, userId, objectData) {
|
||||||
|
return { objectId, userId, objectData };
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {Object} Project
|
||||||
|
* @property {string} objectId
|
||||||
|
* @property {'project'} type
|
||||||
|
* @property {string} name
|
||||||
|
* @property {string[]} childIds
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Project object.
|
||||||
|
*
|
||||||
|
* @param {string} objectId
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string[]} [childIds=[]]
|
||||||
|
* @returns {Project}
|
||||||
|
*/
|
||||||
|
export function createProjectModel(objectId, name, childIds = []) {
|
||||||
|
return { objectId, type: 'project', name, childIds };
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {Object} Root
|
||||||
|
* @property {string} objectId
|
||||||
|
* @property {'root'} type
|
||||||
|
* @property {string[]} childIds
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Root object.
|
||||||
|
*
|
||||||
|
* @param {string} objectId
|
||||||
|
* @param {string[]} [childIds=[]]
|
||||||
|
* @returns {Root}
|
||||||
|
*/
|
||||||
|
export function createRootModel(objectId, childIds = []) {
|
||||||
|
return { objectId, type: 'root', childIds };
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {'not-started' | 'in-progress' | 'completed' | 'cancelled'} TaskStatus
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Task
|
||||||
|
* @property {string} objectId
|
||||||
|
* @property {'task'} type
|
||||||
|
* @property {string} text
|
||||||
|
* @property {TaskStatus} status
|
||||||
|
* @property {string[]} childIds
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Task object.
|
||||||
|
*
|
||||||
|
* @param {string} objectId
|
||||||
|
* @param {string} text
|
||||||
|
* @param {TaskStatus} [status='not-started']
|
||||||
|
* @param {string[]} [childIds=[]]
|
||||||
|
* @returns {Task}
|
||||||
|
*/
|
||||||
|
export function createTaskModel(objectId, text, status = 'not-started', childIds = []) {
|
||||||
|
return { objectId, type: 'task', text, status, childIds };
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {Object} User
|
||||||
|
* @property {string} userId
|
||||||
|
* @property {string} rootObjectId
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a User object.
|
||||||
|
*
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {string} rootObjectId
|
||||||
|
* @returns {User}
|
||||||
|
*/
|
||||||
|
export function createUserModel(userId, rootObjectId) {
|
||||||
|
return { userId, rootObjectId };
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Base URL for API requests.
|
||||||
|
* In development, point to the Go backend directly (e.g., http://localhost:8080).
|
||||||
|
* In production, use the relative /api path so nginx proxies to the backend.
|
||||||
|
*/
|
||||||
|
const API_BASE = import.meta.env.VITE_API_BASE_URL || '/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around fetch for calling the backend API.
|
||||||
|
*
|
||||||
|
* @param {string} path - API path, e.g., `/users` or `/objects/123`.
|
||||||
|
* @param {RequestInit} [options={}] - fetch options.
|
||||||
|
* @returns {Promise<any>} Parsed JSON response.
|
||||||
|
* @throws {Error} On non-OK responses.
|
||||||
|
*/
|
||||||
|
export async function apiFetch(path, options = {}) {
|
||||||
|
const url = `${API_BASE}${path}`;
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.body && typeof config.body === 'object' && !(config.body instanceof FormData)) {
|
||||||
|
config.body = JSON.stringify(config.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, config);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
throw new Error(text || `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { apiFetch } from './api.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ObjectRecord
|
||||||
|
* @property {string} objectId
|
||||||
|
* @property {string} userId
|
||||||
|
* @property {string} objectData
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new object.
|
||||||
|
*
|
||||||
|
* @param {string} objectId
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {string} objectData
|
||||||
|
* @returns {Promise<{status: string}>}
|
||||||
|
*/
|
||||||
|
export function createObject(objectId, userId, objectData) {
|
||||||
|
return apiFetch('/objects', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { objectId, userId, objectData }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch an object by ID.
|
||||||
|
*
|
||||||
|
* @param {string} objectId
|
||||||
|
* @returns {Promise<ObjectRecord>}
|
||||||
|
*/
|
||||||
|
export function getObject(objectId) {
|
||||||
|
return apiFetch(`/objects/${objectId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all objects belonging to a user.
|
||||||
|
*
|
||||||
|
* @param {string} userId
|
||||||
|
* @returns {Promise<ObjectRecord[]>}
|
||||||
|
*/
|
||||||
|
export function getObjectsByUser(userId) {
|
||||||
|
return apiFetch(`/users/${userId}/objects`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an object's data.
|
||||||
|
*
|
||||||
|
* @param {string} objectId
|
||||||
|
* @param {string} objectData
|
||||||
|
* @returns {Promise<{status: string}>}
|
||||||
|
*/
|
||||||
|
export function updateObject(objectId, objectData) {
|
||||||
|
return apiFetch(`/objects/${objectId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: { objectData }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an object.
|
||||||
|
*
|
||||||
|
* @param {string} objectId
|
||||||
|
* @returns {Promise<{status: string}>}
|
||||||
|
*/
|
||||||
|
export function deleteObject(objectId) {
|
||||||
|
return apiFetch(`/objects/${objectId}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { apiFetch } from './api.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} User
|
||||||
|
* @property {string} userId
|
||||||
|
* @property {string} rootObjectId
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new user.
|
||||||
|
*
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {string} rootObjectId
|
||||||
|
* @returns {Promise<{status: string}>}
|
||||||
|
*/
|
||||||
|
export function createUser(userId, rootObjectId) {
|
||||||
|
return apiFetch('/users', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { userId, rootObjectId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a user by ID.
|
||||||
|
*
|
||||||
|
* @param {string} userId
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
export function getUser(userId) {
|
||||||
|
return apiFetch(`/users/${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a user's root object.
|
||||||
|
*
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {string} rootObjectId
|
||||||
|
* @returns {Promise<{status: string}>}
|
||||||
|
*/
|
||||||
|
export function updateUser(userId, rootObjectId) {
|
||||||
|
return apiFetch(`/users/${userId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: { rootObjectId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a user.
|
||||||
|
*
|
||||||
|
* @param {string} userId
|
||||||
|
* @returns {Promise<{status: string}>}
|
||||||
|
*/
|
||||||
|
export function deleteUser(userId) {
|
||||||
|
return apiFetch(`/users/${userId}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple auth store. Replace with real authentication logic as needed.
|
||||||
|
*/
|
||||||
|
function createAuthStore() {
|
||||||
|
const { subscribe, set } = writable(null);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
login: (/** @type {string} */ userId) => set(userId),
|
||||||
|
logout: () => set(null)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const auth = createAuthStore();
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export const ssr = false;
|
||||||
|
export const prerender = false;
|
||||||
@@ -1,2 +1,25 @@
|
|||||||
<h1>Welcome to SvelteKit</h1>
|
<script>
|
||||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
import { goto } from '$app/navigation';
|
||||||
|
import { auth } from '$lib/stores/auth.js';
|
||||||
|
|
||||||
|
let userId = $state('');
|
||||||
|
|
||||||
|
function enterApp() {
|
||||||
|
if (userId.trim()) {
|
||||||
|
auth.login(userId.trim());
|
||||||
|
goto('/dashboard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1>OwOrganizer</h1>
|
||||||
|
|
||||||
|
<p>Enter your user ID to open the app.</p>
|
||||||
|
|
||||||
|
<form onsubmit={(e) => { e.preventDefault(); enterApp(); }}>
|
||||||
|
<label>
|
||||||
|
User ID
|
||||||
|
<input type="text" bind:value={userId} placeholder="e.g., alice" />
|
||||||
|
</label>
|
||||||
|
<button type="submit">Open App</button>
|
||||||
|
</form>
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script>
|
||||||
|
import { auth } from '$lib/stores/auth.js';
|
||||||
|
import { getObjectsByUser } from '$lib/services/objects.js';
|
||||||
|
import Status from '$lib/components/Status.svelte';
|
||||||
|
|
||||||
|
let userId = $derived($auth);
|
||||||
|
let objects = $state([]);
|
||||||
|
let loading = $state(true);
|
||||||
|
let error = $state(null);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!userId) return;
|
||||||
|
loading = true;
|
||||||
|
getObjectsByUser(userId)
|
||||||
|
.then((data) => {
|
||||||
|
objects = data || [];
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.message;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !userId}
|
||||||
|
<p>You are not logged in. <a href="/">Go home</a></p>
|
||||||
|
{:else}
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
<p>Welcome, {userId}.</p>
|
||||||
|
|
||||||
|
<h2>Your Objects</h2>
|
||||||
|
{#if loading}
|
||||||
|
<Status message="Loading objects..." />
|
||||||
|
{:else if error}
|
||||||
|
<p style="color: red">Error: {error}</p>
|
||||||
|
{:else if objects.length === 0}
|
||||||
|
<p>No objects found.</p>
|
||||||
|
{:else}
|
||||||
|
<ul>
|
||||||
|
{#each objects as obj (obj.objectId)}
|
||||||
|
<li>
|
||||||
|
<strong>{obj.objectId}</strong>: {obj.objectData}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-static';
|
||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
compilerOptions: {
|
||||||
|
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
|
||||||
|
runes: ({ filename }) =>
|
||||||
|
filename.split(/[/\\]/).includes('node_modules') ? undefined : true
|
||||||
|
},
|
||||||
|
kit: {
|
||||||
|
adapter: adapter({
|
||||||
|
// Output static files to the build directory.
|
||||||
|
// This is the default, but we make it explicit.
|
||||||
|
pages: 'build',
|
||||||
|
assets: 'build',
|
||||||
|
fallback: 'index.html',
|
||||||
|
precompress: false,
|
||||||
|
strict: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
+1
-15
@@ -1,20 +1,6 @@
|
|||||||
import adapter from '@sveltejs/adapter-auto';
|
|
||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [sveltekit()]
|
||||||
sveltekit({
|
|
||||||
compilerOptions: {
|
|
||||||
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
|
|
||||||
runes: ({ filename }) =>
|
|
||||||
filename.split(/[/\\]/).includes('node_modules') ? undefined : true
|
|
||||||
},
|
|
||||||
|
|
||||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
|
||||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
|
||||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
|
||||||
adapter: adapter()
|
|
||||||
})
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|||||||
Executable
+157
@@ -0,0 +1,157 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Dev startup script for OwOrganizer.
|
||||||
|
# Runs the database in Docker and the backend/frontend natively.
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
DB_CONTAINER_NAME="oworganizer-dev-db"
|
||||||
|
DB_ROOT_PASSWORD="${DB_ROOT_PASSWORD:-top-secret}"
|
||||||
|
DB_PASSWORD="${DB_PASSWORD:-secret}"
|
||||||
|
DB_USER="oworganizer"
|
||||||
|
DB_NAME="oworganizer"
|
||||||
|
DB_PORT="${DB_PORT:-3306}"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RESET='\033[0m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo -e "${GREEN}[dev]${RESET} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "${YELLOW}[dev]${RESET} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
echo
|
||||||
|
warn "Shutting down dev environment..."
|
||||||
|
|
||||||
|
if [[ -n "${BACKEND_PID:-}" ]] && kill -0 "$BACKEND_PID" 2>/dev/null; then
|
||||||
|
log "Stopping backend (PID $BACKEND_PID)..."
|
||||||
|
kill "$BACKEND_PID" 2>/dev/null || true
|
||||||
|
wait "$BACKEND_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${FRONTEND_PID:-}" ]] && kill -0 "$FRONTEND_PID" 2>/dev/null; then
|
||||||
|
log "Stopping frontend (PID $FRONTEND_PID)..."
|
||||||
|
kill "$FRONTEND_PID" 2>/dev/null || true
|
||||||
|
wait "$FRONTEND_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${STOP_DB_ON_EXIT:-true}" == "true" ]]; then
|
||||||
|
log "Stopping database container..."
|
||||||
|
docker stop "$DB_CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||||
|
else
|
||||||
|
warn "Leaving database container '$DB_CONTAINER_NAME' running."
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Dev environment stopped."
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
# Ensure required tools are available
|
||||||
|
command -v docker >/dev/null 2>&1 || { echo -e "${RED}docker is required but not installed.${RESET}"; exit 1; }
|
||||||
|
command -v go >/dev/null 2>&1 || { echo -e "${RED}go is required but not installed.${RESET}"; exit 1; }
|
||||||
|
command -v npm >/dev/null 2>&1 || { echo -e "${RED}npm is required but not installed.${RESET}"; exit 1; }
|
||||||
|
|
||||||
|
# Start the database container if it's not already running
|
||||||
|
if docker ps --format '{{.Names}}' | grep -q "^${DB_CONTAINER_NAME}$"; then
|
||||||
|
warn "Database container '$DB_CONTAINER_NAME' is already running. Reusing it."
|
||||||
|
elif docker ps -a --format '{{.Names}}' | grep -q "^${DB_CONTAINER_NAME}$"; then
|
||||||
|
log "Starting existing database container '$DB_CONTAINER_NAME'..."
|
||||||
|
docker start "$DB_CONTAINER_NAME" >/dev/null
|
||||||
|
else
|
||||||
|
log "Creating and starting database container '$DB_CONTAINER_NAME'..."
|
||||||
|
docker run -d \
|
||||||
|
--name "$DB_CONTAINER_NAME" \
|
||||||
|
-e MYSQL_ROOT_PASSWORD="$DB_ROOT_PASSWORD" \
|
||||||
|
-e MYSQL_USER="$DB_USER" \
|
||||||
|
-e MYSQL_PASSWORD="$DB_PASSWORD" \
|
||||||
|
-e MYSQL_DATABASE="$DB_NAME" \
|
||||||
|
-p "${DB_PORT}:3306" \
|
||||||
|
mariadb:10 \
|
||||||
|
--character-set-server=utf8mb4 \
|
||||||
|
--collation-server=utf8mb4_unicode_ci \
|
||||||
|
>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wait for MariaDB to be ready
|
||||||
|
log "Waiting for database to be ready..."
|
||||||
|
for i in {1..60}; do
|
||||||
|
if docker exec "$DB_CONTAINER_NAME" mysqladmin ping \
|
||||||
|
-h localhost -u "$DB_USER" --password="$DB_PASSWORD" \
|
||||||
|
>/dev/null 2>&1; then
|
||||||
|
# mysqladmin ping can return true before the server accepts SQL connections,
|
||||||
|
# so wait a little longer for the server to finish initializing.
|
||||||
|
sleep 2
|
||||||
|
log "Database is ready."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [[ "$i" -eq 60 ]]; then
|
||||||
|
echo -e "${RED}Database did not become ready in time.${RESET}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Start the backend (with retries in case the DB isn't fully accepting connections yet)
|
||||||
|
log "Starting backend..."
|
||||||
|
cd "$PROJECT_DIR/backend"
|
||||||
|
|
||||||
|
BACKEND_START_ATTEMPTS="${BACKEND_START_ATTEMPTS:-3}"
|
||||||
|
for attempt in $(seq 1 "$BACKEND_START_ATTEMPTS"); do
|
||||||
|
DB_HOST=localhost DB_PASSWORD="$DB_PASSWORD" go run . &
|
||||||
|
BACKEND_PID=$!
|
||||||
|
|
||||||
|
# Wait for backend to be ready
|
||||||
|
log "Waiting for backend to be ready (attempt $attempt/$BACKEND_START_ATTEMPTS)..."
|
||||||
|
ready=false
|
||||||
|
for i in {1..30}; do
|
||||||
|
if curl -s http://localhost:8080/ping >/dev/null 2>&1; then
|
||||||
|
log "Backend is ready at http://localhost:8080"
|
||||||
|
ready=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
|
||||||
|
warn "Backend exited early, possibly waiting for DB."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$ready" == "true" ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$attempt" -eq "$BACKEND_START_ATTEMPTS" ]]; then
|
||||||
|
echo -e "${RED}Backend did not become ready after $BACKEND_START_ATTEMPTS attempts.${RESET}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
warn "Retrying backend startup..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# Start the frontend
|
||||||
|
log "Starting frontend..."
|
||||||
|
cd "$PROJECT_DIR/frontend"
|
||||||
|
if [[ ! -f .env ]]; then
|
||||||
|
warn "No .env file found in frontend/. Creating from .env.example..."
|
||||||
|
cp .env.example .env
|
||||||
|
fi
|
||||||
|
npm run dev &
|
||||||
|
FRONTEND_PID=$!
|
||||||
|
|
||||||
|
log "Frontend starting. It will be available at http://localhost:5173"
|
||||||
|
log "${BLUE}Press Ctrl-C to stop all services.${RESET}"
|
||||||
|
|
||||||
|
# Wait for either process to exit
|
||||||
|
wait -n "$BACKEND_PID" "$FRONTEND_PID" 2>/dev/null || true
|
||||||
Reference in New Issue
Block a user