91 Commits

Author SHA1 Message Date
242559acc9 feat: add tailwindcss 2025-03-06 10:02:35 -03:00
c2bddbf32c chore: ignore .tmp folder 2025-03-06 10:02:13 -03:00
c5043a3527 style: format long line in New constructor 2025-03-06 09:45:01 -03:00
1351cd0cde refactor: import renamed excetion package 2025-03-05 13:52:00 -03:00
51bb55816c chore: update submodule 2025-03-05 13:47:05 -03:00
f13dc0fe0f feat: templates prototype 2025-03-05 13:45:19 -03:00
7b6796e2c1 feat: panic recovering on requests 2025-03-05 12:22:38 -03:00
4875f47f94 chore: rename routes log group to requests 2025-03-05 12:22:21 -03:00
fafd7f76bf chore: hotreloading on file change 2025-03-05 11:00:28 -03:00
489a696e82 feat: exceptions hello world 2025-03-05 10:43:42 -03:00
d2308b5a1b feat: hello world 2025-03-05 10:05:21 -03:00
1edc9c6ff0 chore: update submodule url 2025-03-04 17:42:05 -03:00
4063a6fb0d chore: fresh restart 2025-03-01 19:35:33 -03:00
e16e57f387 chore(deps): add loreddev/x submodule 2025-02-20 09:32:18 -03:00
f1e9468f09 refactor(router): rename parsePath to parsePattern 2024-12-18 14:13:26 -03:00
0af4ea3074 fix(router): trailing slash in mux rest pattern 2024-12-18 14:09:17 -03:00
67230ba75d fix(router): missing join for the rest of the path 2024-12-13 16:59:40 -03:00
c2bbd80dce feat(middlewares): use closures instead of structs for middleware constructors 2024-12-13 16:21:47 -03:00
23ac6d6220 fix(router): missing support for mux server in router constructor 2024-12-13 16:13:26 -03:00
5b5de7b206 feat(router): add support for http methods 2024-12-13 16:12:49 -03:00
b14b0be66b chore(eslint,dev): fix eslint errors in htmx files 2024-10-30 10:35:25 -03:00
a7b5ab174b fix(dev,eslint): remove @eslint/markdown since 'no-irregular-whitespace' breaks with it 2024-10-30 10:34:49 -03:00
478dd0216e feat(cookies): MarshalToWriter helper/alias function 2024-10-24 20:28:31 -03:00
c27b0a4e12 feat(cookies): UnmarshalIfRequest helper/alias function 2024-10-24 20:28:02 -03:00
11312ef5fe refactor(cookies): move errors to bottom of file 2024-10-24 20:12:16 -03:00
d74d71dfa3 feat(cookies,errors): error helper for cookie unmarshaling 2024-10-24 19:53:38 -03:00
6ceef9664a feat(forms): Unmarshaler interface 2024-10-24 19:40:25 -03:00
19cf6e13cc feat(forms,errors): error helper for form and query parsing 2024-10-24 19:39:50 -03:00
dd1d67207d feat(forms): forms and queries parsing and unmarshal 2024-10-24 19:39:11 -03:00
6372b20a0b feat(router,errors): BadRequest error 2024-10-24 19:36:35 -03:00
f27784a0b2 refactor(router,errors): move functiosn to alphabetical order 2024-10-24 19:35:43 -03:00
c05086a93e chore(jsdoc): fix jsdoc in htmx.js and htmx.d.ts files 2024-10-24 17:24:46 -03:00
3ae232a779 feat(cookies): add Marshaler and Unmarshaler interface for cookies 2024-10-24 11:06:01 -03:00
535b7aa975 chore(lint,format): make lint and fmt depend on build/templ 2024-10-23 19:08:03 -03:00
218b991caa refactor(lib): move app routing to lib package
These packages and functions under lib will one day be part of it's
dedicated library/framework, so separating them here helps for the
future.
2024-10-23 19:06:03 -03:00
0e96046259 style(imports,handlers): format imports in handlers 2024-10-23 18:58:13 -03:00
013ed1002a chore(dev,makefile): improve clean command 2024-10-23 18:55:28 -03:00
0ed5f27f92 feat(lib,htmx): install htmx 2024-10-23 18:36:02 -03:00
e769a59b57 refactor(javascript,assets): rename javascript directory to lib 2024-10-23 18:35:38 -03:00
7d49a0fd81 feat(errors,router): add error description in 500 errors 2024-10-22 20:34:35 -03:00
012d0b3137 feat(errors,router): add Endpoint field/data to all errors 2024-10-22 20:34:12 -03:00
1e00971f62 feat(cookies,router): new cookies marshaller and unmarshaller 2024-10-22 20:25:26 -03:00
853c86af0a fix(unocss): rename pages to handlers 2024-10-22 16:58:12 -03:00
96dc9ce119 feat(perf,layouts,css): place global css in style tag for instant loading 2024-10-22 16:57:52 -03:00
3ace8799e2 fix(javascript,assets): js to javascript src link 2024-10-22 16:57:17 -03:00
ea22eedd0e refactor(assets,embedded): rewrite embedded assets files handling 2024-10-22 16:56:50 -03:00
409cb86070 refactor(env,config): create DEV global variable 2024-10-22 16:56:10 -03:00
1034cc4906 refactor(javascript,assets): rename js directory to javascript 2024-10-22 16:55:11 -03:00
390774600f fix(logger,middlewares): status code not being read from response 2024-10-22 11:55:00 -03:00
5448517b67 refactor(pages): move pages to handlers package 2024-10-22 10:09:23 -03:00
50e0ee7bcb fix(dashboard): fix color and fonts 2024-10-22 09:04:20 -03:00
6a561f7d6f feat(dev): development page for testing 2024-10-22 09:03:58 -03:00
fbbbb39fc8 feat(fonts): add Karla and Playfair font families 2024-10-22 09:03:44 -03:00
d65abd3e6f feat(cache,middlewares): cache middlewares 2024-10-22 09:02:58 -03:00
27f29990f5 refactor(router): move Router type to interface 2024-10-21 14:57:53 -03:00
c12db37dc2 refactor(errors,middlwares): new error middleware implementation 2024-10-21 14:27:43 -03:00
a72b2d0482 refactor(router): new router implementation with route groups support 2024-10-21 14:26:46 -03:00
f80dd84784 feat(logger,middlewares): add move information for each request 2024-10-21 14:24:28 -03:00
2219de640d refactor(errors,middlewares): move html displaying to dedicated function 2024-10-18 09:12:45 -03:00
d3bb613252 chore: ignore _templ.txt files 2024-10-18 00:24:10 -03:00
fb97c490a8 fix(errors,middlewares): return plain text on 404 error 2024-10-18 00:23:42 -03:00
d74a13bfd6 fix(errors,middlewares): Accept header prefersHtml boolean operator 2024-10-18 00:23:09 -03:00
c55a516a3d feat(errors,middlwares): add error handler middleware 2024-10-17 23:47:35 -03:00
f3f060ddc8 feat(middlewares): middleware struct interface 2024-10-17 23:46:52 -03:00
65e34b4e29 feat(logger,middlewares): add group do logger in logger middleware 2024-10-17 23:46:08 -03:00
5157deda2a chore(dev): move final binary from dist to .dist directory 2024-10-15 15:33:16 -03:00
35dbf51fb0 feat(dev): development pages for testing and prototypes 2024-10-15 01:04:35 -03:00
8a44e0821d feat(ui,theme): dynamic theme css 2024-10-15 01:04:18 -03:00
bfd24e0dc7 feat(layouts): base layout for all pages 2024-10-15 01:03:39 -03:00
a183a5a069 feat(dev): development middleware 2024-10-15 01:02:31 -03:00
8a4a7f06ad feat(dev): logging middleware 2024-10-15 01:01:47 -03:00
0222d191e9 fix: check errors in error ServeHTTP 2024-10-15 00:59:43 -03:00
4ffe2f4e9e refactor: move Middleware interface to sinle function class 2024-10-15 00:57:06 -03:00
8388e2763e fix(dev): ignore json files from linting 2024-10-13 20:29:44 -03:00
36ab51b337 chore(dev): eslint command to make lint 2024-10-13 20:18:56 -03:00
cfac969a12 feat: hello world js test 2024-10-13 20:18:25 -03:00
09e1a0ce1e chore(dev): eslint configuration
I love Javascript :)
2024-10-13 20:18:04 -03:00
d9eb24b9cd feat(dev): debugger configuration 2024-10-11 22:30:13 -03:00
78718c29bf feat(app): static file serving 2024-10-11 22:19:07 -03:00
e70e18aee5 chore(deps): use eslint_d instead of eslint 2024-10-11 14:13:44 -03:00
518c712038 fix(dev): hot reloading 2024-10-11 14:13:34 -03:00
fe427c0394 fix: commands in makefile 2024-10-07 19:16:55 -03:00
a01742828e fix: incorrect fields in Route 2024-10-07 19:16:44 -03:00
34c6b7ccb4 feat: test page 2024-10-07 19:12:27 -03:00
f1d312bfce chore: configure makefile for development 2024-10-07 19:12:06 -03:00
32d393c1da feat(deps): add unocss 2024-10-07 19:11:45 -03:00
4f423794a3 chore(deps): add templ 2024-10-07 15:40:38 -03:00
47c570855f feat(router): error handling of routes 2024-10-07 15:40:26 -03:00
1c7131c216 feat(router): support for middlewares 2024-10-07 15:40:06 -03:00
bb0c4b371a feat: setup project router 2024-10-07 15:39:46 -03:00
9cd1e1b1e9 chore: add golang tools to flake.niix 2024-10-04 19:41:07 -03:00
45 changed files with 1400 additions and 988 deletions

0
.env Normal file
View File

View File

@@ -1,5 +0,0 @@
AWS_ACCESS_KEY_ID=**************************
AWS_SECRET_ACCESS_KEY=****************************************************************
AWS_DEFAULT_REGION=******
AWS_ENDPOINT_URL=localhost:3000
S3_DEFAULT_BUCKET=comicverse-test-bucket

26
.gitignore vendored
View File

@@ -1,23 +1,3 @@
node_modules
# Output
.output
.vercel
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
data.db
.dist
wind.css
.tmp

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "x"]
path = x
url = https://forge.capytal.company/loreddev/x

20
.golangci.yml Normal file
View File

@@ -0,0 +1,20 @@
run:
timeout: 5m
modules-download-mode: readonly
linters:
disable-all: true
enable:
- errcheck
- goimports
- gofumpt
- revive # golint
- gosimple
- govet
- ineffassign
- staticcheck
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0

1
.npmrc
View File

@@ -1 +0,0 @@
engine-strict=true

View File

@@ -1,4 +0,0 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

View File

@@ -1,8 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

20
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,20 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch APP",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/main.go"
},
{
"name": "Launch APP (Dev)",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/main.go",
"args": [ "-dev" ]
}
]
}

View File

@@ -1,38 +0,0 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

BIN
bun.lockb

Binary file not shown.

77
cmd/cmd.go Normal file
View File

@@ -0,0 +1,77 @@
package cmd
import (
"context"
"errors"
"flag"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"forge.capytal.company/capytalcode/project-comicverse/router"
"forge.capytal.company/loreddev/x/tinyssert"
)
var (
host = flag.String("host", "localhost", "Host to listen to")
port = flag.Uint("port", 8080, "Port to be used for the server.")
templatesDir = flag.String("templates", "", "Templates directory to be used instead of built-in ones.")
verbose = flag.Bool("verbose", false, "Print debug information on logs")
dev = flag.Bool("dev", false, "Run the server in debug mode.")
)
func init() {
flag.Parse()
}
func Execute() {
ctx := context.Background()
assertions := tinyssert.NewDisabledAssertions()
if *dev {
assertions = tinyssert.NewAssertions()
}
level := slog.LevelError
if *dev {
level = slog.LevelDebug
} else if *verbose {
level = slog.LevelInfo
}
log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level}))
app := router.New(assertions, log, *dev)
srv := &http.Server{
Addr: fmt.Sprintf("%s:%d", *host, *port),
Handler: app,
}
c, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
defer stop()
go func() {
log.Info("Starting application",
slog.String("host", *host),
slog.Uint64("port", uint64(*port)),
slog.Bool("verbose", *verbose),
slog.Bool("development", *dev))
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Error("Failed to start application", slog.String("error", err.Error()))
}
}()
<-c.Done()
log.Info("Stopping application gracefully")
if err := srv.Shutdown(ctx); err != nil {
log.Error("Failed to stop application gracefully", slog.String("error", err.Error()))
}
log.Info("FINAL")
os.Exit(0)
}

View File

@@ -1,33 +0,0 @@
import js from '@eslint/js';
import ts from 'typescript-eslint';
import svelte from 'eslint-plugin-svelte';
import prettier from 'eslint-config-prettier';
import globals from 'globals';
/** @type {import('eslint').Linter.Config[]} */
export default [
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
},
{
ignores: ['build/', '.svelte-kit/', 'dist/']
}
];

View File

@@ -3,10 +3,7 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
};
outputs = {
self,
nixpkgs,
}: let
outputs = {nixpkgs, ...}: let
systems = [
"x86_64-linux"
"aarch64-linux"
@@ -21,12 +18,23 @@
in {
devShells = forAllSystems (system: pkgs: {
default = pkgs.mkShell {
CGO_ENABLED = "0";
hardeningDisable = ["all"];
buildInputs = with pkgs; [
awscli2
bun
eslint
nodejs_22
nodePackages_latest.prettier
# Go tools
go
golangci-lint
gofumpt
gotools
delve
# TailwindCSS
tailwindcss
# Sqlite tools
sqlite
lazysql
];
};
});

7
go.mod Normal file
View File

@@ -0,0 +1,7 @@
module forge.capytal.company/capytalcode/project-comicverse
go 1.23.3
toolchain go1.23.6
require forge.capytal.company/loreddev/x v0.0.0-20250227192157-90a5169f1bef

2
go.sum Normal file
View File

@@ -0,0 +1,2 @@
forge.capytal.company/loreddev/x v0.0.0-20250227192157-90a5169f1bef h1:IJ9z7otITB5hhjZ+bmU0yOVsa8K1RWYIZ+cQj9XF6NY=
forge.capytal.company/loreddev/x v0.0.0-20250227192157-90a5169f1bef/go.mod h1:MnU08vmXvYIQlQutVcC6o6Xq1KHZuXGXO78bbHseCFo=

6
go.work Normal file
View File

@@ -0,0 +1,6 @@
go 1.23.6
use (
./.
./x
)

7
main.go Normal file
View File

@@ -0,0 +1,7 @@
package main
import "forge.capytal.company/capytalcode/project-comicverse/cmd"
func main() {
cmd.Execute()
}

42
makefile Normal file
View File

@@ -0,0 +1,42 @@
PORT?=8080
lint:
golangci-lint run .
fmt:
go fmt .
golangci-lint run --fix .
dev/server:
go run github.com/joho/godotenv/cmd/godotenv@v1.5.1 \
go run github.com/air-verse/air@v1.52.2 \
--build.cmd "go build -o .tmp/bin/main ." \
--build.bin ".tmp/bin/main" \
--build.exclude_dir "node_modules" \
--build.include_ext "go,html" \
--build.stop_on_error "false" \
--proxy.enabled true \
--proxy.proxy_port $(PORT) \
--proxy.app_port $$(($(PORT) + 1)) \
--misc.clean_on_exit true \
-- -dev -port $$(($(PORT) + 1))
dev/assets:
tailwindcss -o ./static/css/wind.css -w
dev:
$(MAKE) -j2 dev/server dev/assets
build:
go generate
go build -o ./.dist/app .
run: build
./.dist/app
clean:
# Remove generated directories
if [[ -d ".dist" ]]; then rm -r ./.dist; fi
if [[ -d "tmp" ]]; then rm -r ./tmp; fi
if [[ -d "bin" ]]; then rm -r ./bin; fi

View File

@@ -1,38 +0,0 @@
{
"name": "comicverse",
"version": "0.0.1",
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/eslint": "^9.6.0",
"@types/node": "^22.6.0",
"blob-util": "^2.0.2",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
"globals": "^15.0.0",
"minio": "^8.0.1",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7",
"svelte": "^4.2.7",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.0.0",
"vite": "^5.0.3",
"xml-js": "^1.6.11"
},
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"type": "module"
}

52
router/router.go Normal file
View File

@@ -0,0 +1,52 @@
package router
import (
"errors"
"log/slog"
"net/http"
"forge.capytal.company/capytalcode/project-comicverse/templates"
"forge.capytal.company/loreddev/x/smalltrip"
"forge.capytal.company/loreddev/x/smalltrip/exception"
"forge.capytal.company/loreddev/x/smalltrip/middleware"
"forge.capytal.company/loreddev/x/tinyssert"
)
func New(assertions tinyssert.Assertions, log *slog.Logger, dev bool) http.Handler {
r := smalltrip.NewRouter(
smalltrip.WithAssertions(assertions),
smalltrip.WithLogger(log.WithGroup("smalltrip")),
)
r.Use(middleware.Logger(log.WithGroup("requests")))
if dev {
log.Debug("Development mode activated, using development middleware")
r.Use(middleware.Dev)
} else {
r.Use(middleware.PersistentCache())
}
r.Use(exception.PanicMiddleware())
r.Use(exception.Middleware())
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
err := templates.Templates().ExecuteTemplate(w, "index.html", nil)
if err != nil {
exception.InternalServerError(err).ServeHTTP(w, r)
}
})
r.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
r.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
exception.InternalServerError(errors.New("TEST ERROR"),
exception.WithData("test-data", "test-value"),
).ServeHTTP(w, r)
})
r.HandleFunc("/panic", func(w http.ResponseWriter, r *http.Request) {
panic("TEST PANIC")
})
return r
}
func dashboard(w http.ResponseWriter, r *http.Request) {
}

13
src/app.d.ts vendored
View File

@@ -1,13 +0,0 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View File

@@ -1,12 +0,0 @@
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -1,3 +0,0 @@
export { default as s3 } from './s3';
export { default as db } from './sqlite';
export * from './sqlite'

View File

@@ -1,19 +0,0 @@
import {
AWS_ENDPOINT_URL,
AWS_ACCESS_KEY_ID,
AWS_DEFAULT_REGION,
AWS_SECRET_ACCESS_KEY
} from '$env/static/private';
import * as Minio from 'minio';
const client = new Minio.Client({
endPoint: AWS_ENDPOINT_URL.split(':')[0],
port: Number(AWS_ENDPOINT_URL.split(':')[1]),
useSSL: false,
region: AWS_DEFAULT_REGION,
accessKey: AWS_ACCESS_KEY_ID,
secretKey: AWS_SECRET_ACCESS_KEY
});
export default client;

View File

@@ -1,37 +0,0 @@
import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
const db = await open({
filename: 'data.db',
driver: sqlite3.cached.Database
});
await db.exec(`
CREATE TABLE IF NOT EXISTS projects (
ID text NOT NULL,
Name text NOT NULL,
PRIMARY KEY(ID)
)
`);
type Project = {
id: string;
title: string;
pages: Page[];
};
type Page = {
title: string;
src: string;
background: string;
iteraction: Iteraction[];
};
type Iteraction = {
x: number;
y: number;
link: string;
};
export type { Project, Iteraction, Page };
export default db;

View File

@@ -1,21 +0,0 @@
<svelte:head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.colors.min.css"
/>
<meta name="color-scheme" content="dark" />
</svelte:head>
<main>
<slot></slot>
</main>
<style>
main {
width: 100vw;
height: 100vh;
}
</style>

View File

@@ -1,40 +0,0 @@
import { fail, type Actions } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { db, s3, type Project } from '$lib';
import { AWS_S3_DEFAULT_BUCKET } from '$env/static/private';
export const load = (async ({}) => {
const res = await db.all<Project[]>('SELECT ID, Name FROM projects');
return { projects: res };
}) as PageServerLoad;
export const actions: Actions = {
default: async ({ request }) => {
const data = await request.formData();
const name = data.get('project-name');
if (!name) return fail(400, { name, missing: true });
const uuid = crypto.randomUUID().split('-')[0];
const res = await db.run('INSERT OR IGNORE INTO projects (ID, Name) VALUES (:id, :name)', {
':id': uuid,
':name': name
});
const project: Project = {
id: uuid,
title: name.toString(),
pages: []
};
await s3.putObject(AWS_S3_DEFAULT_BUCKET, `${uuid}/project.json`, JSON.stringify(project));
if (res.changes == undefined) {
return fail(500, { reason: 'Failed to insert project into database' });
}
return { success: true };
}
};

View File

@@ -1,46 +0,0 @@
<script lang="ts">
import type { Project } from '$lib';
import type { PageData } from './$types';
export let data: PageData;
if ((data.projects.length + 1) % 3 !== 0) {
data.projects.push({ Name: '', ID: '' });
data.projects.push({ Name: '', ID: '' });
}
</script>
<section>
{#each data.projects as p}
<article>
<h1><a data-sveltekit-reload href={`/projects/${p.ID}`}>{p.Name}</a></h1>
<p class="id">{p.ID}</p>
</article>
{/each}
<article>
<form method="POST">
<fieldset role="group">
<input type="text" name="project-name" placeholder="Project Name" required />
<input type="submit" value="Create" />
</fieldset>
</form>
</article>
</section>
<style>
section {
display: grid;
@media (min-width: 768px) {
grid-template-columns: repeat(3, minmax(0%, 1fr));
}
grid-column-gap: var(--pico-grid-column-gap);
grid-row-gap: var(--pico-grid-row-gap);
padding: 1rem var(--pico-grid-row-gap);
}
.id {
font-size: 0.7rem;
opacity: 0.3;
}
</style>

View File

@@ -1,41 +0,0 @@
import { type RequestHandler } from '@sveltejs/kit';
import stream from 'node:stream/promises';
import { db, s3, type Project } from '$lib';
import { AWS_S3_DEFAULT_BUCKET } from '$env/static/private';
import { extname } from 'node:path';
export const GET = (async ({ params }) => {
const file = await s3.getObject(AWS_S3_DEFAULT_BUCKET, `${params.project}/${params.file}`);
file.on('error', (err: any) => {
console.log(err);
});
let chunks: Buffer[] = [];
let buf;
file.on('data', (chunk) => {
chunks.push(Buffer.from(chunk));
});
file.on('end', () => {
buf = Buffer.concat(chunks);
});
await stream.finished(file)
let res = new Response(buf);
res.headers.set(
'Content-Type',
(() => {
switch (extname(params.file!)) {
case '.png':
return 'image/png';
case '.json':
return 'application/json';
}
return 'text/plain';
})()
);
res.headers.set('Cache-Control', 'max-age=604800')
return res;
}) as RequestHandler;

View File

@@ -1,101 +0,0 @@
import { error, fail, redirect, type Actions } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import stream from 'node:stream/promises';
import { db, s3, type Project } from '$lib';
import { AWS_S3_DEFAULT_BUCKET } from '$env/static/private';
import { extname } from 'node:path';
export const prerender = false;
export const ssr = false;
export const load = (async ({ params }) => {
const res = await db.get<{ id: string; name: string }>(
'SELECT ID, Name FROM projects WHERE ID = ?',
params.id
);
if (res === undefined) {
return fail(404, { reason: 'Failed to find project into database' });
}
const project = await s3.getObject(AWS_S3_DEFAULT_BUCKET, `${params.id}/project.json`);
project.on('error', (err: any) => {
console.log(err);
});
let p: string = '';
project.on('data', (chunk: any) => {
p += chunk;
});
await stream.finished(project);
let proj = JSON.parse(p) as Project;
return { project: proj };
}) as PageServerLoad;
export const actions = {
delete: async ({ params }) => {
const res = await db.run('DELETE FROM projects WHERE ID = ?', params.id);
await s3.removeObject(AWS_S3_DEFAULT_BUCKET, `${params.id}/project.json`);
if (res === undefined) {
return fail(500, { reason: 'Failed to delete project' });
}
redirect(303, '/');
},
'delete-file': async ({ params, request }) => {
const form = await request.formData();
const file = form?.get('file') as string;
const project = await s3.getObject(AWS_S3_DEFAULT_BUCKET, `${params.id}/project.json`);
project.on('error', (err: any) => {
console.log(err);
});
let p: string = '';
project.on('data', (chunk: any) => {
p += chunk;
});
await stream.finished(project);
let proj = JSON.parse(p) as Project;
proj.pages = proj.pages.filter((p) => p.src != file);
await s3.removeObject(AWS_S3_DEFAULT_BUCKET, `${params.id}/${file}`);
await s3.putObject(AWS_S3_DEFAULT_BUCKET, `${params.id}/project.json`, JSON.stringify(proj));
},
addpage: async ({ request, params }) => {
const form = await request.formData();
const file = form?.get('file') as File;
const title = form?.get('title') as string;
const color = form?.get('color') as string;
const iteractions = form?.get('iteractions') as string;
console.log(file);
const project = await s3.getObject(AWS_S3_DEFAULT_BUCKET, `${params.id}/project.json`);
project.on('error', (err: any) => {
console.log(err);
});
let p: string = '';
project.on('data', (chunk: any) => {
p += chunk;
});
await stream.finished(project);
let proj = JSON.parse(p) as Project;
const filename = `${crypto.randomUUID().split('-')[0]}${extname(file?.name)}`;
proj.pages.push({
title: title,
src: filename,
background: color,
iteraction: JSON.parse(iteractions)
});
const buf = Buffer.from(await file.arrayBuffer());
await s3.putObject(AWS_S3_DEFAULT_BUCKET, `${params.id}/project.json`, JSON.stringify(proj));
await s3.putObject(AWS_S3_DEFAULT_BUCKET, `${params.id}/${filename}`, buf);
}
} as Actions;

View File

@@ -1,384 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { PageData } from './$types';
import { arrayBufferToBlob } from 'blob-util';
import IImage from './IteractiveImage.svelte';
export let data: PageData;
const pages = data.project.pages;
let modal = false;
let reader: Element;
let scroll: number;
let color = hexToRgb(pages[0]?.background ?? '#181818');
let currentPage = 0;
let colorPerc = 0;
let currentColor = color;
let nextColor = hexToRgb(pages[1]?.background ?? pages[0]?.background ?? '#181818');
let currentChunk = 0;
let nextChunk = 0;
let browser = false;
let maxScroll = 0;
let chunk = 0;
let chunks: number[] = [];
onMount(() => {
browser = true;
maxScroll = Math.max(reader.scrollHeight - reader.clientHeight);
});
function hexToRgb(color: string): number[] {
return [
parseInt(color.substring(1, 3), 16) / 255,
parseInt(color.substring(3, 5), 16) / 255,
parseInt(color.substring(5, 7), 16) / 255
];
}
function rgbToHex(rgb: number[]): string {
return `#${
Math.round(rgb[0] * 255)
.toString(16)
.padStart(2, '0') +
Math.round(rgb[1] * 255)
.toString(16)
.padStart(2, '0') +
Math.round(rgb[2] * 255)
.toString(16)
.padStart(2, '0')
}`;
}
function blendRgbColors(c1: number[], c2: number[], ratio: number): number[] {
return [
c1[0] * (1 - ratio) + c2[0] * ratio,
c1[1] * (1 - ratio) + c2[1] * ratio,
c1[2] * (1 - ratio) + c2[2] * ratio
];
}
let fileInput: Element;
let blobUrl: string | undefined = undefined;
let currentIteraction: { x: number; y: number; link: string };
let iteractionUrl = '';
let iteractions: { x: number; y: number; link: string }[] = [];
let imageElement: Element;
let imageX = 0;
let imageY = 0;
let imageWidth = 0;
let imageHeight = 0;
function readFile(file: Blob) {
let reader = new FileReader();
reader.onloadend = function (e) {
let buf = e.target?.result;
let blob = arrayBufferToBlob(buf as ArrayBuffer, file.type);
blobUrl = window.URL.createObjectURL(blob);
};
reader.readAsArrayBuffer(file);
}
let temp: any;
let images: Map<string, { width: number; height: number }> = new Map();
</script>
{#if browser}
<pre style="position: fixed; bottom: 0; font-size: 0.6rem;">
<code
>{JSON.stringify(
{
page: currentPage,
color: {
background: rgbToHex(color),
current: rgbToHex(currentColor),
next: rgbToHex(nextColor),
percentage: Math.round(colorPerc)
},
scroll: {
current: scroll,
max: maxScroll,
chunks: chunks,
currentChunk: currentChunk,
nextChunk: nextChunk
}
},
null,
2
)}
</code>
</pre>
<details style="position: fixed; bottom: 0; font-size: 0.6rem;">
<pre>
<code>{JSON.stringify(pages)}</code>
</pre>
</details>
{/if}
<dialog open={modal}>
<article>
<header>
<button
aria-label="Close"
rel="prev"
on:click={() => {
modal = false;
}}
></button>
<p>
<strong>Add new page</strong>
</p>
</header>
<form method="POST" action="?/addpage" enctype="multipart/form-data">
<input type="text" required placeholder="Page title" name="title" />
<input type="color" required placeholder="Background color" name="color" />
{#if blobUrl}
<div class="blob-image">
<div class="blob-image-image">
<div
class="iteraction-box"
style={`${[
`margin-left:${Math.round((imageX / 100) * imageWidth)}px;`,
`margin-top:${Math.round((imageY / 100) * imageHeight)}px;`
].join('')} pointer-events: none;`}
></div>
{#each iteractions as i}
<a
class="iteraction-box"
href={i.link}
style={[
`margin-left:${Math.round((i.x / 100) * imageWidth)}px;`,
`margin-top:${Math.round((i.y / 100) * imageHeight)}px;`
].join('')}
></a>
{/each}
<img
style="margin: auto 0;"
src={blobUrl}
bind:this={imageElement}
on:mousemove={(e) => {
let rect = imageElement.getBoundingClientRect();
imageX = Math.round(((e.clientX - rect.left) / rect.width) * 100);
imageY = Math.round(((e.clientY - rect.top) / rect.height) * 100);
imageWidth = rect.width;
imageHeight = rect.height;
}}
on:click={() => {
currentIteraction ||= { x: 0, y: 0, link: '' };
currentIteraction.x = imageX;
currentIteraction.y = imageY;
}}
alt=""
/>
</div>
<fieldset role="group">
<input
type="url"
placeholder="Iteraction url"
bind:value={iteractionUrl}
on:change={() => {
currentIteraction ||= { x: 0, y: 0, link: '' };
currentIteraction.link = iteractionUrl;
}}
on:mouseout={() => {
currentIteraction ||= { x: 0, y: 0, link: '' };
currentIteraction.link = iteractionUrl;
}}
/>
<input
type="button"
value="Add"
on:click|preventDefault={() => {
iteractions.push({
x: currentIteraction.x,
y: currentIteraction.y,
link: currentIteraction.link
});
iteractions = iteractions;
}}
/>
</fieldset>
<div>
<code>{imageX} {imageY}</code>
<code>
Iteraction: {JSON.stringify(currentIteraction)}
</code>
<input
style="display:hidden;"
type="text"
value={JSON.stringify(iteractions.filter(Boolean))}
name="iteractions"
/>
</div>
</div>
{/if}
<input
type="file"
required
name="file"
bind:this={fileInput}
on:change={() => {
// @ts-ignore
readFile(fileInput.files[0]);
}}
/>
<input type="submit" value="Add page" />
</form>
</article>
</dialog>
<section class="project">
<aside>
<a data-sveltekit-reload href="/" style="font-size: 0.5rem;">Return home</a>
<section>
<h1>{data.project.title}</h1>
<p class="id">{data.project.id}</p>
<button
class="add"
on:click={() => {
modal = true;
}}>Add page</button
>
<form method="POST">
<input type="submit" formaction="?/delete" value="Delete" class="pico-background-red-500" />
</form>
</section>
</aside>
{#key maxScroll}
{#if browser}
<article
class="reader"
style={`--bg-color: rgba(${color.map((c) => c * 255).join(',')}, 0.8)`}
bind:this={reader}
on:scroll={() => {
scroll = reader.scrollTop;
if (maxScroll === 0) {
maxScroll = Math.max(reader.scrollHeight - reader.clientHeight);
chunk = Math.round(maxScroll / pages.length);
for (let i = 0; i < pages.length; i++) {
chunks = [...chunks, chunk * i];
}
}
let i = chunks.findIndex((c) => c > scroll - chunk);
currentColor = hexToRgb(pages[i]?.background);
nextColor = pages[i + 1]?.background ? hexToRgb(pages[i + 1]?.background) : currentColor;
currentChunk = chunks[i];
nextChunk = chunks[i + 1] ?? maxScroll;
colorPerc = ((scroll - currentChunk) / (nextChunk - currentChunk)) * 100;
color = blendRgbColors(currentColor, nextColor, colorPerc / 100);
currentPage = i;
}}
>
<div class="pages">
{#each pages as page, key}
{@const coord = key * chunk}
<div class="page" style={`background-color:${page.background}`}>
<IImage {page} projectId={data.project.id} />
<form method="POST" action="?/delete-file" class="delete-file">
<fieldset role="group">
<input type="text" value={`${page.src}`} name="file" />
<input type="submit" value="Delete page" class="pico-background-red-500" />
</fieldset>
</form>
</div>
<code>{coord}</code>
{/each}
</div>
</article>
{/if}
{/key}
</section>
<style>
h1 {
font-size: 1.5rem;
}
.id {
font-size: 0.5rem;
opacity: 0.3;
}
.iteraction-box {
width: 30px;
height: 30px;
display: block;
background-color: #ff0000;
opacity: 0.3;
position: absolute;
z-index: 100;
}
.blob-image-image {
position: relative;
}
.reader {
display: flex;
width: 80vw;
height: 100vh;
justify-content: center;
padding-top: 5rem;
padding-bottom: 5rem;
margin-bottom: 0;
background-color: var(--bg-color);
overflow-y: scroll;
}
.page {
width: calc(1080px / 3.5);
min-height: calc(1920px / 3.5);
@media (min-width: 1024px) {
width: calc(1080px / 2.5);
min-height: calc(1920px / 2.5);
}
background-color: #fff;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 0;
box-shadow: 0rem 1rem 1rem 0rem rgba(0, 0, 0, 0.5);
& form {
margin: 1rem;
margin-bottom: 0;
}
}
.pages {
display: flex;
flex-direction: column;
gap: 1rem;
}
.project {
display: flex;
margin-bottom: 0;
}
.add {
width: 100%;
margin-bottom: 0.5rem;
}
aside {
padding: 1rem;
width: 20vw;
& * {
font-size: 0.8rem !important;
@media (min-width: 1024px) {
font-size: unset;
}
}
}
</style>

View File

@@ -1,64 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
type Page = {
title: string;
src: string;
background: string;
iteraction: Iteraction[];
};
type Iteraction = {
x: number;
y: number;
link: string;
};
export let page: Page;
export let projectId: string;
let image: Element;
let width: number;
let height: number;
let browser = false;
function setCoords() {
let rect = image.getBoundingClientRect();
width = rect.width;
height = rect.height;
}
onMount(() => {
setCoords();
browser = true;
});
</script>
<div style="position: relative;" on:resize={() => setCoords()}>
{#if page.iteraction !== undefined && browser}
{#each page.iteraction as i}
<a
class="iteraction-box"
href={i.link}
target="_blank"
style={[
`margin-left:${(i.x / 100) * width}px;`,
`margin-top:${(i.y / 100) * height}px;`
].join('')}
></a>
{/each}
{/if}
<img bind:this={image} width="1080" height="1920" src={`/files/${projectId}/${page.src}`} />
</div>
<style>
.iteraction-box {
width: 30px;
height: 30px;
display: block;
background-color: #ff0000;
opacity: 0.3;
position: absolute;
z-index: 100;
}
</style>

0
static/css/.gitkeep Normal file
View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

24
static/static.go Normal file
View File

@@ -0,0 +1,24 @@
package static
import (
"embed"
"io/fs"
)
//go:generate tailwindcss -o static/css/wind.css
//go:embed css/*.css
var staticFiles embed.FS
func Files(local ...bool) fs.FS {
var l bool
if len(local) > 0 {
l = local[0]
}
if !l {
return staticFiles
}
return staticFiles
}

View File

@@ -1,18 +0,0 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

1077
tailwind.config.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
{"id":"88ba21ea","title":"test project","pages":[]}

12
templates/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/static/css/wind.css">
</head>
<body class="text-red-600">
<h1>Hello, world</h1>
</body>
</html>

18
templates/templates.go Normal file
View File

@@ -0,0 +1,18 @@
package templates
import (
"embed"
"html/template"
)
//go:embed *.html test/*.html
var embedded embed.FS
var temps = template.Must(template.ParseFS(embedded,
"*.html",
"test/*.html",
))
func Templates() *template.Template {
return temps
}

12
templates/test/test.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h1>Hello, world 2</h1>
</body>
</html>

View File

@@ -1,19 +0,0 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

View File

@@ -1,10 +0,0 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
server: {
port: 3000,
host: '192.168.1.7'
}
});

1
x Submodule

Submodule x added at 0ccb26ab78