Slave/frontend shell #2

Merged
nobswebdev merged 4 commits from slave/frontend-shell into master 2026-10-05 14:25:44 +00:00
20 changed files with 1304 additions and 76 deletions
+3
View File
@@ -34,4 +34,7 @@ THROTTLE_HOTLINK_PER_IP_LIMIT=1000
THROTTLE_HOTLINK_PER_ID_TTL_MS=60000 THROTTLE_HOTLINK_PER_ID_TTL_MS=60000
THROTTLE_HOTLINK_PER_ID_LIMIT=120 THROTTLE_HOTLINK_PER_ID_LIMIT=120
VITE_API_BASE_URL=http://localhost:3000/api
VITE_CLEARNET_DOMAIN=localhost:5173
VITE_ONION_HOSTNAME=
VITE_MAX_IMAGES_PER_UPLOAD=10 VITE_MAX_IMAGES_PER_UPLOAD=10
+4 -1
View File
@@ -12,6 +12,9 @@
/frontend/node_modules /frontend/node_modules
/frontend/dist /frontend/dist
.vscode .vscode/*
!.vscode/settings.json
!.vscode/extensions.json
!.vscode/tailwind.css-data.json
.idea .idea
*.log *.log
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["bradlc.vscode-tailwindcss"]
}
+10
View File
@@ -0,0 +1,10 @@
{
"css.customData": [".vscode/tailwind.css-data.json"],
"files.associations": {
"**/frontend/src/assets/css/main.css": "tailwindcss"
},
"tailwindCSS.includeLanguages": {
"vue": "html"
},
"tailwindCSS.experimental.configFile": "frontend/src/assets/css/main.css"
}
+33
View File
@@ -0,0 +1,33 @@
{
"version": 1.1,
"atDirectives": [
{
"name": "@theme",
"description": "Tailwind CSS v4 theme configuration."
},
{
"name": "@custom-variant",
"description": "Tailwind CSS v4 custom variant."
},
{
"name": "@apply",
"description": "Tailwind CSS utility composition."
},
{
"name": "@layer",
"description": "Tailwind CSS cascade layer."
},
{
"name": "@source",
"description": "Tailwind CSS v4 content source paths."
},
{
"name": "@utility",
"description": "Tailwind CSS v4 custom utility."
},
{
"name": "@variant",
"description": "Tailwind CSS v4 variant definition."
}
]
}
+1037 -39
View File
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -9,7 +9,12 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"vue": "^3.5.32" "@tailwindcss/vite": "^4.3.3",
"axios": "^1.20.0",
"pinia": "^4.0.3",
"tailwindcss": "^4.3.3",
"vue": "^3.5.32",
"vue-router": "^4.6.4"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.7", "@types/node": "^22.10.7",
+1 -30
View File
@@ -1,32 +1,3 @@
<template> <template>
<main class="home"> <router-view />
<h1>Private Image</h1>
</main>
</template> </template>
<style scoped>
.home {
box-sizing: border-box;
min-height: 100vh;
margin: 0;
padding: 2rem;
font-family:
system-ui,
-apple-system,
Segoe UI,
Roboto,
sans-serif;
line-height: 1.5;
}
h1 {
margin: 0 0 0.5rem;
font-size: 1.75rem;
font-weight: 600;
}
p {
margin: 0;
color: #444;
}
</style>
+75
View File
@@ -0,0 +1,75 @@
@import 'tailwindcss';
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
:root {
--radius: 0.5rem;
--background: oklch(0.99 0.002 260);
--foreground: oklch(0.2 0.02 260);
--card: oklch(1 0 0);
--card-foreground: oklch(0.2 0.02 260);
--primary: oklch(0.42 0.08 260);
--primary-foreground: oklch(0.99 0.002 260);
--secondary: oklch(0.96 0.01 260);
--secondary-foreground: oklch(0.28 0.03 260);
--muted: oklch(0.96 0.008 260);
--muted-foreground: oklch(0.48 0.02 260);
--accent: oklch(0.94 0.02 260);
--accent-foreground: oklch(0.28 0.03 260);
--destructive: oklch(0.55 0.2 25);
--border: oklch(0.9 0.01 260);
--input: oklch(0.9 0.01 260);
--ring: oklch(0.42 0.08 260);
}
.dark {
--background: oklch(0.16 0.02 260);
--foreground: oklch(0.96 0.01 260);
--card: oklch(0.2 0.02 260);
--card-foreground: oklch(0.96 0.01 260);
--primary: oklch(0.72 0.1 260);
--primary-foreground: oklch(0.18 0.02 260);
--secondary: oklch(0.24 0.02 260);
--secondary-foreground: oklch(0.96 0.01 260);
--muted: oklch(0.24 0.02 260);
--muted-foreground: oklch(0.68 0.02 260);
--accent: oklch(0.28 0.03 260);
--accent-foreground: oklch(0.96 0.01 260);
--destructive: oklch(0.62 0.2 25);
--border: oklch(0.32 0.02 260);
--input: oklch(0.32 0.02 260);
--ring: oklch(0.72 0.1 260);
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground antialiased;
}
}
+29
View File
@@ -0,0 +1,29 @@
const env = (key: string): string => {
const value = import.meta.env[key];
if (value === undefined || value === null) {
return '';
}
return String(value);
};
const envInt = (key: string, fallback: number): number => {
const raw = env(key);
if (!raw) {
return fallback;
}
const parsed = parseInt(raw, 10);
return Number.isFinite(parsed) ? parsed : fallback;
};
export const config = {
api: {
baseUrl: env('VITE_API_BASE_URL')
},
share: {
clearnetDomain: env('VITE_CLEARNET_DOMAIN'),
onionHostname: env('VITE_ONION_HOSTNAME')
},
limits: {
maxImagesPerUpload: envInt('VITE_MAX_IMAGES_PER_UPLOAD', 10)
}
};
+5
View File
@@ -0,0 +1,5 @@
export const ROUTE_NAMES = {
Upload: 'Upload',
Viewer: 'Viewer',
Delete: 'Delete'
} as const;
+10
View File
@@ -0,0 +1,10 @@
import axios from 'axios';
import { config } from '@/config';
export const api = axios.create({
baseURL: config.api.baseUrl,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
});
+6 -1
View File
@@ -1,4 +1,9 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
import { createPinia } from 'pinia';
import '@/assets/css/main.css';
import App from './App.vue'; import App from './App.vue';
import router from './router';
createApp(App).mount('#app'); const pinia = createPinia();
createApp(App).use(pinia).use(router).mount('#app');
+28
View File
@@ -0,0 +1,28 @@
import { createRouter, createWebHistory } from 'vue-router';
import { ROUTE_NAMES } from '@/consts/routeNames';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: ROUTE_NAMES.Upload,
component: () => import('@/views/UploadView.vue'),
meta: { title: 'Private Image — Upload' }
},
{
path: '/v/:id',
name: ROUTE_NAMES.Viewer,
component: () => import('@/views/ViewerView.vue'),
meta: { title: 'Private Image — Viewer' }
},
{
path: '/d/:id',
name: ROUTE_NAMES.Delete,
component: () => import('@/views/DeleteView.vue'),
meta: { title: 'Private Image — Delete' }
}
]
});
export default router;
@@ -0,0 +1,23 @@
import axios from 'axios';
export function resolveAxiosErrorMessage(error: unknown, fallback = 'Request failed'): string {
if (axios.isAxiosError(error)) {
const data = error.response?.data;
if (typeof data === 'object' && data !== null && 'message' in data) {
const message = (data as { message: unknown }).message;
if (typeof message === 'string') {
return message;
}
if (Array.isArray(message)) {
return message.map(String).join(', ');
}
}
if (error.message) {
return error.message;
}
}
if (error instanceof Error) {
return error.message;
}
return fallback;
}
+6
View File
@@ -0,0 +1,6 @@
<template>
<main class="flex min-h-screen flex-col p-8">
<h1 class="text-2xl font-semibold tracking-tight">Delete</h1>
<p class="mt-2 text-muted-foreground">Delete upload — coming soon.</p>
</main>
</template>
+6
View File
@@ -0,0 +1,6 @@
<template>
<main class="flex min-h-screen flex-col p-8">
<h1 class="text-2xl font-semibold tracking-tight">Upload</h1>
<p class="mt-2 text-muted-foreground">Upload UI coming soon.</p>
</main>
</template>
+6
View File
@@ -0,0 +1,6 @@
<template>
<main class="flex min-h-screen flex-col p-8">
<h1 class="text-2xl font-semibold tracking-tight">Viewer</h1>
<p class="mt-2 text-muted-foreground">Zero-knowledge viewer — coming soon.</p>
</main>
</template>
+11
View File
@@ -1 +1,12 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string;
readonly VITE_CLEARNET_DOMAIN: string;
readonly VITE_ONION_HOSTNAME: string;
readonly VITE_MAX_IMAGES_PER_UPLOAD: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
+2 -4
View File
@@ -1,5 +1,6 @@
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
@@ -11,8 +12,5 @@ export default defineConfig({
'@': path.resolve(__dirname, 'src') '@': path.resolve(__dirname, 'src')
} }
}, },
plugins: [vue()], plugins: [vue(), tailwindcss()]
server: {
port: 5173
}
}); });