This commit is contained in:
112
AGENTS.md
Normal file
112
AGENTS.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# aranroig.com
|
||||||
|
|
||||||
|
Monorepo for aranroig.com — a personal portfolio website with blog, art gallery, and project showcase.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Frontend**: Nuxt 4 + Vue 3 + TypeScript + SCSS
|
||||||
|
- **Backend**: Express 5 + Mongoose (MongoDB)
|
||||||
|
- **i18n**: @nuxtjs/i18n (English, Spanish, Catalan)
|
||||||
|
- **Content**: Nuxt Content v3 with Zod-sourced Markdown collections
|
||||||
|
- **Image optimization**: @nuxt/image
|
||||||
|
- **Build**: Vite
|
||||||
|
- **Deployment**: Docker Compose + Nginx reverse proxy + Gitea Actions CI/CD
|
||||||
|
- **Process management**: PM2 (production)
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
├── backend/ # Express API server (port 5000)
|
||||||
|
│ └── src/
|
||||||
|
│ ├── index.js # Entry point, single /api/test endpoint
|
||||||
|
│ └── db.js # MongoDB connection via Mongoose
|
||||||
|
├── frontend/ # Nuxt application
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── assets/css/ # Global SCSS: colors.scss (themes), fonts.scss, main.scss
|
||||||
|
│ │ ├── components/ # Vue components (TUI-styled)
|
||||||
|
│ │ │ ├── layouts/ # Page layout wrappers
|
||||||
|
│ │ │ ├── parts/ # Header, footer, navigation pieces
|
||||||
|
│ │ │ └── content/ # Blog, art content components
|
||||||
|
│ │ ├── composables/ # useApi(), theme.ts (light/dark + accent colors)
|
||||||
|
│ │ ├── i18n.config.ts# VueI18n fallback locale setup
|
||||||
|
│ │ └── pages/ # File-based routing (index, blog, art, contact)
|
||||||
|
│ ├── content/ # Markdown collections organized by locale
|
||||||
|
│ │ ├── fixed/ # Static pages per locale
|
||||||
|
│ │ ├── blog/ # Blog posts
|
||||||
|
│ │ ├── art/ # Art pieces
|
||||||
|
│ │ └── projects/ # Project cards
|
||||||
|
│ ├── i18n/locales/ # en.json, es.json, ca.json translation files
|
||||||
|
│ ├── content.config.ts # Content collection schemas (Zod)
|
||||||
|
│ └── nuxt.config.ts # Nuxt config: modules, i18n, runtime config
|
||||||
|
├── docker-compose.yml # Services: nginx, frontend, backend
|
||||||
|
├── nginx.conf # Reverse proxy routing /api/ → backend:5000
|
||||||
|
├── ecosystem.config.js # PM2 production deployment config
|
||||||
|
└── .gitea/workflows/ # Gitea Actions CI/CD (deploy on master push)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
From repo root:
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `npm run dev` | Run frontend + backend concurrently with logging prefixes `[BACKEND]` and `[FRONTEND]` |
|
||||||
|
| `npm install` | Install monorepo dependencies (triggers `frontend/postinstall`) |
|
||||||
|
|
||||||
|
In `frontend/`:
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `npm run dev` | Nuxt dev server with live reload |
|
||||||
|
| `npm run build` | Production build with `.env.production` |
|
||||||
|
| `npm run generate` | Static site generation |
|
||||||
|
| `npm run preview` | Preview built production output |
|
||||||
|
|
||||||
|
In `backend/`:
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `npm run dev` | Nodemon on `src/index.js` (auto-restart, port 5000) |
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
- Use the monorepo root script: `npm run dev` — runs both frontend and backend in parallel.
|
||||||
|
- Frontend uses the `~/` alias mapping to `frontend/app/`. Auto-imported composables include `useAsyncData`, `useI18n`, `useRoute`, `useLocalePath`, etc.
|
||||||
|
- Backend reads `.env.development` or `.env.production` based on `NODE_ENV`. Requires `DB_URI` for MongoDB.
|
||||||
|
- Frontend runtime config key `public.apiBaseUrl` sets the backend API URL used by `useApi()`.
|
||||||
|
- Content lives in `frontend/content/` under collection folders (`blog`, `fixed`, `art`, `projects`), each with locale subdirectories (`en`, `es`, `ca`).
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- Vue SFCs use `<script setup>` (TypeScript via `lang="ts"` where needed, plain JS otherwise).
|
||||||
|
- All styling is SCSS with scoped styles per component. Page-level blog/art pages use global styles.
|
||||||
|
- Mobile-first breakpoints: `600px`, `900px`, `1200px` (max-width queries).
|
||||||
|
- File names: kebab-case for components and pages.
|
||||||
|
- CSS variables driven by SCSS maps define themes (light/dark) and accent colors.
|
||||||
|
- TUI aesthetic: box-drawing characters, monospace font (**Hermit** / "Hurmit" via Nerd Fonts), `steps()` easing transitions, pixel-art SVG elements.
|
||||||
|
|
||||||
|
## Content Collections
|
||||||
|
|
||||||
|
Content frontmatter is validated with Zod schemas defined in `frontend/content.config.ts`:
|
||||||
|
|
||||||
|
| Collection | Fields |
|
||||||
|
|------------|--------|
|
||||||
|
| `blog` | title, slug, date, description |
|
||||||
|
| `art` | title, slug, thumb, date |
|
||||||
|
| `projects` | title, slug, description, link, tech (array) |
|
||||||
|
| `fixed` | title, slug, description |
|
||||||
|
|
||||||
|
Write Markdown files under the corresponding collection folder with locale subdirectories. Frontmatter must conform to the Zod schema.
|
||||||
|
|
||||||
|
## Theming
|
||||||
|
|
||||||
|
- Theme selector switches between light/dark modes and accent color palettes (dragon-themed names: katlum, solus, silang, nozt, albor).
|
||||||
|
- Preferences persist in `localStorage`. Respects `prefers-color-scheme` on first visit.
|
||||||
|
- Theme setup runs via `setupTheme()` composable called on app mount (`app.vue`).
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
1. Gitea Actions CI/CD triggers on push to `master`, builds Docker images, pushes to Gitea registry.
|
||||||
|
2. SSHs into deploy host and runs `docker-compose pull && docker-compose up -d`.
|
||||||
|
3. For manual deployment, `docker-compose.yml` orchestrates nginx + frontend + backend services.
|
||||||
|
4. Production process management uses PM2 via `ecosystem.config.js`.
|
||||||
@@ -2,28 +2,30 @@
|
|||||||
|
|
||||||
$themes: (
|
$themes: (
|
||||||
dark: (
|
dark: (
|
||||||
background: #141414,
|
background: #0e0e0e,
|
||||||
background-line: #202324,
|
background-line: #181818,
|
||||||
background-fore: #10141f,
|
background-fore: #1a1a1a,
|
||||||
hover: #21262d,
|
hover: #1f1f1f,
|
||||||
selected: #4a4a4b,
|
selected: #3a3a3a,
|
||||||
border-color: #819796,
|
border-color: #5c5c5c,
|
||||||
border: #202324,
|
border: #181818,
|
||||||
text: #ebede9,
|
text: #d4d4d4,
|
||||||
container-shadow: #151d28,
|
container-shadow: #0a0a0a,
|
||||||
sticky-header-bg: #20202077
|
sticky-header-bg: #161616cc,
|
||||||
|
frame-glow: #44444455,
|
||||||
),
|
),
|
||||||
light: (
|
light: (
|
||||||
background: #ffffff,
|
background: #ffffff,
|
||||||
background-line: #f0f0f0,
|
background-line: #f0f0f0,
|
||||||
background-fore: #ffffff,
|
background-fore: #f5f5f5,
|
||||||
border-color: #e0e0e0,
|
border-color: #e0e0e0,
|
||||||
border: #f0f0f0,
|
border: #f0f0f0,
|
||||||
hover: #e9e9e9,
|
hover: #e9e9e9,
|
||||||
selected: #d4d4d4,
|
selected: #d4d4d4,
|
||||||
text: #1e1e1e,
|
text: #1e1e1e,
|
||||||
container-shadow: #5f6774,
|
container-shadow: #5f6774,
|
||||||
sticky-header-bg: #fff
|
sticky-header-bg: #fff,
|
||||||
|
frame-glow: #00000022,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
scroll-padding-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-size: 28px 28px;
|
background-color: var(--color-background);
|
||||||
background-image:
|
|
||||||
linear-gradient(to right, var(--color-background-line) 1px, transparent 1px),
|
|
||||||
linear-gradient(to bottom, var(--color-background-line) 1px, var(--color-background) 1px);
|
|
||||||
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -17,6 +19,7 @@ body {
|
|||||||
* {
|
* {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-family: 'Hurmit';
|
font-family: 'Hurmit';
|
||||||
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pixelated {
|
.pixelated {
|
||||||
@@ -31,20 +34,103 @@ ul {
|
|||||||
list-style: none; /* Remove default bullets */
|
list-style: none; /* Remove default bullets */
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
ul > li {padding: 0px 25px} /* Stretching li elements a little so it looks prettier */
|
|
||||||
li {
|
/* TUI-style nested lists with box-drawing chars */
|
||||||
|
ul > li {
|
||||||
|
padding: 4px 24px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
li::before {
|
li::before {
|
||||||
content: "";
|
width: auto;
|
||||||
width: 8px;
|
height: auto;
|
||||||
height: 8px;
|
background-color: transparent;
|
||||||
background-color: var(--color-text);
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
top: 0px;
|
||||||
left: 4px;
|
left: 0px;
|
||||||
|
color: var(--color-border-color);
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--color-link);
|
color: var(--color-link);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TUI-style blockquotes */
|
||||||
|
blockquote {
|
||||||
|
margin: 12px 0;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-left: 3px solid var(--color-link);
|
||||||
|
background-color: var(--color-background-line);
|
||||||
|
font-style: italic;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote::before {
|
||||||
|
content: "┃ ";
|
||||||
|
position: absolute;
|
||||||
|
left: -16px;
|
||||||
|
color: var(--color-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TUI-style code blocks */
|
||||||
|
pre, code {
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
background-color: var(--color-background-line);
|
||||||
|
border: 1px solid var(--color-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
padding: 12px 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 12px 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "┌── CODE ─────────────────────────────────────┐";
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
left: 8px;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
background-color: var(--color-background-fore);
|
||||||
|
padding: 0 4px;
|
||||||
|
color: var(--color-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TUI-style horizontal rules */
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--color-border-color);
|
||||||
|
margin: 20px 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "──────────";
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: var(--color-background);
|
||||||
|
padding: 0 8px;
|
||||||
|
color: var(--color-border-color);
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selection styling */
|
||||||
|
::selection {
|
||||||
|
background-color: var(--color-link);
|
||||||
|
color: var(--color-background-fore);
|
||||||
}
|
}
|
||||||
@@ -1,64 +1,140 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="tui-frame">
|
||||||
|
<span class="frame-corner tui-corner-tl"></span>
|
||||||
<span class="corner tl"></span>
|
<span class="frame-line frame-line-top" aria-hidden="true">═══════════════════</span>
|
||||||
<span class="corner tr"></span>
|
<span class="frame-corner frame-corner-tr"></span>
|
||||||
<span class="corner bl"></span>
|
|
||||||
<span class="corner br"></span>
|
|
||||||
|
|
||||||
|
<div class="tui-inner">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<span class="frame-line frame-line-bottom" aria-hidden="true">═══════════════════</span>
|
||||||
|
<span class="frame-corner frame-corner-bl"></span>
|
||||||
|
<span class="frame-corner frame-corner-br"></span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
$separation: 2px;
|
.tui-frame {
|
||||||
|
|
||||||
.card{
|
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 15px;
|
margin: 15px;
|
||||||
background: var(--color-background-fore);
|
background: var(--color-background-fore);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-inner {
|
||||||
padding: 8px 24px;
|
padding: 8px 24px;
|
||||||
color: white;
|
color: white;
|
||||||
border: 1px solid var(--color-border-color);
|
position: relative;
|
||||||
box-shadow: 8px -8px 0px 0px var(--color-container-shadow);
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.corner{
|
.frame-corner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 22px;
|
width: 24px;
|
||||||
height: 22px;
|
height: 24px;
|
||||||
border-color: #cfcfcf;
|
font-family: 'Hurmit', monospace;
|
||||||
|
color: var(--color-border-color);
|
||||||
|
font-size: 0;
|
||||||
|
line-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Top Left */
|
.tui-corner-tl,
|
||||||
.tl{
|
.frame-corner-tr {
|
||||||
top: -$separation;
|
top: -2px;
|
||||||
left: -$separation;
|
|
||||||
border-top:2px solid;
|
|
||||||
border-left:2px solid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Top Right */
|
.frame-corner-bl,
|
||||||
.tr{
|
.frame-corner-br {
|
||||||
top: -$separation;
|
bottom: -2px;
|
||||||
right: -$separation;
|
|
||||||
border-top:2px solid;
|
|
||||||
border-right:2px solid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bottom Left */
|
.frame-corner-tl {
|
||||||
.bl{
|
left: -2px;
|
||||||
bottom: -$separation;
|
&::before {
|
||||||
left: -$separation;
|
content: "╔";
|
||||||
border-bottom:2px solid;
|
}
|
||||||
border-left:2px solid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bottom Right */
|
.frame-corner-tr {
|
||||||
.br{
|
right: -2px;
|
||||||
bottom: -$separation;
|
&::before {
|
||||||
right: -$separation;
|
content: "╗";
|
||||||
border-bottom:2px solid;
|
}
|
||||||
border-right:2px solid;
|
}
|
||||||
|
|
||||||
|
.frame-corner-bl {
|
||||||
|
left: -2px;
|
||||||
|
&::before {
|
||||||
|
content: "╚";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-corner-br {
|
||||||
|
right: -2px;
|
||||||
|
&::before {
|
||||||
|
content: "╝";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-line {
|
||||||
|
position: absolute;
|
||||||
|
left: 24px;
|
||||||
|
right: 24px;
|
||||||
|
color: var(--color-border-color);
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 0;
|
||||||
|
line-height: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-line-top {
|
||||||
|
top: -2px;
|
||||||
|
&::before {
|
||||||
|
content: "═══════════════════";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-line-bottom {
|
||||||
|
bottom: -2px;
|
||||||
|
&::before {
|
||||||
|
content: "═══════════════════";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
.frame-corner,
|
||||||
|
.frame-line {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-frame {
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: "═══════════════════";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
color: var(--color-border-color);
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 8px;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
bottom: -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-inner {
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="footer">
|
<div class="tui-statusbar">
|
||||||
<span>{{ $t('prefooter') }}</span>
|
<span>{{ $t('prefooter') }}</span>
|
||||||
<br>
|
<br>
|
||||||
<span>{{ $t('footer') }}</span>
|
<span>{{ $t('footer') }}</span>
|
||||||
@@ -7,7 +10,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.footer {
|
.tui-statusbar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -20,3 +23,4 @@ span {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,131 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const localePath = useLocalePath()
|
const localePath = useLocalePath()
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const scrollTargets: Record<string, string> = {
|
||||||
|
'index': 'top',
|
||||||
|
'blog': '#scroll-blog',
|
||||||
|
'contact': '#scroll-contact',
|
||||||
|
'art': '#scroll-art'
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPath = computed(() => route.path)
|
||||||
|
|
||||||
|
function handleClick(targetKey: string, e: MouseEvent) {
|
||||||
|
if (e.ctrlKey || e.metaKey || e.altKey || e.button !== 0) return
|
||||||
|
const isHome = currentPath.value === localePath('index')
|
||||||
|
if (!isHome) return
|
||||||
|
e.preventDefault()
|
||||||
|
const targetId = scrollTargets[targetKey] || 'top'
|
||||||
|
if (targetId === 'top') {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
} else {
|
||||||
|
const el = document.querySelector(targetId)
|
||||||
|
if (el) {
|
||||||
|
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
|
} else {
|
||||||
|
const hash = targetId.substring(1)
|
||||||
|
router.replace({ hash })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="menus">
|
<div class="tui-tabs">
|
||||||
<NuxtLink :to="localePath('index')">/</NuxtLink>
|
<a :href="localePath('index')" class="tui-tab" @click="handleClick('index', $event)">
|
||||||
<NuxtLink :to="localePath('blog')">/{{ $t('header.links.blog') }}</NuxtLink>
|
<span class="tab-sep" v-if="false">│</span>
|
||||||
<NuxtLink :to="localePath('contact')">/{{ $t('header.links.contact') }}</NuxtLink>
|
<span class="tab-label">/</span>
|
||||||
<NuxtLink :to="localePath('art')">/{{ $t('header.links.art') }}</NuxtLink>
|
</a>
|
||||||
<!-- <NuxtLink class="disabled">Drawings</NuxtLink> -->
|
<a :href="localePath('blog')" class="tui-tab" @click="handleClick('blog', $event)">
|
||||||
|
<span class="tab-sep">│</span>
|
||||||
|
<span class="tab-label">/{{ $t('header.links.blog') }}</span>
|
||||||
|
</a>
|
||||||
|
<a :href="localePath('contact')" class="tui-tab" @click="handleClick('contact', $event)">
|
||||||
|
<span class="tab-sep">│</span>
|
||||||
|
<span class="tab-label">/{{ $t('header.links.contact') }}</span>
|
||||||
|
</a>
|
||||||
|
<a :href="localePath('art')" class="tui-tab" @click="handleClick('art', $event)">
|
||||||
|
<span class="tab-sep">│</span>
|
||||||
|
<span class="tab-label">/{{ $t('header.links.art') }}</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
.menus > a {
|
.tui-tabs {
|
||||||
margin-right: 10px;
|
display: inline-flex;
|
||||||
margin-left: 10px;
|
background-color: var(--color-background-fore);
|
||||||
text-decoration: none;
|
border: 1px solid var(--color-border-color);
|
||||||
text-shadow: 0 0 1px var(--color-link);
|
box-shadow: inset 2px -2px 0px 0px rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tui-tab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
text-shadow: none;
|
||||||
|
transition: all 0.1s steps(2, end);
|
||||||
|
border-right: 1px solid var(--color-border-color);
|
||||||
|
|
||||||
.menus > a.router-link-exact-active {
|
&:last-child {
|
||||||
color: var(--color-link);
|
border-right: none;
|
||||||
text-shadow: 0 0 8px var(--color-link);
|
}
|
||||||
|
|
||||||
|
.tab-sep {
|
||||||
|
color: var(--color-text);
|
||||||
|
opacity: 0.3;
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-hover);
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-tab.router-link-exact-active {
|
||||||
|
color: var(--color-background-fore);
|
||||||
|
background-color: var(--color-link);
|
||||||
|
text-shadow: 0 0 8px rgba(255,255,255,0.3);
|
||||||
|
|
||||||
|
.tab-sep {
|
||||||
|
color: var(--color-background-fore);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled {
|
.disabled {
|
||||||
color: #aaaaaa77;
|
color: #aaaaaa77;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
.tui-tab {
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
|
||||||
|
.tab-sep {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
.tui-tab {
|
||||||
|
padding: 3px 5px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
|
||||||
|
.tab-sep {
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -4,46 +4,45 @@ import SiteOptions from './site_options/SiteOptions.vue';
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="sticky-header">
|
<div class="tui-minimalbar">
|
||||||
<div class="container">
|
<div class="minimal-inner">
|
||||||
<div class="sticky-header-inner">
|
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<h1>ARANROIG.COM</h1>
|
<span class="sticky-title" aria-hidden="true">◆ ARANROIG.COM</span>
|
||||||
<HeaderLinks />
|
<HeaderLinks />
|
||||||
</div>
|
</div>
|
||||||
<SiteOptions />
|
<SiteOptions />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div style="height: 80px"></div>
|
<div style="height: 80px"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.sticky-header {
|
.tui-minimalbar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
|
||||||
background: var(--color-sticky-header-bg, #fff);
|
background: var(--color-sticky-header-bg);
|
||||||
border-bottom: 1px solid var(--color-sticky-header-border, rgba(0, 0, 0, 0.08));
|
backdrop-filter: blur(8px);
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 4px 0px 0px var(--color-container-shadow);
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sticky-header-inner {
|
.minimal-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 10px 0;
|
padding: 6px 24px;
|
||||||
|
|
||||||
h1 {
|
@media screen and (max-width: 900px) {
|
||||||
margin: 0;
|
padding: 5px 16px;
|
||||||
font-size: inherit;
|
}
|
||||||
white-space: nowrap;
|
|
||||||
font-weight: normal;
|
@media screen and (max-width: 600px) {
|
||||||
|
padding: 4px 12px;
|
||||||
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +50,22 @@ import SiteOptions from './site_options/SiteOptions.vue';
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-left: 30px;
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sticky-title {
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -2,13 +2,51 @@
|
|||||||
import HeaderLinks from './HeaderLinks.vue';
|
import HeaderLinks from './HeaderLinks.vue';
|
||||||
import SiteOptions from './site_options/SiteOptions.vue';
|
import SiteOptions from './site_options/SiteOptions.vue';
|
||||||
import StickyHeader from './StickyHeader.vue';
|
import StickyHeader from './StickyHeader.vue';
|
||||||
|
|
||||||
|
const asciiLines = [
|
||||||
|
"░█▀█░█▀▄░█▀█░█▀█░█▀▄░█▀█░▀█▀░█▀▀",
|
||||||
|
"░█▀█░█▀▄░█▀█░█░█░█▀▄░█░█░░█░░█░█",
|
||||||
|
"░▀░▀░▀░▀░▀░▀░▀░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀"
|
||||||
|
];
|
||||||
|
|
||||||
|
const revealedLines = ref<number>(0);
|
||||||
|
let asciiTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
const HAS_ANIMATED_KEY = 'ascii-animated';
|
||||||
|
|
||||||
|
function startAsciiAnimation() {
|
||||||
|
if (sessionStorage.getItem(HAS_ANIMATED_KEY)) {
|
||||||
|
revealedLines.value = asciiLines.length;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = 400;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
asciiTimer = setInterval(() => {
|
||||||
|
count++;
|
||||||
|
revealedLines.value = count;
|
||||||
|
if (count >= asciiLines.length) {
|
||||||
|
clearInterval(asciiTimer!);
|
||||||
|
asciiTimer = null;
|
||||||
|
sessionStorage.setItem(HAS_ANIMATED_KEY, '1');
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
startAsciiAnimation();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (asciiTimer) clearInterval(asciiTimer);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header-container website">
|
<div class="header-container website">
|
||||||
<h1>ARANROIG.COM</h1>
|
<pre v-if="revealedLines > 0" class="ascii-title" aria-hidden="true">{{ asciiLines.slice(0, revealedLines).join('\n') }}</pre>
|
||||||
<HeaderLinks></HeaderLinks>
|
<HeaderLinks></HeaderLinks>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,6 +70,10 @@ import StickyHeader from './StickyHeader.vue';
|
|||||||
|
|
||||||
.header-container {
|
.header-container {
|
||||||
&.website {
|
&.website {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
@media screen and (max-width: 600px) {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
@@ -42,4 +84,23 @@ import StickyHeader from './StickyHeader.vue';
|
|||||||
position:relative;
|
position:relative;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ascii-title {
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
color: var(--color-link);
|
||||||
|
text-shadow: 0 0 8px var(--color-link), 0 0 4px var(--color-link);
|
||||||
|
font-size: clamp(0.35rem, 1.2vw, 0.65rem);
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.1ch;
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
.ascii-title {
|
||||||
|
font-size: clamp(0.28rem, 1.8vw, 0.5rem);
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.1ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -14,35 +14,33 @@ function onScroll() {
|
|||||||
|
|
||||||
onMounted(() => window.addEventListener('scroll', onScroll, { passive: true }));
|
onMounted(() => window.addEventListener('scroll', onScroll, { passive: true }));
|
||||||
onUnmounted(() => window.removeEventListener('scroll', onScroll));
|
onUnmounted(() => window.removeEventListener('scroll', onScroll));
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="sticky-header" :class="{ visible: isVisible }">
|
<div class="tui-stickybar" :class="{ visible: isVisible }">
|
||||||
<div class="container">
|
<div class="sticky-inner">
|
||||||
<div class="sticky-header-inner">
|
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<h1>ARANROIG.COM</h1>
|
<span class="sticky-title" aria-hidden="true">◆ ARANROIG.COM</span>
|
||||||
<HeaderLinks />
|
<HeaderLinks />
|
||||||
</div>
|
</div>
|
||||||
<SiteOptions />
|
<SiteOptions />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
.sticky-header {
|
.tui-stickybar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
|
||||||
background: var(--color-sticky-header-bg, #fff);
|
background: var(--color-sticky-header-bg);
|
||||||
border-bottom: 1px solid var(--color-sticky-header-border, rgba(0, 0, 0, 0.08));
|
backdrop-filter: blur(8px);
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 4px 0px 0px var(--color-container-shadow);
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
|
|
||||||
// Hidden state — translated up and invisible
|
// Hidden state — translated up and invisible
|
||||||
transform: translateY(-100%);
|
transform: translateY(-100%);
|
||||||
@@ -57,21 +55,26 @@ onUnmounted(() => window.removeEventListener('scroll', onScroll));
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
transition:
|
||||||
|
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sticky-header-inner {
|
.sticky-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 10px 0;
|
padding: 6px 24px;
|
||||||
|
|
||||||
h1 {
|
@media screen and (max-width: 900px) {
|
||||||
margin: 0;
|
padding: 5px 16px;
|
||||||
font-size: inherit; // inherit from your existing h1 styles
|
}
|
||||||
white-space: nowrap;
|
|
||||||
font-weight: normal;
|
@media screen and (max-width: 600px) {
|
||||||
|
padding: 4px 12px;
|
||||||
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +82,22 @@ onUnmounted(() => window.removeEventListener('scroll', onScroll));
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-left: 30px;
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sticky-title {
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -9,11 +9,16 @@ const spritePath = computed(() => {
|
|||||||
});
|
});
|
||||||
const hasAnimated = useState('title-animated', () => false)
|
const hasAnimated = useState('title-animated', () => false)
|
||||||
|
|
||||||
const fullText = "ARANROIG.COM";
|
const asciiArt = [
|
||||||
const displayedText = ref("");
|
"█▀█ █▀▄ █▀█ █▀█ █▀▄ █▀█ ▀█▀ █▀▀ █▀▀ █▀█ █▄█",
|
||||||
|
"█▀█ █▀▄ █▀█ █ █ █▀▄ █ █ █ █ █ █ █ █ █ █",
|
||||||
|
"▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀▀▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀"
|
||||||
|
];
|
||||||
|
const fullText = asciiArt.join('\n');
|
||||||
|
|
||||||
let index = 0
|
const displayedArt = ref("");
|
||||||
let interval = null
|
let charIndex = 0;
|
||||||
|
let intervalId: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
const dragon_names = ["katlum", "solus"];
|
const dragon_names = ["katlum", "solus"];
|
||||||
const sprite_names = dragon_names.map(name => `/sprites/${name}/${name}.gif`);
|
const sprite_names = dragon_names.map(name => `/sprites/${name}/${name}.gif`);
|
||||||
@@ -33,41 +38,45 @@ const preloadImages = (imageArray) => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
preloadImages(sprite_names);
|
preloadImages(sprite_names);
|
||||||
if (hasAnimated.value) {
|
|
||||||
displayedText.value = fullText
|
if (hasAnimated.value || displayedArt.value === fullText) {
|
||||||
return
|
displayedArt.value = fullText;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
interval = setInterval(() => {
|
intervalId = setInterval(() => {
|
||||||
if (index < fullText.length) {
|
if (charIndex < fullText.length) {
|
||||||
displayedText.value += fullText[index]
|
displayedArt.value += fullText[charIndex];
|
||||||
index++
|
charIndex++;
|
||||||
} else {
|
} else {
|
||||||
clearInterval(interval)
|
clearInterval(intervalId!);
|
||||||
|
intervalId = null;
|
||||||
hasAnimated.value = true;
|
hasAnimated.value = true;
|
||||||
}
|
}
|
||||||
}, 35) // velocidad de escritura
|
}, 5);
|
||||||
})
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
clearInterval(interval)
|
if (intervalId) clearInterval(intervalId);
|
||||||
})
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="header-container website">
|
<div class="header-container website">
|
||||||
<h1 class="title">{{ displayedText }}</h1>
|
<pre v-html="displayedArt + (displayedArt.length < fullText.length ? '_' : '')" class="title ascii-art" aria-label="ARANROIG.COM"></pre>
|
||||||
<HeaderLinks></HeaderLinks>
|
<HeaderLinks></HeaderLinks>
|
||||||
</div>
|
</div>
|
||||||
<div class="undertable">
|
<div class="header-container right">
|
||||||
<Sprite :path="spritePath" bottom="-60px" left="-75px"></Sprite>
|
|
||||||
</div>
|
|
||||||
<div class="header-container">
|
|
||||||
<SiteOptions></SiteOptions>
|
<SiteOptions></SiteOptions>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="undertable-wrapper">
|
||||||
|
<div class="undertable">
|
||||||
|
<Sprite :path="spritePath"></Sprite>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<StickyHeader></StickyHeader>
|
<StickyHeader></StickyHeader>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -90,34 +99,33 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.undertable {
|
.undertable-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
left: -200px;
|
margin-top: -100px;
|
||||||
margin-top: 450px;
|
display: flex;
|
||||||
margin-left: -425px;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
@media screen and (max-width: 1200px) and (min-width: 900px) {
|
@media screen and (max-width: 1200px) and (min-width: 900px) {
|
||||||
left: -50px;
|
margin-top: -340px;
|
||||||
bottom: 20px;
|
|
||||||
margin-top: 350px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 900px) and (min-width: 600px) {
|
@media screen and (max-width: 900px) and (min-width: 600px) {
|
||||||
left: 70px;
|
margin-top: -240px;
|
||||||
bottom: 50px;
|
|
||||||
margin-top: 250px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
@media screen and (max-width: 600px) {
|
||||||
left: 150px;
|
margin-top: -240px;
|
||||||
bottom: 70px;
|
|
||||||
margin-top: 250px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.undertable {
|
||||||
|
position: relative;
|
||||||
|
width: 160px;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
.web-links {
|
.web-links {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
@@ -127,14 +135,13 @@ onBeforeUnmount(() => {
|
|||||||
background-color: var(--color-background);
|
background-color: var(--color-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.ascii-art {
|
||||||
width: 300px;
|
font-family: 'Hurmit', monospace;
|
||||||
}
|
font-size: clamp(0.45rem, 1.8vw, 0.85rem);
|
||||||
|
line-height: 1.18;
|
||||||
.title::after {
|
letter-spacing: -0.05ch;
|
||||||
content: "_";
|
white-space: pre;
|
||||||
animation: blink 3s infinite;
|
color: var(--color-text);
|
||||||
animation-timing-function: steps(1, end);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blink {
|
@keyframes blink {
|
||||||
|
|||||||
@@ -55,14 +55,14 @@ const changeLocale = (lang) => {
|
|||||||
background: var(--color-background-fore);
|
background: var(--color-background-fore);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
border: 1px solid #30363d;
|
border: 1px solid var(--color-border-color);
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 6px;
|
border-radius: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: #30363d;
|
background-color: var(--color-selected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Dropdown transitions */
|
/* Dropdown transitions */
|
||||||
@@ -81,10 +81,21 @@ const changeLocale = (lang) => {
|
|||||||
right: 110px;
|
right: 110px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
background: var(--color-background-fore);
|
background: var(--color-background-fore);
|
||||||
border: 1px solid #30363d;
|
border: 1px solid var(--color-border-color);
|
||||||
border-radius: 8px;
|
border-radius: 0;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
box-shadow: 0 8px 24px rgba(0,0,0,0.6);
|
box-shadow: 4px -4px 0px 0px rgba(0,0,0,0.3);
|
||||||
|
border-left: 2px solid var(--color-border-color);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "▸";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: -16px;
|
||||||
|
top: -2px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-border-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Items */
|
/* Items */
|
||||||
|
|||||||
@@ -89,19 +89,39 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.circle {
|
.circle {
|
||||||
width: 12px;
|
width: 10px;
|
||||||
height: 12px;
|
height: 10px;
|
||||||
margin-top: -3px;
|
margin-top: -1px;
|
||||||
border-radius: 50%; /* makes it a circle */
|
border-radius: 0;
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: 2px var(--color-border) solid;
|
border: 1px solid var(--color-border-color);
|
||||||
|
|
||||||
&.dark {
|
&.dark {
|
||||||
background-color: rgb(28, 28, 28);
|
background-color: rgb(28, 28, 28);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "◆";
|
||||||
|
color: white;
|
||||||
|
font-size: 6px;
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
left: 1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&.light {
|
&.light {
|
||||||
background-color: rgb(232, 232, 232);
|
background-color: rgb(232, 232, 232);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "◆";
|
||||||
|
color: #666;
|
||||||
|
font-size: 6px;
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
left: 1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&.katlum {
|
&.katlum {
|
||||||
background-color: #4f8fba;
|
background-color: #4f8fba;
|
||||||
@@ -141,9 +161,9 @@ onUnmounted(() => {
|
|||||||
background: var(--color-background-fore);
|
background: var(--color-background-fore);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
border: 1px solid #30363d;
|
border: 1px solid var(--color-border-color);
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 6px;
|
border-radius: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
}
|
}
|
||||||
@@ -166,10 +186,10 @@ onUnmounted(() => {
|
|||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
background: var(--color-background-fore);
|
background: var(--color-background-fore);
|
||||||
border: 1px solid #30363d;
|
border: 1px solid var(--color-border-color);
|
||||||
border-radius: 8px;
|
border-radius: 0;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
box-shadow: 0 8px 24px rgba(0,0,0,0.6);
|
box-shadow: 4px -4px 0px 0px rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sections */
|
/* Sections */
|
||||||
@@ -183,7 +203,7 @@ onUnmounted(() => {
|
|||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
border-radius: 5px;
|
border-radius: 0;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -209,8 +229,16 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
/* Divider */
|
/* Divider */
|
||||||
.divider {
|
.divider {
|
||||||
height: 1px;
|
height: 0;
|
||||||
background: #30363d;
|
color: var(--color-border-color);
|
||||||
margin: 6px 0;
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 0;
|
||||||
|
line-height: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin: 8px 16px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "═══════════";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -14,7 +14,7 @@ const { data: post } = await useAsyncData(`art-${slug}`, () =>
|
|||||||
<!-- Render the blog post as Prose & Vue components -->
|
<!-- Render the blog post as Prose & Vue components -->
|
||||||
<MinimalHeader></MinimalHeader>
|
<MinimalHeader></MinimalHeader>
|
||||||
<div class="extended-container">
|
<div class="extended-container">
|
||||||
<ContentRenderer :value="post" class="art" />
|
<ContentRenderer v-if="post" :value="post" class="art" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
import FixedLayout from '~/components/layouts/FixedLayout.vue';
|
import FixedLayout from '~/components/layouts/FixedLayout.vue';
|
||||||
import TableHeader from '~/components/parts/TableHeader.vue';
|
import MinimalHeader from '~/components/parts/MinimalHeader.vue';
|
||||||
|
|
||||||
const { locale } = useI18n();
|
const { locale, t } = useI18n();
|
||||||
const localePath = useLocalePath();
|
const localePath = useLocalePath();
|
||||||
|
|
||||||
const { data: posts } = useAsyncData('art-posts', async () => {
|
const { data: posts } = useAsyncData('art-posts', async () => {
|
||||||
@@ -40,17 +41,28 @@ const { data: posts } = useAsyncData('art-posts', async () => {
|
|||||||
}, { watch: [locale, () => useRoute().path] });
|
}, { watch: [locale, () => useRoute().path] });
|
||||||
|
|
||||||
const isFallback = (art) => art.path.startsWith('/art/en/') && locale.value !== 'en';
|
const isFallback = (art) => art.path.startsWith('/art/en/') && locale.value !== 'en';
|
||||||
|
|
||||||
|
const ART_LIMIT = 6;
|
||||||
|
const showAll = ref(false);
|
||||||
|
|
||||||
|
const displayedArt = computed(() => {
|
||||||
|
const allPosts = posts.value || [];
|
||||||
|
if (showAll.value || allPosts.length <= ART_LIMIT) return allPosts;
|
||||||
|
return allPosts.slice(0, ART_LIMIT);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TableHeader></TableHeader>
|
<MinimalHeader></MinimalHeader>
|
||||||
<FixedLayout>
|
<FixedLayout>
|
||||||
<Container>
|
<Container>
|
||||||
|
<h2 class="section-title">ART GALLERY</h2>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<NuxtLink v-for="art in posts"
|
<NuxtLink v-for="art in displayedArt"
|
||||||
:key="art.slug"
|
:key="art.slug"
|
||||||
class="selector"
|
class="selector"
|
||||||
:to="isFallback(art) ? `/art/${art.slug}` : localePath(`/art/${art.slug}`)">
|
:to="isFallback(art) ? `/art/${art.slug}` : localePath(`/art/${art.slug}`)">
|
||||||
|
<span class="selector-border-top" aria-hidden="true">────────</span>
|
||||||
<NuxtImg
|
<NuxtImg
|
||||||
:src="art.thumb"
|
:src="art.thumb"
|
||||||
:alt="art.title"
|
:alt="art.title"
|
||||||
@@ -59,49 +71,167 @@ const isFallback = (art) => art.path.startsWith('/art/en/') && locale.value !==
|
|||||||
height="250"
|
height="250"
|
||||||
fit="cover"
|
fit="cover"
|
||||||
/>
|
/>
|
||||||
<div class="overlay-text">{{ art.title }}</div>
|
<span class="selector-border-bottom" aria-hidden="true">────────</span>
|
||||||
|
<div class="overlay-label">{{ art.title }}</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
<p v-if="posts && posts.length > ART_LIMIT" class="show-more-toggle">
|
||||||
|
<button type="button" class="tui-link" @click="showAll = !showAll">{{ showAll ? t('pages.show_less') : t('pages.show_more') }}</button>
|
||||||
|
</p>
|
||||||
</Container>
|
</Container>
|
||||||
</FixedLayout>
|
</FixedLayout>
|
||||||
<Footer></Footer>
|
<Footer></Footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.section-title {
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin: 28px 0 16px 0;
|
||||||
|
padding-left: 12px;
|
||||||
|
line-height: 1.3;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "├";
|
||||||
|
color: var(--color-link);
|
||||||
|
margin-right: 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background-color: var(--color-link);
|
||||||
|
margin-left: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
box-shadow: 0 0 4px var(--color-link);
|
||||||
|
animation: blink-cursor 1s steps(1) infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink-cursor {
|
||||||
|
0%, 50% { opacity: 1; }
|
||||||
|
51%, 100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(250px, 1fr));
|
grid-template-columns: repeat(3, minmax(250px, 1fr));
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 40px 20px 40px 20px;
|
padding: 24px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector {
|
.selector {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 250px;
|
height: 250px;
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s ease;
|
transition: all 0.1s steps(3, end);
|
||||||
display: block; /* ensures NuxtLink behaves as a block */
|
display: block;
|
||||||
}
|
border: 2px solid var(--color-border-color);
|
||||||
.selector:hover {
|
background-color: var(--color-background-fore);
|
||||||
transform: scale(1.03);
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--color-link);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 4px -4px 0px 0px var(--color-link);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-border-top,
|
||||||
|
.selector-border-bottom {
|
||||||
|
position: absolute;
|
||||||
|
left: 30px;
|
||||||
|
right: 30px;
|
||||||
|
height: 0;
|
||||||
|
color: var(--color-border-color);
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 0;
|
||||||
|
line-height: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 10;
|
||||||
|
transition: color 0.1s steps(2, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-border-top {
|
||||||
|
top: -2px;
|
||||||
|
&::before { content: "═══════════════════"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-border-bottom {
|
||||||
|
bottom: -2px;
|
||||||
|
&::before { content: "═══════════════════"; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.selector-img {
|
.selector-img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.overlay-text {
|
|
||||||
|
.overlay-label {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 10px;
|
bottom: -2px;
|
||||||
left: 10px;
|
left: 30px;
|
||||||
color: white;
|
background-color: var(--color-link);
|
||||||
font-size: 18px;
|
color: var(--color-background-fore);
|
||||||
font-weight: bold;
|
padding: 2px 8px;
|
||||||
text-shadow: 0 2px 8px rgba(0,0,0,0.7);
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
box-shadow: inset 0 -2px 0px 0px rgba(0,0,0,0.3);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
/* ...rest unchanged */
|
|
||||||
|
.show-more-toggle {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-left: 14px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-link);
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-shadow: 0 0 4px var(--color-link);
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-shadow: 0 0 10px var(--color-link), 0 0 3px var(--color-link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(200px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.section-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: repeat(1, minmax(200px, 1fr));
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector {
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -15,7 +15,7 @@ const { data: post } = await useAsyncData(`blog-${slug}`, () =>
|
|||||||
<PageHeader></PageHeader>
|
<PageHeader></PageHeader>
|
||||||
<FixedLayout>
|
<FixedLayout>
|
||||||
<Container>
|
<Container>
|
||||||
<ContentRenderer :value="post" class="blog" />
|
<ContentRenderer v-if="post" :value="post" class="blog" />
|
||||||
</Container>
|
</Container>
|
||||||
</FixedLayout>
|
</FixedLayout>
|
||||||
<Footer></Footer>
|
<Footer></Footer>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import TableHeader from '~/components/parts/TableHeader.vue';
|
import MinimalHeader from '~/components/parts/MinimalHeader.vue';
|
||||||
import { useAsyncData } from '#app';
|
import { useAsyncData } from '#app';
|
||||||
import FixedLayout from '~/components/layouts/FixedLayout.vue';
|
import FixedLayout from '~/components/layouts/FixedLayout.vue';
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
|
const { t } = useI18n();
|
||||||
const localePath = useLocalePath()
|
const localePath = useLocalePath()
|
||||||
|
|
||||||
const {data: posts, refresh} = useAsyncData('blog-posts', async () =>
|
const {data: posts, refresh} = useAsyncData('blog-posts', async () =>
|
||||||
@@ -12,31 +14,154 @@ await queryCollection(`blog`).where('path', 'LIKE', `/blog/${locale.value}/%`).o
|
|||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
refresh();
|
refresh();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const BLOG_LIMIT = 5;
|
||||||
|
const showAll = ref(false);
|
||||||
|
|
||||||
|
const displayedPosts = computed(() => {
|
||||||
|
const allPosts = posts.value || [];
|
||||||
|
if (showAll.value || allPosts.length <= BLOG_LIMIT) return allPosts;
|
||||||
|
return allPosts.slice(0, BLOG_LIMIT);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TableHeader></TableHeader>
|
<MinimalHeader></MinimalHeader>
|
||||||
<FixedLayout>
|
<FixedLayout>
|
||||||
<Container>
|
<Container>
|
||||||
<h2>Blog</h2>
|
<section class="blog-section">
|
||||||
<ul>
|
<h2 class="section-title">BLOG ENTRIES</h2>
|
||||||
<li v-for="post in posts" :key="post.slug">
|
<ul class="tui-list">
|
||||||
<NuxtLink :to="localePath({ name: 'blog-slug', params: { slug: post.slug } })">{{ post.title }}</NuxtLink>
|
<li v-for="post in displayedPosts" :key="post.slug" class="blog-entry">
|
||||||
<span class="dash">-</span>
|
<NuxtLink class="entry-link" :to="localePath({ name: 'blog-slug', params: { slug: post.slug } })">
|
||||||
<span>{{ post.date }}</span>
|
<span class="entry-title">{{ post.title }}</span>
|
||||||
<span class="dash">-</span>
|
</NuxtLink>
|
||||||
<span>{{ post.description }}</span>
|
<span class="entry-meta">[{{ post.date }}] {{ post.description }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<p v-if="posts && posts.length > BLOG_LIMIT" class="show-more-toggle">
|
||||||
|
<button type="button" class="tui-link" @click="showAll = !showAll">{{ showAll ? t('pages.show_less') : t('pages.show_more') }}</button>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
</Container>
|
</Container>
|
||||||
</FixedLayout>
|
</FixedLayout>
|
||||||
<Footer></Footer>
|
<Footer></Footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.dash
|
.section-title {
|
||||||
{
|
font-family: 'Hurmit', monospace;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin: 28px 0 16px 0;
|
||||||
|
padding-left: 12px;
|
||||||
|
line-height: 1.3;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "├";
|
||||||
|
color: var(--color-link);
|
||||||
|
margin-right: 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background-color: var(--color-link);
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-right: 10px;
|
vertical-align: middle;
|
||||||
|
box-shadow: 0 0 4px var(--color-link);
|
||||||
|
animation: blink-cursor 1s steps(1) infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-entry {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-left: 2px solid var(--color-border-color);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
transition: all 0.1s steps(2, end);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-left-color: var(--color-link);
|
||||||
|
background-color: var(--color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--color-text);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-link);
|
||||||
|
text-shadow: 0 0 6px var(--color-link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-title {
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.1s steps(2, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-meta {
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-more-toggle {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-left: 14px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-link);
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-shadow: 0 0 4px var(--color-link);
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-shadow: 0 0 10px var(--color-link), 0 0 3px var(--color-link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink-cursor {
|
||||||
|
0%, 50% { opacity: 1; }
|
||||||
|
51%, 100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
.section-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-entry {
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -7,7 +7,9 @@ const { get, post } = api();
|
|||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
// Move useAsyncData to top level — NOT inside onMounted
|
const currentPath = computed(() => route.path);
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
const { data: markdown } = await useAsyncData(`fixed-root`, async () =>
|
const { data: markdown } = await useAsyncData(`fixed-root`, async () =>
|
||||||
await queryCollection(`fixed`).path(`/fixed/${locale.value}/root`).first()
|
await queryCollection(`fixed`).path(`/fixed/${locale.value}/root`).first()
|
||||||
,{watch: [locale]})
|
,{watch: [locale]})
|
||||||
@@ -17,6 +19,66 @@ const { data: projects } = await useAsyncData(`projects`, async () =>
|
|||||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()),
|
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()),
|
||||||
{ watch: [locale] })
|
{ watch: [locale] })
|
||||||
|
|
||||||
|
const { data: posts } = await useAsyncData('blog-posts',
|
||||||
|
() => queryCollection(`blog`).where('path', 'LIKE', `/blog/${locale.value}/%`).order('date', 'DESC').all(),
|
||||||
|
{ watch: [locale] });
|
||||||
|
|
||||||
|
const { data: contactMarkdown } = await useAsyncData(`fixed-contact`, async () =>
|
||||||
|
await queryCollection(`fixed`).path(`/fixed/${locale.value}/contact`).first()
|
||||||
|
,{watch: [locale]})
|
||||||
|
|
||||||
|
const localePath = useLocalePath();
|
||||||
|
|
||||||
|
const artPosts = useState<any[]>('art-posts', () => null as any);
|
||||||
|
if (!artPosts.value?.length) {
|
||||||
|
const currentLocale = locale.value;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
if (currentLocale === 'en') {
|
||||||
|
artPosts.value = await queryCollection('art')
|
||||||
|
.where('path', 'LIKE', `/art/en/%`)
|
||||||
|
.order('date', 'DESC')
|
||||||
|
.all();
|
||||||
|
} else {
|
||||||
|
const enPosts = await queryCollection('art')
|
||||||
|
.where('path', 'LIKE', `/art/en/%`)
|
||||||
|
.order('date', 'DESC')
|
||||||
|
.all();
|
||||||
|
const localePosts = await queryCollection('art')
|
||||||
|
.where('path', 'LIKE', `/art/${currentLocale}/%`)
|
||||||
|
.order('date', 'DESC')
|
||||||
|
.all();
|
||||||
|
const translatedSlugs = new Set(
|
||||||
|
localePosts.map((p) => p.path.replace(`/art/${currentLocale}/`, ''))
|
||||||
|
);
|
||||||
|
const enFallbacks = enPosts.filter(
|
||||||
|
(p) => !translatedSlugs.has(p.path.replace('/art/en/', ''))
|
||||||
|
);
|
||||||
|
artPosts.value = [...localePosts, ...enFallbacks].sort(
|
||||||
|
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error loading art posts:', e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isArtFallback = (art) => art.path.startsWith('/art/en/') && locale.value !== 'en';
|
||||||
|
|
||||||
|
const HOME_BLOG_LIMIT = 3;
|
||||||
|
const HOME_ART_LIMIT = 6;
|
||||||
|
|
||||||
|
const displayedBlogPosts = computed(() => {
|
||||||
|
const all = posts.value || [];
|
||||||
|
return all.slice(0, HOME_BLOG_LIMIT);
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayedArtPosts = computed(() => {
|
||||||
|
const all = artPosts.value || [];
|
||||||
|
return all.slice(0, HOME_ART_LIMIT);
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await get('/test');
|
const response = await get('/test');
|
||||||
@@ -24,17 +86,46 @@ onMounted(async () => {
|
|||||||
console.error('API Error:', error);
|
console.error('API Error:', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function scrollToTop() {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToSection(e: Event, targetId: string) {
|
||||||
|
e.preventDefault();
|
||||||
|
const el = document.querySelector(targetId);
|
||||||
|
if (el) {
|
||||||
|
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
} else {
|
||||||
|
window.location.hash = targetId.substring(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sectionTargets = {
|
||||||
|
index: '#top',
|
||||||
|
blog: '#scroll-blog',
|
||||||
|
contact: '#scroll-contact',
|
||||||
|
art: '#scroll-art'
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TableHeader></TableHeader>
|
<TableHeader></TableHeader>
|
||||||
|
|
||||||
|
<div id="top" style="scroll-margin-top: 60px;"> </div>
|
||||||
|
|
||||||
<FixedLayout>
|
<FixedLayout>
|
||||||
|
<section class="intro-section">
|
||||||
<Container>
|
<Container>
|
||||||
<ContentRenderer v-if="markdown" :value="markdown"></ContentRenderer>
|
<ContentRenderer v-if="markdown" :value="markdown"></ContentRenderer>
|
||||||
|
</Container>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="projects-section" v-if="projects && projects.length > 0">
|
<section class="projects-section" id="scroll-projects" v-if="projects && projects.length > 0">
|
||||||
|
<Container>
|
||||||
<h2 class="section-title">{{ t('pages.projects_heading') }}</h2>
|
<h2 class="section-title">{{ t('pages.projects_heading') }}</h2>
|
||||||
|
<!--
|
||||||
<div class="projects-grid">
|
<div class="projects-grid">
|
||||||
<div
|
<div
|
||||||
v-for="project in projects"
|
v-for="project in projects"
|
||||||
@@ -58,9 +149,15 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
-->
|
||||||
|
(Under construction...)
|
||||||
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="stats-section">
|
<!--
|
||||||
|
<section class="stats-section" id="scroll-stats">
|
||||||
|
<Container>
|
||||||
<h2 class="section-title">{{ t('pages.stats_heading') }}</h2>
|
<h2 class="section-title">{{ t('pages.stats_heading') }}</h2>
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
@@ -68,88 +165,215 @@ onMounted(async () => {
|
|||||||
<span class="stat-card-corner tr"></span>
|
<span class="stat-card-corner tr"></span>
|
||||||
<span class="stat-card-corner bl"></span>
|
<span class="stat-card-corner bl"></span>
|
||||||
<span class="stat-card-corner br"></span>
|
<span class="stat-card-corner br"></span>
|
||||||
|
<span class="stat-card-frame-top"></span>
|
||||||
|
<span class="stat-card-frame-bottom"></span>
|
||||||
|
<div class="stat-card-header">CODE</div>
|
||||||
|
<div class="stat-card-content">
|
||||||
<svg class="stat-pixel-art" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 3h2v2H3V3zm4 0h2v2H7V3zm4 0h2v2h-2V3zM3 7h2v2H3V7zm6 0h2v2H9V7zm4 0h2v2h-2V7zM3 11h2v2H3v-2zm4 0h2v2H7v-2zm4 0h2v2h-2v-2z"/></svg>
|
<svg class="stat-pixel-art" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 3h2v2H3V3zm4 0h2v2H7V3zm4 0h2v2h-2V3zM3 7h2v2H3V7zm6 0h2v2H9V7zm4 0h2v2h-2V7zM3 11h2v2H3v-2zm4 0h2v2H7v-2zm4 0h2v2h-2v-2z"/></svg>
|
||||||
<span class="stat-number">50K+</span>
|
<span class="stat-number">50K+</span>
|
||||||
<span class="stat-label">{{ t('pages.stat_lines_of_code') }}</span>
|
<span class="stat-label">{{ t('pages.stat_lines_of_code') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<span class="stat-card-corner tl"></span>
|
<span class="stat-card-corner tl"></span>
|
||||||
<span class="stat-card-corner tr"></span>
|
<span class="stat-card-corner tr"></span>
|
||||||
<span class="stat-card-corner bl"></span>
|
<span class="stat-card-corner bl"></span>
|
||||||
<span class="stat-card-corner br"></span>
|
<span class="stat-card-corner br"></span>
|
||||||
|
<span class="stat-card-frame-top"></span>
|
||||||
|
<span class="stat-card-frame-bottom"></span>
|
||||||
|
<div class="stat-card-header">WORK</div>
|
||||||
|
<div class="stat-card-content">
|
||||||
<svg class="stat-pixel-art" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 3h8v2H6v2h6v2H8v2h4v2H4V3z"/></svg>
|
<svg class="stat-pixel-art" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 3h8v2H6v2h6v2H8v2h4v2H4V3z"/></svg>
|
||||||
<span class="stat-number">12+</span>
|
<span class="stat-number">12+</span>
|
||||||
<span class="stat-label">{{ t('pages.stat_projects') }}</span>
|
<span class="stat-label">{{ t('pages.stat_projects') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<span class="stat-card-corner tl"></span>
|
<span class="stat-card-corner tl"></span>
|
||||||
<span class="stat-card-corner tr"></span>
|
<span class="stat-card-corner tr"></span>
|
||||||
<span class="stat-card-corner bl"></span>
|
<span class="stat-card-corner bl"></span>
|
||||||
<span class="stat-card-corner br"></span>
|
<span class="stat-card-corner br"></span>
|
||||||
|
<span class="stat-card-frame-top"></span>
|
||||||
|
<span class="stat-card-frame-bottom"></span>
|
||||||
|
<div class="stat-card-header">CAFFE</div>
|
||||||
|
<div class="stat-card-content">
|
||||||
<svg class="stat-pixel-art" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 5h8v2H4V5zm-2 2h2v1H2V7zm10 0h2v1h-2V7zM3 9h1v3h1v-1h3v1h1V9h1v3h1v-1h2v1h1v-4H3z"/></svg>
|
<svg class="stat-pixel-art" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 5h8v2H4V5zm-2 2h2v1H2V7zm10 0h2v1h-2V7zM3 9h1v3h1v-1h3v1h1V9h1v3h1v-1h2v1h1v-4H3z"/></svg>
|
||||||
<span class="stat-number">∞</span>
|
<span class="stat-number">∞</span>
|
||||||
<span class="stat-label">{{ t('pages.stat_coffee') }}</span>
|
<span class="stat-label">{{ t('pages.stat_coffee') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<span class="stat-card-corner tl"></span>
|
<span class="stat-card-corner tl"></span>
|
||||||
<span class="stat-card-corner tr"></span>
|
<span class="stat-card-corner tr"></span>
|
||||||
<span class="stat-card-corner bl"></span>
|
<span class="stat-card-corner bl"></span>
|
||||||
<span class="stat-card-corner br"></span>
|
<span class="stat-card-corner br"></span>
|
||||||
|
<span class="stat-card-frame-top"></span>
|
||||||
|
<span class="stat-card-frame-bottom"></span>
|
||||||
|
<div class="stat-card-header">PLAY</div>
|
||||||
|
<div class="stat-card-content">
|
||||||
<svg class="stat-pixel-art" viewBox="0 0 16 16" aria-hidden="true"><path d="M2 4h3v2H3v1h2v2H8v-2h2V6h-1V4h3v2h-2v3H5V6H4V4zm7 2h2v2h-2V6z"/></svg>
|
<svg class="stat-pixel-art" viewBox="0 0 16 16" aria-hidden="true"><path d="M2 4h3v2H3v1h2v2H8v-2h2V6h-1V4h3v2h-2v3H5V6H4V4zm7 2h2v2h-2V6z"/></svg>
|
||||||
<span class="stat-number">28</span>
|
<span class="stat-number">28</span>
|
||||||
<span class="stat-label">{{ t('pages.stat_board_games') }}</span>
|
<span class="stat-label">{{ t('pages.stat_board_games') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
|
||||||
<span class="stat-card-corner tl"></span>
|
|
||||||
<span class="stat-card-corner tr"></span>
|
|
||||||
<span class="stat-card-corner bl"></span>
|
|
||||||
<span class="stat-card-corner br"></span>
|
|
||||||
<svg class="stat-pixel-art" viewBox="0 0 16 16" aria-hidden="true"><path d="M5 2h6v2H5V2zm-2 4h10v1H3V6zm1 2h8v1H4V8zm2 2h4v1H6v-1zM6 2h4v12H6V2z"/></svg>
|
|
||||||
<span class="stat-number">8+</span>
|
|
||||||
<span class="stat-label">{{ t('pages.stat_years_programming') }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<span class="stat-card-corner tl"></span>
|
<span class="stat-card-corner tl"></span>
|
||||||
<span class="stat-card-corner tr"></span>
|
<span class="stat-card-corner tr"></span>
|
||||||
<span class="stat-card-corner bl"></span>
|
<span class="stat-card-corner bl"></span>
|
||||||
<span class="stat-card-corner br"></span>
|
<span class="stat-card-corner br"></span>
|
||||||
|
<span class="stat-card-frame-top"></span>
|
||||||
|
<span class="stat-card-frame-bottom"></span>
|
||||||
|
<div class="stat-card-header">TIME</div>
|
||||||
|
<div class="stat-card-content">
|
||||||
|
<svg class="stat-pixel-art" viewBox="0 0 16 16" aria-hidden="true"><path d="M5 2h6v2H5V2zm-2 4h10v1H3V6zm1 2h8v1H4V8zm2 2h4v1H6v-1zM6 2h4v12H6V2z"/></svg>
|
||||||
|
<span class="stat-number">8+</span>
|
||||||
|
<span class="stat-label">{{ t('pages.stat_years_programming') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<span class="stat-card-corner tl"></span>
|
||||||
|
<span class="stat-card-corner tr"></span>
|
||||||
|
<span class="stat-card-corner bl"></span>
|
||||||
|
<span class="stat-card-corner br"></span>
|
||||||
|
<span class="stat-card-frame-top"></span>
|
||||||
|
<span class="stat-card-frame-bottom"></span>
|
||||||
|
<div class="stat-card-header">DEPLOY</div>
|
||||||
|
<div class="stat-card-content">
|
||||||
<svg class="stat-pixel-art" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 3h8v2H9v2h2v2h-2v2h-2v2h6v2H2v-2h2v-2H2V7h2V5H2V3z"/></svg>
|
<svg class="stat-pixel-art" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 3h8v2H9v2h2v2h-2v2h-2v2h6v2H2v-2h2v-2H2V7h2V5H2V3z"/></svg>
|
||||||
<span class="stat-number">150+</span>
|
<span class="stat-number">150+</span>
|
||||||
<span class="stat-label">{{ t('pages.stat_deployments') }}</span>
|
<span class="stat-label">{{ t('pages.stat_deployments') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
|
</section>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<section class="blog-section" id="scroll-blog">
|
||||||
|
<Container>
|
||||||
|
<h2 class="section-title">{{ t('pages.blog_heading') }}</h2>
|
||||||
|
<ul class="tui-list">
|
||||||
|
<li v-for="post in displayedBlogPosts" :key="post.slug" class="blog-entry">
|
||||||
|
<a class="entry-link" :href="localePath({ name: 'blog-slug', params: { slug: post.slug } })">
|
||||||
|
<span class="entry-title">{{ post.title }}</span>
|
||||||
|
</a>
|
||||||
|
<span class="entry-meta">[{{ post.date }}] {{ post.description }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p v-if="posts && posts.length > 0" class="show-more-link">
|
||||||
|
<NuxtLink :to="localePath('/blog')" class="tui-link">{{ t('pages.show_more') }}</NuxtLink>
|
||||||
|
</p>
|
||||||
|
</Container>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="contact-section" id="scroll-contact">
|
||||||
|
<Container>
|
||||||
|
<h2 class="section-title">{{ t('pages.contact_heading') }}</h2>
|
||||||
|
<ContentRenderer v-if="contactMarkdown" :value="contactMarkdown"></ContentRenderer>
|
||||||
|
</Container>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="art-section" id="scroll-art">
|
||||||
|
<Container>
|
||||||
|
<h2 class="section-title">{{ t('pages.art_heading') }}</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<a v-for="art in displayedArtPosts" :key="art.slug"
|
||||||
|
class="selector"
|
||||||
|
:href="isArtFallback(art) ? localePath(`/art/${art.slug}`) : localePath(`/art/${art.slug}`)">
|
||||||
|
<span class="selector-border-top" aria-hidden="true">────────</span>
|
||||||
|
<img
|
||||||
|
:src="art.thumb"
|
||||||
|
:alt="art.title"
|
||||||
|
class="selector-img"
|
||||||
|
/>
|
||||||
|
<span class="selector-border-bottom" aria-hidden="true">────────</span>
|
||||||
|
<div class="overlay-label">{{ art.title }}</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p v-if="artPosts && artPosts.length > 0" class="show-more-link">
|
||||||
|
<NuxtLink :to="localePath('/art')" class="tui-link">{{ t('pages.show_more') }}</NuxtLink>
|
||||||
|
</p>
|
||||||
|
</Container>
|
||||||
|
</section>
|
||||||
</FixedLayout>
|
</FixedLayout>
|
||||||
<Footer></Footer>
|
<Footer></Footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.projects-section {
|
.intro-section {
|
||||||
margin-top: 24px;
|
margin-top: 290px;
|
||||||
|
|
||||||
|
> div:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-frame:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1200px) and (min-width: 900px) {
|
||||||
|
margin-top: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) and (min-width: 600px) {
|
||||||
|
margin-top: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
margin-top: 280px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-section,
|
||||||
|
.stats-section,
|
||||||
|
.blog-section,
|
||||||
|
.contact-section,
|
||||||
|
.art-section {
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
.tui-frame:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-family: 'Hurmit', monospace;
|
font-family: 'Hurmit', monospace;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-size: 1.3rem;
|
font-size: 1.2rem;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
margin-bottom: 16px;
|
margin: 28px 0 16px 0;
|
||||||
border-left: 3px solid var(--color-link);
|
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
|
line-height: 1.3;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "├";
|
||||||
|
color: var(--color-link);
|
||||||
|
margin-right: 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 8px;
|
width: 6px;
|
||||||
height: 8px;
|
height: 6px;
|
||||||
background-color: var(--color-link);
|
background-color: var(--color-link);
|
||||||
margin-left: 6px;
|
margin-left: 10px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
box-shadow: 0 0 6px var(--color-link);
|
box-shadow: 0 0 4px var(--color-link);
|
||||||
|
animation: blink-cursor 1s steps(1) infinite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-title:first-child {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink-cursor {
|
||||||
|
0%, 50% { opacity: 1; }
|
||||||
|
51%, 100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
.projects-grid {
|
.projects-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -162,6 +386,24 @@ onMounted(async () => {
|
|||||||
border: 1px solid var(--color-border-color);
|
border: 1px solid var(--color-border-color);
|
||||||
box-shadow: 4px -4px 0px 0px var(--color-container-shadow);
|
box-shadow: 4px -4px 0px 0px var(--color-container-shadow);
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
|
transition: all 0.1s steps(2, end);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--color-link);
|
||||||
|
box-shadow: 4px -4px 0px 0px var(--color-link);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "[ PROJECT ]";
|
||||||
|
position: absolute;
|
||||||
|
top: -9px;
|
||||||
|
left: 12px;
|
||||||
|
background-color: var(--color-background-fore);
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--color-link);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-card-corner {
|
.project-card-corner {
|
||||||
@@ -181,7 +423,7 @@ onMounted(async () => {
|
|||||||
color: var(--color-link);
|
color: var(--color-link);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
text-shadow: 0 0 6px var(--color-link);
|
text-shadow: 0 0 4px var(--color-link);
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -193,7 +435,7 @@ onMounted(async () => {
|
|||||||
.project-description {
|
.project-description {
|
||||||
font-family: 'Hurmit', monospace;
|
font-family: 'Hurmit', monospace;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
margin: 6px 0 10px 0;
|
margin: 8px 0 12px 0;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
@@ -201,16 +443,17 @@ onMounted(async () => {
|
|||||||
.project-tech {
|
.project-tech {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-tag {
|
.tech-tag {
|
||||||
font-family: 'Hurmit', monospace;
|
font-family: 'Hurmit', monospace;
|
||||||
font-size: 0.75rem;
|
font-size: 0.7rem;
|
||||||
color: var(--color-background-fore);
|
color: var(--color-background-fore);
|
||||||
background-color: var(--color-link);
|
background-color: var(--color-link);
|
||||||
padding: 2px 8px;
|
padding: 1px 6px;
|
||||||
box-shadow: 2px -2px 0px 0px rgba(0, 0, 0, 0.3);
|
box-shadow: 2px -2px 0px 0px rgba(0, 0, 0, 0.3);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-section {
|
.stats-section {
|
||||||
@@ -226,17 +469,27 @@ onMounted(async () => {
|
|||||||
.stat-card {
|
.stat-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--color-background-fore);
|
background: var(--color-background-fore);
|
||||||
border: 1px solid var(--color-border-color);
|
border: 2px solid var(--color-border-color);
|
||||||
box-shadow: 3px -3px 0px 0px var(--color-container-shadow);
|
box-shadow:
|
||||||
padding: 14px 10px;
|
inset 1px -1px 0px 0px rgba(0,0,0,0.3),
|
||||||
|
4px -4px 0px 0px var(--color-container-shadow);
|
||||||
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
gap: 6px;
|
gap: 0;
|
||||||
}
|
overflow: hidden;
|
||||||
|
transition: all 0.1s steps(2, end);
|
||||||
|
|
||||||
.stat-card-corner {
|
&:hover {
|
||||||
|
border-color: var(--color-link);
|
||||||
|
box-shadow:
|
||||||
|
inset 1px -1px 0px 0px rgba(0,0,0,0.3),
|
||||||
|
4px -4px 0px 0px var(--color-link);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-corner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
@@ -246,20 +499,78 @@ onMounted(async () => {
|
|||||||
&.tr { top: -1px; right: -1px; border-top: 2px solid; border-right: 2px solid; }
|
&.tr { top: -1px; right: -1px; border-top: 2px solid; border-right: 2px solid; }
|
||||||
&.bl { bottom: -1px; left: -1px; border-bottom: 2px solid; border-left: 2px solid; }
|
&.bl { bottom: -1px; left: -1px; border-bottom: 2px solid; border-left: 2px solid; }
|
||||||
&.br { bottom: -1px; right: -1px; border-bottom: 2px solid; border-right: 2px solid; }
|
&.br { bottom: -1px; right: -1px; border-bottom: 2px solid; border-right: 2px solid; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-frame-top {
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
left: 14px;
|
||||||
|
right: 14px;
|
||||||
|
height: 0;
|
||||||
|
color: var(--color-border-color);
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 0;
|
||||||
|
line-height: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "═════";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-frame-bottom {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2px;
|
||||||
|
left: 14px;
|
||||||
|
right: 14px;
|
||||||
|
height: 0;
|
||||||
|
color: var(--color-border-color);
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 0;
|
||||||
|
line-height: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "═════";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-header {
|
||||||
|
background-color: var(--color-link);
|
||||||
|
color: var(--color-background-fore);
|
||||||
|
padding: 3px 12px;
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 0.55rem;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: inset 0 -2px 0px 0px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-content {
|
||||||
|
padding: 16px 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-pixel-art {
|
.stat-pixel-art {
|
||||||
width: 40px;
|
width: 36px;
|
||||||
height: 40px;
|
height: 36px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 2px;
|
||||||
fill: var(--color-link);
|
fill: var(--color-link);
|
||||||
filter: drop-shadow(0 0 6px var(--color-link));
|
filter: drop-shadow(0 0 4px var(--color-link));
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-number {
|
.stat-number {
|
||||||
font-family: 'Hurmit', monospace;
|
font-family: 'Hurmit', monospace;
|
||||||
font-size: 1.8rem;
|
font-size: 1.6rem;
|
||||||
color: var(--color-link);
|
color: var(--color-link);
|
||||||
text-shadow: 0 0 8px var(--color-link), 0 0 4px var(--color-link);
|
text-shadow: 0 0 8px var(--color-link), 0 0 4px var(--color-link);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -269,16 +580,178 @@ onMounted(async () => {
|
|||||||
.stat-label {
|
.stat-label {
|
||||||
font-family: 'Hurmit', monospace;
|
font-family: 'Hurmit', monospace;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-size: 0.7rem;
|
font-size: 0.65rem;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blog section */
|
||||||
|
.blog-section {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-entry {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-left: 2px solid var(--color-border-color);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
transition: all 0.1s steps(2, end);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-left-color: var(--color-link);
|
||||||
|
background-color: var(--color-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--color-text);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-link);
|
||||||
|
text-shadow: 0 0 6px var(--color-link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-title {
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.1s steps(2, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-meta {
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-more-link {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-left: 14px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-link);
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-shadow: 0 0 4px var(--color-link);
|
||||||
|
transition: all 0.1s steps(2, end);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-shadow: 0 0 10px var(--color-link), 0 0 3px var(--color-link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contact section */
|
||||||
|
.contact-section {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Art section */
|
||||||
|
.art-section {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(250px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector {
|
||||||
|
width: 100%;
|
||||||
|
height: 250px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.1s steps(3, end);
|
||||||
|
display: block;
|
||||||
|
border: 2px solid var(--color-border-color);
|
||||||
|
background-color: var(--color-background-fore);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--color-link);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 4px -4px 0px 0px var(--color-link);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-border-top,
|
||||||
|
.selector-border-bottom {
|
||||||
|
position: absolute;
|
||||||
|
left: 30px;
|
||||||
|
right: 30px;
|
||||||
|
height: 0;
|
||||||
|
color: var(--color-border-color);
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 0;
|
||||||
|
line-height: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 10;
|
||||||
|
transition: color 0.1s steps(2, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-border-top {
|
||||||
|
top: -2px;
|
||||||
|
&::before { content: "═══════════════════"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-border-bottom {
|
||||||
|
bottom: -2px;
|
||||||
|
&::before { content: "═══════════════════"; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-label {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2px;
|
||||||
|
left: 30px;
|
||||||
|
background-color: var(--color-link);
|
||||||
|
color: var(--color-background-fore);
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-family: 'Hurmit', monospace;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
box-shadow: inset 0 -2px 0px 0px rgba(0,0,0,0.3);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink-cursor {
|
||||||
|
0%, 50% { opacity: 1; }
|
||||||
|
51%, 100% { opacity: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(200px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
@@ -303,17 +776,35 @@ onMounted(async () => {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card-header {
|
||||||
padding: 10px 6px;
|
padding: 2px 8px;
|
||||||
|
font-size: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-content {
|
||||||
|
padding: 12px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-number {
|
.stat-number {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.stat-pixel-art {
|
.stat-pixel-art {
|
||||||
width: 32px;
|
width: 28px;
|
||||||
height: 32px;
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: repeat(1, minmax(200px, 1fr));
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector {
|
||||||
|
height: 180px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -28,15 +28,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
|
"intro_heading": "INTRO",
|
||||||
|
"contact_heading": "Contacte",
|
||||||
"projects_heading": "Projectes",
|
"projects_heading": "Projectes",
|
||||||
|
"blog_heading": "Entrades de Blog",
|
||||||
|
"art_heading": "Galeria d'Art",
|
||||||
"stats_heading": "Estad\u00edstiques i Info",
|
"stats_heading": "Estad\u00edstiques i Info",
|
||||||
"stat_lines_of_code": "L\u00ednies de Codi Escrites",
|
"stat_lines_of_code": "L\u00ednies de Codi Escrites",
|
||||||
"stat_projects": "Projectes Completats",
|
"stat_projects": "Projectes Completats",
|
||||||
"stat_coffee": "Tasses de Caf\u00e8 Consumides",
|
"stat_coffee": "Tasses de Caf\u00e8 Consumides",
|
||||||
"stat_board_games": "Jocs de Taula Propis",
|
"stat_board_games": "Jocs de Taula Propis",
|
||||||
"stat_years_programming": "Anys Programant",
|
"stat_years_programming": "Anys Programant",
|
||||||
"stat_deployments": "Desplegaments a Producci\u00f3"
|
"stat_deployments": "Desplegaments a Producci\u00f3",
|
||||||
|
"show_more": "mostrar m\u00e9s...",
|
||||||
|
"show_less": "mostrar menys"
|
||||||
},
|
},
|
||||||
"prefooter": "Aquesta pàgina web ha sigut construida per un humà.",
|
"prefooter": "Compilada des de la res at las 00:00:00",
|
||||||
"footer": "(C) 2026 Aran Roig. Tots els drets no sé que."
|
"footer": "(C) 2026 Aran Roig. Tots els drets no sé que."
|
||||||
}
|
}
|
||||||
@@ -28,16 +28,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"root": "",
|
"intro_heading": "INTRO",
|
||||||
|
"contact_heading": "Contact",
|
||||||
"projects_heading": "Projects",
|
"projects_heading": "Projects",
|
||||||
|
"blog_heading": "Blog Entries",
|
||||||
|
"art_heading": "Art Gallery",
|
||||||
"stats_heading": "Stats & Info",
|
"stats_heading": "Stats & Info",
|
||||||
"stat_lines_of_code": "Lines of Code Written",
|
"stat_lines_of_code": "Lines of Code Written",
|
||||||
"stat_projects": "Projects Completed",
|
"stat_projects": "Projects Completed",
|
||||||
"stat_coffee": "Cups of Coffee Consumed",
|
"stat_coffee": "Cups of Coffee Consumed",
|
||||||
"stat_board_games": "Board Games Owned",
|
"stat_board_games": "Board Games Owned",
|
||||||
"stat_years_programming": "Years Programming",
|
"stat_years_programming": "Years Programming",
|
||||||
"stat_deployments": "Deployments to Production"
|
"stat_deployments": "Deployments to Production",
|
||||||
|
"show_more": "show more...",
|
||||||
|
"show_less": "show less"
|
||||||
},
|
},
|
||||||
"prefooter": "This webpage has been made by a human.",
|
"prefooter": "",
|
||||||
"footer": "(C) 2026 Aran Roig. All rights whatever."
|
"footer": "(C) 2026 Aran Roig. All rights whatever."
|
||||||
}
|
}
|
||||||
@@ -28,15 +28,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
|
"intro_heading": "INTRODUCCI\u00d3N",
|
||||||
|
"contact_heading": "Contacto",
|
||||||
"projects_heading": "Proyectos",
|
"projects_heading": "Proyectos",
|
||||||
|
"blog_heading": "Entradas de Blog",
|
||||||
|
"art_heading": "Galer\u00eda de Arte",
|
||||||
"stats_heading": "Estad\u00edsticas e Info",
|
"stats_heading": "Estad\u00edsticas e Info",
|
||||||
"stat_lines_of_code": "L\u00edneas de C\u00f3digo Escritas",
|
"stat_lines_of_code": "L\u00edneas de C\u00f3digo Escritas",
|
||||||
"stat_projects": "Proyectos Completados",
|
"stat_projects": "Proyectos Completados",
|
||||||
"stat_coffee": "Tazas de Caf\u00e9 Consumidas",
|
"stat_coffee": "Tazas de Caf\u00e9 Consumidas",
|
||||||
"stat_board_games": "Juegos de Mesa Propios",
|
"stat_board_games": "Juegos de Mesa Propios",
|
||||||
"stat_years_programming": "A\u00f1os Programando",
|
"stat_years_programming": "A\u00f1os Programando",
|
||||||
"stat_deployments": "Desplegues a Producci\u00f3n"
|
"stat_deployments": "Desplegues a Producci\u00f3n",
|
||||||
|
"show_more": "mostrar m\u00e1s...",
|
||||||
|
"show_less": "mostrar menos"
|
||||||
},
|
},
|
||||||
"prefooter": "Esta página web ha sido construida por un humano.",
|
"prefooter": "Compilada desde la nada a las 00:00:00",
|
||||||
"footer": "(C) 2026 Aran Roig. Todos los derechos no se que."
|
"footer": "(C) 2026 Aran Roig. Todos los derechos no se que."
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user