Merge pull request 'Slave/frontend shell' (#2) from slave/frontend-shell into master
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
@@ -34,4 +34,7 @@ THROTTLE_HOTLINK_PER_IP_LIMIT=1000
|
||||
THROTTLE_HOTLINK_PER_ID_TTL_MS=60000
|
||||
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
|
||||
|
||||
+4
-1
@@ -12,6 +12,9 @@
|
||||
/frontend/node_modules
|
||||
/frontend/dist
|
||||
|
||||
.vscode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/tailwind.css-data.json
|
||||
.idea
|
||||
*.log
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["bradlc.vscode-tailwindcss"]
|
||||
}
|
||||
Vendored
+10
@@ -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"
|
||||
}
|
||||
Vendored
+33
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
Generated
+1037
-39
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,12 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"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": {
|
||||
"@types/node": "^22.10.7",
|
||||
|
||||
+1
-30
@@ -1,32 +1,3 @@
|
||||
<template>
|
||||
<main class="home">
|
||||
<h1>Private Image</h1>
|
||||
</main>
|
||||
<router-view />
|
||||
</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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export const ROUTE_NAMES = {
|
||||
Upload: 'Upload',
|
||||
Viewer: 'Viewer',
|
||||
Delete: 'Delete'
|
||||
} as const;
|
||||
@@ -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'
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,9 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import '@/assets/css/main.css';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
|
||||
createApp(App).mount('#app');
|
||||
const pinia = createPinia();
|
||||
|
||||
createApp(App).use(pinia).use(router).mount('#app');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Vendored
+11
@@ -1 +1,12 @@
|
||||
/// <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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
@@ -11,8 +12,5 @@ export default defineConfig({
|
||||
'@': path.resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 5173
|
||||
}
|
||||
plugins: [vue(), tailwindcss()]
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user