diff --git a/frontend/public/about/hotlink-flow.jpg b/frontend/public/about/hotlink-flow.jpg new file mode 100644 index 0000000..f4a36b6 Binary files /dev/null and b/frontend/public/about/hotlink-flow.jpg differ diff --git a/frontend/public/about/zk-flow.jpg b/frontend/public/about/zk-flow.jpg new file mode 100644 index 0000000..cc7ff4e Binary files /dev/null and b/frontend/public/about/zk-flow.jpg differ diff --git a/frontend/src/assets/css/main.css b/frontend/src/assets/css/main.css index f0d965b..495633a 100644 --- a/frontend/src/assets/css/main.css +++ b/frontend/src/assets/css/main.css @@ -1,7 +1,5 @@ @import 'tailwindcss'; -@custom-variant dark (&:is(.dark *)); - @theme inline { --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); @@ -28,43 +26,23 @@ :root { --radius: 0.5rem; - --background: oklch(0.99 0.002 260); - --foreground: oklch(0.2 0.02 260); + --background: oklch(1 0 0); + --foreground: oklch(0.244 0.012 263); --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); + --card-foreground: oklch(0.244 0.012 263); + --primary: oklch(0.497 0.227 263); + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.94 0.025 263); + --secondary-foreground: oklch(0.28 0.04 263); + --muted: oklch(0.955 0.018 263); + --muted-foreground: oklch(0.46 0.04 263); + --accent: oklch(0.93 0.04 263); + --accent-foreground: oklch(0.28 0.04 263); --destructive: oklch(0.55 0.2 25); --success: oklch(0.45 0.14 155); - --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); - --success: oklch(0.68 0.14 155); - --border: oklch(0.32 0.02 260); - --input: oklch(0.32 0.02 260); - --ring: oklch(0.72 0.1 260); + --border: oklch(0.88 0.025 263); + --input: oklch(0.88 0.025 263); + --ring: oklch(0.497 0.227 263); } @layer base { @@ -76,3 +54,67 @@ @apply bg-background text-foreground antialiased; } } + +body:has(.lightbox:target) { + overflow: hidden; +} + +.expandable-image__link { + color: inherit; + cursor: zoom-in; +} + +.lightbox { + display: none; + position: fixed; + inset: 0; + z-index: 100; + align-items: center; + justify-content: center; + padding: 1.5rem; +} + +.lightbox:target { + display: flex; +} + +.lightbox__backdrop { + position: absolute; + inset: 0; + background: rgb(0 0 0 / 0.92); +} + +.lightbox__img { + position: relative; + z-index: 1; + max-width: min(95vw, 75rem); + max-height: 90vh; + width: auto; + height: auto; + object-fit: contain; + border: 1px solid var(--border); + border-radius: var(--radius-lg); +} + +.lightbox__close { + position: absolute; + top: 1rem; + right: 1rem; + z-index: 2; + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + font-size: 1.75rem; + line-height: 1; + color: var(--foreground); + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius-md); + text-decoration: none; +} + +.lightbox__close:hover { + border-color: var(--primary); +} diff --git a/frontend/src/components.d.ts b/frontend/src/components.d.ts index 1e40178..fe183f3 100644 --- a/frontend/src/components.d.ts +++ b/frontend/src/components.d.ts @@ -11,6 +11,10 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + AboutComparisonTable: typeof import('./components/about/AboutComparisonTable.vue')['default'] + AboutSectionNav: typeof import('./components/about/AboutSectionNav.vue')['default'] + AppHeader: typeof import('./components/layout/AppHeader.vue')['default'] + ExpandableImage: typeof import('./components/ExpandableImage.vue')['default'] InlineTabList: typeof import('./components/InlineTabList.vue')['default'] MetadataInspector: typeof import('./components/upload/MetadataInspector.vue')['default'] MetadataOutputPanel: typeof import('./components/upload/MetadataOutputPanel.vue')['default'] diff --git a/frontend/src/components/ExpandableImage.vue b/frontend/src/components/ExpandableImage.vue new file mode 100644 index 0000000..130ba93 --- /dev/null +++ b/frontend/src/components/ExpandableImage.vue @@ -0,0 +1,72 @@ + + + diff --git a/frontend/src/components/about/AboutComparisonTable.vue b/frontend/src/components/about/AboutComparisonTable.vue new file mode 100644 index 0000000..c7a6597 --- /dev/null +++ b/frontend/src/components/about/AboutComparisonTable.vue @@ -0,0 +1,137 @@ + + + diff --git a/frontend/src/components/about/AboutSectionNav.vue b/frontend/src/components/about/AboutSectionNav.vue new file mode 100644 index 0000000..34c052a --- /dev/null +++ b/frontend/src/components/about/AboutSectionNav.vue @@ -0,0 +1,29 @@ + + + diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index c0ff66e..cf7ea6a 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -3,6 +3,17 @@ import { ROUTE_NAMES } from '@/consts/routeNames'; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), + scrollBehavior(to, _from, savedPosition) { + if (savedPosition) { + return savedPosition; + } + + if (to.hash) { + return { el: to.hash, behavior: 'smooth' }; + } + + return { top: 0 }; + }, routes: [ { path: '/', @@ -10,6 +21,12 @@ const router = createRouter({ component: () => import('@/views/UploadView.vue'), meta: { title: 'Private Image - Upload' } }, + { + path: '/about', + name: ROUTE_NAMES.About, + component: () => import('@/views/AboutView.vue'), + meta: { title: 'Private Image - About' } + }, { path: '/v/:id', name: ROUTE_NAMES.Viewer, @@ -31,4 +48,10 @@ const router = createRouter({ ] }); +router.afterEach(to => { + const title = to.meta.title; + + document.title = typeof title === 'string' ? title : 'Private Image'; +}); + export default router; diff --git a/frontend/src/types/about/ComparisonTable.ts b/frontend/src/types/about/ComparisonTable.ts new file mode 100644 index 0000000..1be4840 --- /dev/null +++ b/frontend/src/types/about/ComparisonTable.ts @@ -0,0 +1,11 @@ +export type ComparisonHostId = 'privateImage' | 'imgbb' | 'imgur' | 'catbox'; + +export type ComparisonHost = { + id: ComparisonHostId; + name: string; +}; + +export type ComparisonRow = { + feature: string; + values: Record; +}; diff --git a/frontend/src/views/AboutView.vue b/frontend/src/views/AboutView.vue new file mode 100644 index 0000000..472acd1 --- /dev/null +++ b/frontend/src/views/AboutView.vue @@ -0,0 +1,197 @@ +