Compare commits
10 Commits
test/ipub-
...
prototype
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fbd2c1fbd | |||
| daf8282177 | |||
| 01976ebf6c | |||
| a77af8b50b | |||
| c430ee4a5d | |||
| 4d593595b6 | |||
| 49557153a6 | |||
| b2e148e252 | |||
| df9f42decd | |||
| f68b1c022b |
5
.example.env
Normal file
5
.example.env
Normal file
@@ -0,0 +1,5 @@
|
||||
AWS_ACCESS_KEY_ID=**************************
|
||||
AWS_SECRET_ACCESS_KEY=****************************************************************
|
||||
AWS_DEFAULT_REGION=******
|
||||
AWS_ENDPOINT_URL=localhost:3000
|
||||
S3_DEFAULT_BUCKET=comicverse-test-bucket
|
||||
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
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
|
||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
||||
# Package Managers
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
8
.prettierrc
Normal file
8
.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
38
README.md
Normal file
38
README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# 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.
|
||||
33
eslint.config.js
Normal file
33
eslint.config.js
Normal file
@@ -0,0 +1,33 @@
|
||||
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/']
|
||||
}
|
||||
];
|
||||
@@ -22,10 +22,11 @@
|
||||
devShells = forAllSystems (system: pkgs: {
|
||||
default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
awscli2
|
||||
bun
|
||||
eslint
|
||||
nodejs_22
|
||||
nodePackages_latest.prettier
|
||||
bun
|
||||
];
|
||||
};
|
||||
});
|
||||
|
||||
38
package.json
Normal file
38
package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
13
src/app.d.ts
vendored
Normal file
13
src/app.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// 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 {};
|
||||
12
src/app.html
Normal file
12
src/app.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!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>
|
||||
3
src/lib/index.ts
Normal file
3
src/lib/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as s3 } from './s3';
|
||||
export { default as db } from './sqlite';
|
||||
export * from './sqlite'
|
||||
19
src/lib/s3.ts
Normal file
19
src/lib/s3.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
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;
|
||||
37
src/lib/sqlite.ts
Normal file
37
src/lib/sqlite.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
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;
|
||||
21
src/routes/+layout.svelte
Normal file
21
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<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>
|
||||
40
src/routes/+page.server.ts
Normal file
40
src/routes/+page.server.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
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 };
|
||||
}
|
||||
};
|
||||
46
src/routes/+page.svelte
Normal file
46
src/routes/+page.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<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>
|
||||
41
src/routes/files/[project]/[file]/+server.ts
Normal file
41
src/routes/files/[project]/[file]/+server.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
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;
|
||||
101
src/routes/projects/[id]/+page.server.ts
Normal file
101
src/routes/projects/[id]/+page.server.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
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;
|
||||
384
src/routes/projects/[id]/+page.svelte
Normal file
384
src/routes/projects/[id]/+page.svelte
Normal file
@@ -0,0 +1,384 @@
|
||||
<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>
|
||||
64
src/routes/projects/[id]/IteractiveImage.svelte
Normal file
64
src/routes/projects/[id]/IteractiveImage.svelte
Normal file
@@ -0,0 +1,64 @@
|
||||
<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>
|
||||
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
18
svelte.config.js
Normal file
18
svelte.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
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;
|
||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
10
vite.config.ts
Normal file
10
vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
server: {
|
||||
port: 3000,
|
||||
host: '192.168.1.7'
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user