more boilerplate and dev environment
This commit is contained in:
@@ -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",
|
||||
"version": "0.0.1",
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^7.0.1",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/kit": "^2.63.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||
"svelte": "^5.56.1",
|
||||
@@ -434,10 +434,10 @@
|
||||
"acorn": "^8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/adapter-auto": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-7.0.1.tgz",
|
||||
"integrity": "sha512-dvuPm1E7M9NI/+canIQ6KKQDU2AkEefEZ2Dp7cY6uKoPq9Z/PhOXABe526UdW2mN986gjVkuSLkOYIBnS/M2LQ==",
|
||||
"node_modules/@sveltejs/adapter-static": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.8.tgz",
|
||||
"integrity": "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"prepare": "svelte-kit sync || echo ''"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^7.0.1",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/kit": "^2.63.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||
"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>
|
||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
||||
<script>
|
||||
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 { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
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()
|
||||
})
|
||||
]
|
||||
plugins: [sveltekit()]
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user