Compare commits

..

54 commits
0.3.4 ... main

Author SHA1 Message Date
Sebin Nyshkim
2830fd9350 fix: get favicon from absolute path 2023-09-06 00:20:22 +02:00
Sebin Nyshkim
986126a2bb fix: disable legacy mode of vue-i18n for use with composition API 2023-09-06 00:19:17 +02:00
Sebin Nyshkim
3d13c256e3 style: close component tag immediately 2023-09-06 00:17:56 +02:00
Sebin Nyshkim
f67ba1e655 refactor: get routes via getRoutes() function instead of importing router config 2023-09-06 00:17:27 +02:00
Sebin Nyshkim
1553825719 refactor: add type definition for emits 2023-09-05 22:49:23 +02:00
Sebin Nyshkim
f1961f5cef chore: bump version number 2023-09-04 22:18:12 +02:00
Sebin Nyshkim
a67cddc74b build: update packages 2023-09-04 22:17:47 +02:00
Sebin Nyshkim
68a43b6b9c feat: add transitions between pages 2023-09-04 22:17:24 +02:00
Sebin Nyshkim
f2b408fce5 refactor: type MessageSchema in createI18n() constructor 2023-09-04 20:19:26 +02:00
Sebin Nyshkim
35f5343149 style: run formatter 2023-09-04 20:06:56 +02:00
Sebin Nyshkim
b5eae39f89 build: include json files under src/translations/ 2023-09-04 19:53:15 +02:00
Sebin Nyshkim
39993112b7 refactor: update asset URLs for vite-imagetools v5
Shorthand parameters have been removed as of vite-imagetools v5
2023-09-04 19:52:19 +02:00
Sebin Nyshkim
d4dc47b1e1 build: update to new create-vue scaffold 2023-09-04 19:19:43 +02:00
Sebin Nyshkim
25ee71b615 chore: bump version number 2023-07-17 15:46:32 +02:00
Sebin Nyshkim
23a24c5842 feat: add interface for translation key iteration 2023-07-17 15:39:34 +02:00
Sebin Nyshkim
29f549ddd9 build: update npm packages 2023-07-17 15:37:46 +02:00
Sebin Nyshkim
223a32580c fix: wrong open graph meta tag attributes 2023-07-17 14:53:07 +02:00
Sebin Nyshkim
3723e4b036 build: update npm packages 2023-04-03 00:33:48 +02:00
Sebin Nyshkim
b57c5a9be8 style: run linter 2023-04-03 00:33:35 +02:00
Sebin Nyshkim
bb724f581b build: inline postcss config into vite config 2023-04-03 00:29:07 +02:00
Sebin Nyshkim
44d974a870 feat: update prettier config 2023-04-03 00:28:37 +02:00
Sebin Nyshkim
3c83d66bf5 fix: add colored background to language switcher for better visibility 2023-03-28 20:17:26 +02:00
Sebin Nyshkim
79f1191e36 chore: bump version number 2023-03-28 00:03:41 +02:00
Sebin Nyshkim
8f4c1fc7d9 fix: modal does not scroll with current position on page 2023-03-28 00:03:06 +02:00
Sebin Nyshkim
cf4155784c chore: bump version number 2023-03-27 23:51:47 +02:00
Sebin Nyshkim
0b952dd789 build: update npm packages 2023-03-27 23:51:19 +02:00
Sebin Nyshkim
da0d919267 feat: add language switcher modal to top level of page 2023-03-27 23:49:39 +02:00
Sebin Nyshkim
d7e3ae07b5 feat: add custom properties for modal dialogs 2023-03-27 23:48:34 +02:00
Sebin Nyshkim
7fc87ea224 feat: add translations for language switching 2023-03-27 23:48:10 +02:00
Sebin Nyshkim
f5117882f8 refactor: change LocaleSwitcher to use radio buttons instead of select 2023-03-27 23:47:40 +02:00
Sebin Nyshkim
7da03fc635 feat: add ModalDialog component 2023-03-27 23:46:17 +02:00
Sebin Nyshkim
2382405651 feat: add LinkButton component 2023-03-27 23:45:51 +02:00
Sebin Nyshkim
20c7a512ed feat: add LanguageButton component 2023-03-27 23:44:54 +02:00
Sebin Nyshkim
b3a288e281 style: order imports 2023-03-27 23:40:53 +02:00
Sebin Nyshkim
21ed580988 feat: remove prefixed properties
Now handled via autoprefixer
2023-03-27 23:35:33 +02:00
Sebin Nyshkim
dbb317e19a feat: use same background color as bottom most background svg element 2023-03-27 23:34:38 +02:00
Sebin Nyshkim
87dd1649b9 build: add autoprefixer 2023-03-27 23:32:06 +02:00
Sebin Nyshkim
53c2987dcf chore: remove unused icons 2023-03-27 23:29:58 +02:00
Sebin Nyshkim
d826d278a5 feat: add icons for language switching 2023-03-27 23:29:32 +02:00
Sebin Nyshkim
b87e84e5ce feat: add globe icon 2023-03-26 21:17:57 +02:00
Sebin Nyshkim
d72c3efeeb feat: add v-model capabilities to LocaleSwitcher component 2023-03-26 21:01:13 +02:00
Sebin Nyshkim
9757a3d076 feat: add LocaleSwitcher component 2023-03-25 02:10:17 +01:00
Sebin Nyshkim
eefc4cc8d4 feat: replace hard-coded content with i18n translation keys 2023-03-25 02:09:39 +01:00
Sebin Nyshkim
7fe9cdeef3 refactor: split up navigation components for easier i18n 2023-03-25 02:05:30 +01:00
Sebin Nyshkim
9536a6fa77 feat: add translations 2023-03-25 02:02:37 +01:00
Sebin Nyshkim
8ea083c6d1 feat: set up i18n 2023-03-25 02:02:20 +01:00
Sebin Nyshkim
c258de84a2 feat: add i18n support 2023-03-25 02:01:36 +01:00
Sebin Nyshkim
8e04a3430a feat: enable hyphenation for paragraphs 2023-03-25 01:57:14 +01:00
Sebin Nyshkim
fb9cfd0783 build: update npm packages 2023-03-24 19:23:47 +01:00
Sebin Nyshkim
65bd7bde60 chore: bump version number 2023-03-23 15:57:16 +01:00
Sebin Nyshkim
69182778c9 refactor: use shorthand property for sticky positioning of background svg 2023-03-23 13:55:10 +01:00
Sebin Nyshkim
1707b6f4af fix: navigation unintentionally resizing on hover on mobile viewport sizes 2023-03-23 13:43:17 +01:00
Sebin Nyshkim
df3014e2a5 refactor: use shorthand property for navigation positioning 2023-03-23 13:41:08 +01:00
Sebin Nyshkim
f7ae855221 refactor: adjust timeline sizings for mobile, cleanup component styling 2023-03-23 13:33:49 +01:00
44 changed files with 2872 additions and 4686 deletions

2
.browserslistrc Normal file
View file

@ -0,0 +1,2 @@
last 2 versions
not dead

View file

@ -1,15 +1,15 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution");
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/eslint-config-typescript",
"@vue/eslint-config-prettier",
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: "latest",
},
};
ecmaVersion: 'latest'
}
}

View file

@ -1 +1,8 @@
{}
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}

View file

@ -2,8 +2,8 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="favicon" href="favicon.png" type="image/png" />
<link rel="icon" href="favicon.png" type="image/png" />
<link rel="favicon" href="/favicon.png" type="image/png" />
<link rel="icon" href="/favicon.png" type="image/png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Viktor Kraastav Reference Page</title>
@ -17,11 +17,13 @@
<meta name="twitter:description" content="The official reference page for Viktor Kraastav with picture references and in-depth character descriptions" />
<meta name="twitter:image" content="https://ref.sebin-nyshkim.net/viktor/preview.png" />
<meta name="og:type" content="website" />
<meta name="og:title" content="Viktor Kraastav - Reference Page" />
<meta name="og:url" content="https://ref.sebin-nyshkim.net/viktor/" />
<meta name="og:image" content="https://ref.sebin-nyshkim.net/viktor/preview.png" />
<meta name="og:description" content="The official reference page for Viktor Kraastav with picture references and in-depth character descriptions" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Viktor Kraastav - Reference Page" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="de_DE" />
<meta property="og:url" content="https://ref.sebin-nyshkim.net/viktor/" />
<meta property="og:image" content="https://ref.sebin-nyshkim.net/viktor/preview.png" />
<meta property="og:description" content="The official reference page for Viktor Kraastav with picture references and in-depth character descriptions" />
</head>
<body>
<script type="module" src="/src/main.ts"></script>

5326
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,35 +1,38 @@
{
"name": "viktor-reference",
"version": "0.3.4",
"version": "0.4.3",
"scripts": {
"dev": "vite --host",
"build": "run-p type-check build-only",
"preview": "vite preview --port 4173",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"vue": "^3.2.45",
"vue-router": "^4.1.6"
"vue": "^3.3.4",
"vue-i18n": "^9.2.2",
"vue-router": "^4.2.4"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.2.0",
"@types/node": "^18.15.5",
"@types/vue-markdown": "^2.2.1",
"@vitejs/plugin-vue": "^4.1.0",
"@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^11.0.2",
"@vue/tsconfig": "^0.1.3",
"eslint": "^8.36.0",
"eslint-plugin-vue": "^9.9.0",
"@rushstack/eslint-patch": "^1.3.3",
"@tsconfig/node18": "^18.2.1",
"@types/node": "^20.5.9",
"@vitejs/plugin-vue": "^4.3.4",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/tsconfig": "^0.4.0",
"autoprefixer": "^10.4.15",
"eslint": "^8.48.0",
"eslint-plugin-vue": "^9.17.0",
"normalize.css": "^8.0.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.6",
"sass": "^1.59.3",
"typescript": "^4.9.5",
"vite": "^4.2.1",
"vite-imagetools": "^4.0.18",
"vue-tsc": "^1.2.0"
"prettier": "^3.0.3",
"sass": "^1.66.1",
"typescript": "^5.2.2",
"vite": "^4.4.9",
"vite-imagetools": "^5.0.8",
"vue-tsc": "^1.8.8"
}
}

View file

@ -1,18 +1,69 @@
<script setup lang="ts">
import { RouterView } from "vue-router";
import SiteNavigation from "@/components/SiteNavigation.vue";
import SiteFooter from "@/components/SiteFooter.vue";
import { version } from "../package.json";
import { ref } from 'vue'
import { RouterView } from 'vue-router'
import { version } from '../package.json'
import ModalDialog from '@/components/ModalDialog.vue'
import LocaleSwitcher from '@/components/LocaleSwitcher.vue'
import LinkButton from '@/components/LinkButton.vue'
import SiteNavigation from '@/components/SiteNavigation.vue'
import NavigationItem from '@/components/NavigationItem.vue'
import LanguageButton from '@/components/LanguageButton.vue'
import SiteFooter from '@/components/SiteFooter.vue'
import LanguageIcon from '@/assets/icons/LanguageIcon.vue'
const locales = [
{ code: 'en', name: 'English', flag: '🇬🇧' },
{ code: 'de', name: 'Deutsch', flag: '🇩🇪' }
]
const langswitcher = ref<InstanceType<typeof ModalDialog>>()
const showModal = () => {
langswitcher.value?.showModal()
}
const close = () => {
langswitcher.value?.close()
}
</script>
<template>
<SiteNavigation />
<ModalDialog id="lang-select" ref="langswitcher">
<template #heading>{{ $t('langswitcher.title') }}</template>
<template #message>
<p>{{ $t('langswitcher.prompt') }}</p>
<LocaleSwitcher id="locale-switch" v-model="$i18n.locale" :locales="locales" />
</template>
<template #buttons>
<LinkButton @click.prevent="close()">
{{ $t('langswitcher.buttonClose') }}
</LinkButton>
</template>
</ModalDialog>
<SiteNavigation>
<NavigationItem
v-for="(route, idx) in $router.getRoutes()"
:key="idx"
:icon="route.meta?.icon"
:href="route.path"
>
{{ $t(`${route.meta?.title}`) }}
</NavigationItem>
</SiteNavigation>
<main>
<RouterView />
<LanguageButton>
<LanguageIcon @click.prevent="showModal()" />
</LanguageButton>
<RouterView v-slot="{ Component }">
<Transition name="fade" mode="out-in">
<component :is="Component" :key="$route.path" />
</Transition>
</RouterView>
</main>
<SiteFooter>
v{{ version }} &copy; {{ new Date().getFullYear() }} Sebin Nyshkim
</SiteFooter>
<SiteFooter>v{{ version }} &copy; {{ new Date().getFullYear() }} Sebin Nyshkim</SiteFooter>
</template>

View file

@ -1,3 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg>
</template>

View file

@ -0,0 +1,3 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-111 111-47-47c-9.4-9.4-24.6-9.4-33.9 0s-9.4 24.6 0 33.9l64 64c9.4 9.4 24.6 9.4 33.9 0L369 209z"/></svg>
</template>

View file

@ -0,0 +1,3 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M464 256A208 208 0 1 0 48 256a208 208 0 1 0 416 0zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z"/></svg>
</template>

View file

@ -0,0 +1,3 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M0 128C0 92.7 28.7 64 64 64H256h48 16H576c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H320 304 256 64c-35.3 0-64-28.7-64-64V128zm320 0V384H576V128H320zM178.3 175.9c-3.2-7.2-10.4-11.9-18.3-11.9s-15.1 4.7-18.3 11.9l-64 144c-4.5 10.1 .1 21.9 10.2 26.4s21.9-.1 26.4-10.2l8.9-20.1h73.6l8.9 20.1c4.5 10.1 16.3 14.6 26.4 10.2s14.6-16.3 10.2-26.4l-64-144zM160 233.2L179 276H141l19-42.8zM448 164c11 0 20 9 20 20v4h44 16c11 0 20 9 20 20s-9 20-20 20h-2l-1.6 4.5c-8.9 24.4-22.4 46.6-39.6 65.4c.9 .6 1.8 1.1 2.7 1.6l18.9 11.3c9.5 5.7 12.5 18 6.9 27.4s-18 12.5-27.4 6.9l-18.9-11.3c-4.5-2.7-8.8-5.5-13.1-8.5c-10.6 7.5-21.9 14-34 19.4l-3.6 1.6c-10.1 4.5-21.9-.1-26.4-10.2s.1-21.9 10.2-26.4l3.6-1.6c6.4-2.9 12.6-6.1 18.5-9.8l-12.2-12.2c-7.8-7.8-7.8-20.5 0-28.3s20.5-7.8 28.3 0l14.6 14.6 .5 .5c12.4-13.1 22.5-28.3 29.8-45H448 376c-11 0-20-9-20-20s9-20 20-20h52v-4c0-11 9-20 20-20z"/></svg>
</template>

View file

@ -1,3 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M226.5 92.9c14.3 42.9-.3 86.2-32.6 96.8s-70.1-15.6-84.4-58.5s.3-86.2 32.6-96.8s70.1 15.6 84.4 58.5zM100.4 198.6c18.9 32.4 14.3 70.1-10.2 84.1s-59.7-.9-78.5-33.3S-2.7 179.3 21.8 165.3s59.7 .9 78.5 33.3zM69.2 401.2C121.6 259.9 214.7 224 256 224s134.4 35.9 186.8 177.2c3.6 9.7 5.2 20.1 5.2 30.5v1.6c0 25.8-20.9 46.7-46.7 46.7c-11.5 0-22.9-1.4-34-4.2l-88-22c-15.3-3.8-31.3-3.8-46.6 0l-88 22c-11.1 2.8-22.5 4.2-34 4.2C84.9 480 64 459.1 64 433.3v-1.6c0-10.4 1.6-20.8 5.2-30.5zM421.8 282.7c-24.5-14-29.1-51.7-10.2-84.1s54-47.3 78.5-33.3s29.1 51.7 10.2 84.1s-54 47.3-78.5 33.3zM310.1 189.7c-32.3-10.6-46.9-53.9-32.6-96.8s52.1-69.1 84.4-58.5s46.9 53.9 32.6 96.8s-52.1 69.1-84.4 58.5z"/></svg>
</template>

View file

@ -1,39 +1,39 @@
<script setup lang="ts">
import FAIcon from "@/assets/icons/FurAffinityIcon.vue";
import TwitterIcon from "@/assets/icons/TwitterIcon.vue";
import FAIcon from '@/assets/icons/FurAffinityIcon.vue'
import TwitterIcon from '@/assets/icons/TwitterIcon.vue'
interface ArtistLink {
furaffinity?: string;
twitter?: string;
furaffinity?: string
twitter?: string
}
interface Attribution {
artwork: string;
artist: string;
links: ArtistLink;
artwork: string
artist: string
links: ArtistLink
}
interface Props {
attributions: Attribution[];
attributions: Attribution[]
}
defineProps<Props>();
defineProps<Props>()
</script>
<template>
<table class="attribution-table">
<thead class="attribution-table__head">
<tr class="attribution-table__row">
<th class="attribution-table__heading artwork">Artwork</th>
<th class="attribution-table__heading artist">Artist</th>
<th class="attribution-table__heading artwork">
{{ $t('attributions.artwork.headings[0]') }}
</th>
<th class="attribution-table__heading artist">
{{ $t('attributions.artwork.headings[1]') }}
</th>
</tr>
</thead>
<tbody class="attribution-table__body">
<tr
class="attribution-table__row"
v-for="(attrib, idx) in attributions"
:key="idx"
>
<tr class="attribution-table__row" v-for="(attrib, idx) in attributions" :key="idx">
<td class="attribution-table__cell artwork">
<img :src="attrib.artwork" alt="Image attribution" />
</td>

View file

@ -1,33 +1,30 @@
<script setup lang="ts">
interface ColorDict {
name: string;
value: string;
name: string
value: string
}
interface Props {
colors: ColorDict[];
colors: ColorDict[]
}
defineProps<Props>();
defineProps<Props>()
</script>
<template>
<table class="color-table">
<thead class="color-table__head">
<tr class="color-table__row">
<th class="color-table__heading name">Body part</th>
<th class="color-table__heading value">Value</th>
<th class="color-table__heading color">Color</th>
<th class="color-table__heading name">{{ $t('data.colors.headings[0]') }}</th>
<th class="color-table__heading value">{{ $t('data.colors.headings[1]') }}</th>
<th class="color-table__heading color">{{ $t('data.colors.headings[2]') }}</th>
</tr>
</thead>
<tbody class="color-table__body">
<tr class="color-table__row" v-for="(color, idx) in colors" :key="idx">
<td class="color-table__cell name">{{ color.name }}</td>
<td class="color-table__cell name">{{ $t(color.name) }}</td>
<td class="color-table__cell value">{{ color.value }}</td>
<td
class="color-table__cell color"
:style="{ 'background-color': color.value }"
></td>
<td class="color-table__cell color" :style="{ 'background-color': color.value }"></td>
</tr>
</tbody>
</table>
@ -47,8 +44,16 @@ defineProps<Props>();
}
&.value {
font-family: Menlo, JetBrains Mono, Source Code Pro, Monaco, Ubuntu Mono,
Roboto Mono, Cascadia Code, Consolas, monospace;
font-family:
Menlo,
JetBrains Mono,
Source Code Pro,
Monaco,
Ubuntu Mono,
Roboto Mono,
Cascadia Code,
Consolas,
monospace;
text-align: center;
}
}

View file

@ -1,29 +1,25 @@
<script setup lang="ts">
interface Props {
headings: string[];
data: string[][];
headings: string[]
data: string[][]
}
defineProps<Props>();
defineProps<Props>()
</script>
<template>
<table class="data-table">
<thead class="data-table__head">
<tr class="data-table__row">
<th
class="data-table__heading"
v-for="(heading, idx) in headings"
:key="idx"
>
{{ heading }}
<th class="data-table__heading" v-for="(heading, idx) in headings" :key="idx">
{{ $t(heading) }}
</th>
</tr>
</thead>
<tbody class="data-table__body">
<tr class="data-table__row" v-for="(row, idx) in data" :key="idx">
<td class="data-table__cell" v-for="(cell, idx) in row" :key="idx">
{{ cell }}
{{ $t(cell) }}
</td>
</tr>
</tbody>

View file

@ -0,0 +1,27 @@
<template>
<div class="lang-button">
<slot></slot>
</div>
</template>
<style lang="scss">
.lang-button {
position: fixed;
inset: 0.25rem 0.5rem auto auto;
min-width: 3rem;
z-index: 9001;
cursor: pointer;
&:before {
position: absolute;
inset: 0.325rem 0 0.7rem 0;
content: '';
background-color: var(--color-background);
border-radius: 0.3rem;
}
svg {
fill: var(--color-text);
}
}
</style>

View file

@ -0,0 +1,49 @@
<script setup lang="ts">
interface Props {
href?: string
download?: boolean | any
}
defineProps<Props>()
</script>
<template>
<a class="link-button" :href="href" :[download]="download">
<slot></slot>
</a>
</template>
<style lang="scss">
.link-button {
display: block;
position: relative;
top: 0;
background-color: var(--color-button);
color: var(--color-button-text);
font-weight: 700;
text-decoration: none;
text-align: center;
margin: 0.5rem 0;
border-radius: 0.25rem;
padding: 0.5rem 1rem;
box-shadow: 0 0.5rem 0 0 var(--color-button-box-shadow);
transition: all 0.1s ease-out;
&:hover {
cursor: pointer;
top: -0.25rem;
box-shadow: 0 0.75rem 0 0 var(--color-button-box-shadow);
}
&:active {
top: 0.25rem;
box-shadow: 0 0.25rem 0 0 var(--color-button-box-shadow);
}
}
</style>

View file

@ -0,0 +1,93 @@
<script setup lang="ts">
import { computed } from 'vue'
import CircleCheckIcon from '@/assets/icons/CircleCheckIcon.vue'
import CircleIcon from '@/assets/icons/CircleIcon.vue'
interface LocaleOption {
code: string
name: string
flag: string
}
interface Props {
modelValue: string
id: string
locales: LocaleOption[]
}
interface Emits {
(e: 'update:modelValue', value: string): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const selectModel = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
</script>
<template>
<div class="localeselect">
<div v-for="locale in locales" class="localeselect__locale" :key="`locale-${locale.code}`">
<input
type="radio"
name="lang"
class="localeselect__input"
:id="`lang-${locale.code}`"
:value="locale.code"
v-model="selectModel"
/>
<label class="localeselect__label" :for="`lang-${locale.code}`">
<CircleCheckIcon v-if="$i18n.locale === locale.code" />
<CircleIcon v-else />
<span>{{ locale.name }} {{ locale.flag }}</span>
</label>
</div>
</div>
</template>
<style lang="scss">
.localeselect {
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: stretch;
margin: var(--paragraph-margin) 0;
&__locale {
display: flex;
flex-flow: row nowrap;
justify-content: flex-start;
align-items: center;
}
&__input {
display: none;
}
&__label {
display: flex;
flex-flow: row nowrap;
justify-content: flex-start;
align-items: center;
svg {
flex: 1 0 1.25rem;
fill: var(--color-text);
transition: 0.4s;
}
span {
flex: 1 0 auto;
margin: 0 0 0 0.5rem;
}
}
}
</style>

View file

@ -0,0 +1,95 @@
<script setup lang="ts">
import { ref } from 'vue'
interface Props {
id: string
}
defineProps<Props>()
const modal = ref<HTMLDialogElement>()
const showModal = () => {
modal.value?.showModal()
document.body.inert = true
document.body.classList.add('scroll-lock')
}
const close = () => {
modal.value?.close()
document.body.inert = false
document.body.classList.remove('scroll-lock')
}
defineExpose({ showModal, close })
</script>
<template>
<dialog :id="id" class="modal" ref="modal">
<form method="dialog" class="modal__content">
<h2 class="modal__heading">
<slot name="heading"></slot>
</h2>
<div class="modal__message">
<slot name="message"></slot>
</div>
<div class="modal__buttons">
<slot name="buttons"></slot>
</div>
</form>
</dialog>
</template>
<style lang="scss">
.modal {
position: fixed;
background: var(--color-modal-background);
color: var(--color-text);
width: var(--modal-width);
margin: auto;
border: var(--modal-border);
border-radius: 1rem;
padding: 1rem;
animation: fade-in 1s;
&::backdrop {
backdrop-filter: blur(1rem);
animation: fade-in 1s;
}
&__content {
display: flex;
flex-flow: column nowrap;
justify-content: center;
text-align: center;
> * {
flex: 1 1 auto;
}
}
&__message {
flex: 1 1 100%;
}
&__buttons .link-button {
width: 100%;
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
}
</style>

View file

@ -0,0 +1,86 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router'
interface Props {
icon: unknown | object
href: string
}
defineProps<Props>()
</script>
<template>
<RouterLink class="navigation__link" :to="href">
<component class="navigation__link-icon" :is="icon" />
<span class="navigation__link-text">
<slot></slot>
</span>
</RouterLink>
</template>
<style lang="scss">
.navigation {
&__link {
flex: var(--navigation-link-flex);
display: flex;
flex-flow: row nowrap;
justify-content: var(--navigation-link-justify);
align-items: center;
height: var(--navigation-link-height);
padding: var(--navigation-link-padding);
color: var(--color-router-link);
box-shadow: none;
&:hover {
box-shadow: var(--navigation-link-box-shadow);
svg {
fill: var(--color-router-link-hover);
}
}
&:hover &-text {
color: var(--color-router-link-hover);
}
&:last-child {
margin: var(--navigation-link-last-child-margin);
}
> * {
flex: 1 0 0;
}
&.router-link-exact-active {
color: var(--color-router-link-hover);
background-color: var(--color-router-link);
&:hover {
color: var(--color-router-link-hover);
}
svg {
fill: var(--color-router-link-hover);
}
}
}
&__link-icon {
width: var(--navigation-link-icon-size);
min-width: var(--navigation-link-icon-size);
max-width: var(--navigation-link-icon-size);
margin: 0 var(--navigation-link-icon-spacing);
fill: var(--color-router-link);
}
&__link-text {
display: var(--navigation-link-display);
font-size: var(--navigation-link-text-font-size);
margin: 0 0 0 1rem;
white-space: nowrap;
}
}
</style>

View file

@ -1,17 +1,14 @@
<script setup lang="ts">
interface Props {
dropshadow?: boolean;
dropshadow?: boolean
}
defineProps<Props>();
defineProps<Props>()
</script>
<template>
<figure class="figure">
<picture
class="figure__image"
:class="{ 'figure__image--dropshadow': dropshadow }"
>
<picture class="figure__image" :class="{ 'figure__image--dropshadow': dropshadow }">
<slot></slot>
</picture>

View file

@ -1,25 +1,7 @@
<script setup lang="ts">
import router from "@/router";
import { RouterLink } from "vue-router";
</script>
<template>
<nav class="navigation">
<div class="navigation__list">
<RouterLink
class="navigation__link"
v-for="(route, idx) in router.options.routes"
:key="idx"
:to="route.path"
>
<component
class="navigation__link-icon"
:is="route.meta?.icon"
></component>
<span class="navigation__link-text">
{{ route.meta?.title }}
</span>
</RouterLink>
<slot></slot>
</div>
</nav>
</template>
@ -27,10 +9,7 @@ import { RouterLink } from "vue-router";
<style lang="scss">
.navigation {
position: fixed;
top: var(--navigation-position-top);
right: var(--navigation-position-right);
bottom: var(--navigation-position-bottom);
left: var(--navigation-position-left);
inset: var(--navigation-position);
overflow: hidden;
width: var(--navigation-width);
@ -54,68 +33,5 @@ import { RouterLink } from "vue-router";
width: 100%;
height: 100%;
}
&__link {
flex: var(--navigation-link-flex);
display: flex;
flex-flow: row nowrap;
justify-content: var(--navigation-link-justify);
align-items: center;
height: var(--navigation-link-height);
padding: var(--navigation-link-padding);
color: var(--color-router-link);
box-shadow: none;
&:hover {
box-shadow: var(--navigation-link-box-shadow);
svg {
fill: var(--color-router-link-hover);
}
}
&:hover &-text {
color: var(--color-router-link-hover);
}
&:last-child {
margin: var(--navigation-link-last-child-margin);
}
> * {
flex: 1 0 0;
}
&.router-link-exact-active {
color: var(--color-router-link-hover);
background-color: var(--color-router-link);
&:hover {
color: var(--color-router-link-hover);
}
svg {
fill: var(--color-router-link-hover);
}
}
}
&__link-icon {
width: var(--navigation-link-icon-size);
min-width: var(--navigation-link-icon-size);
max-width: var(--navigation-link-icon-size);
margin: 0 var(--navigation-link-icon-spacing);
fill: var(--color-router-link);
}
&__link-text {
display: var(--navigation-link-display);
font-size: var(--navigation-link-text-font-size);
margin: 0 0 0 1rem;
white-space: nowrap;
}
}
</style>

View file

@ -26,36 +26,22 @@
position: relative;
&:not(:last-child):before {
content: "";
content: '';
position: absolute;
top: var(--timeline-stroke-position-top);
left: var(--timeline-stroke-position-horizontal);
inset: var(--timeline-stroke-position-odd);
height: var(--timeline-stroke-length);
border-left: var(--timeline-stroke-thickness) solid
var(--timeline-stroke-color);
border-left: var(--timeline-stroke-thickness) solid var(--timeline-stroke-color);
}
@media (min-width: 64em) {
&:nth-child(odd) {
margin: var(--timeline-item-margin-odd);
}
&:nth-child(odd) {
margin: var(--timeline-item-margin-odd);
}
&:nth-child(even) {
margin: var(--timeline-item-margin-even);
&:nth-child(even) {
margin: var(--timeline-item-margin-even);
&:before {
left: auto;
right: var(--timeline-stroke-position-horizontal);
}
.timeline-item__icon {
order: 1;
}
.timeline-item__content {
text-align: right;
margin: 0 1rem 0 0;
}
&:before {
inset: var(--timeline-stroke-position-even);
}
}
@ -71,8 +57,7 @@
background-color: var(--timeline-circle-background);
margin: 0;
border: var(--timeline-stroke-thickness) solid
var(--timeline-stroke-color);
border: var(--timeline-stroke-thickness) solid var(--timeline-stroke-color);
border-radius: 100%;
padding: var(--timeline-circle-padding);
@ -82,9 +67,19 @@
}
}
&:nth-child(even) &__icon {
order: var(--timeline-item-flex-order);
}
&__content {
flex: 1 0 0;
margin: 0 0 0 1rem;
margin: 0;
padding: var(--timeline-item-content-padding-odd);
}
&:nth-child(even) &__content {
text-align: var(--timeline-item-text-align-even);
padding: var(--timeline-item-content-padding-even);
}
&__headline {

View file

@ -6,12 +6,11 @@
<style lang="scss">
.timeline {
max-width: calc(var(--timeline-circle-size) * 15);
max-width: var(--timeline-max-width);
position: relative;
margin: 0 auto;
padding: 0 var(--container-spacing-right-safe) 0
var(--container-spacing-left-safe);
padding: 0 var(--container-spacing-right-safe) 0 var(--container-spacing-left-safe);
list-style: none;

View file

@ -2,13 +2,9 @@
<header class="welcome">
<div class="welcome__image">
<picture>
<source
srcset="@/assets/viktor-avatar.png?w=400;800&avif&quality=75&srcset"
/>
<source
srcset="@/assets/viktor-avatar.png?w=400;800&webp&quality=100&srcset"
/>
<img src="@/assets/viktor-avatar.png?w=400&png" alt="Viktor Avatar" />
<source srcset="@/assets/viktor-avatar.png?w=400;800&format=avif&quality=75&as=srcset" />
<source srcset="@/assets/viktor-avatar.png?w=400;800&format=webp&quality=100&as=srcset" />
<img src="@/assets/viktor-avatar.png?w=400&format=png" alt="Viktor Avatar" />
</picture>
</div>

7
src/lang/index.ts Normal file
View file

@ -0,0 +1,7 @@
import en from './translations/en.json'
import de from './translations/de.json'
export default {
en,
de
}

View file

@ -0,0 +1,138 @@
{
"welcomeHeader": {
"mainTitle": "Viktor Kraastav",
"subTitle": "Charakter-Referenzseite"
},
"nav": {
"home": "Startseite",
"general": "Allgemein",
"anatomy": "Anatomie",
"careerPath": "Lebenslauf",
"attributions": "Zuteilung"
},
"langswitcher": {
"title": "Sprache",
"prompt": "Diese Seite anzeigen in:",
"buttonClose": "Schließen"
},
"data": {
"general": {
"heading": ["Schlüssel", "Wert"],
"fullName": ["Vollständiger Name", "Viktor Kraastav"],
"dob": ["Geboren am"],
"gender": ["Geschlecht", "männlich ♂️"],
"height": ["Größe"],
"weight": ["Gewicht"]
},
"sexuality": {
"heading": ["Schlüssel", "Wert"],
"identifiesAs": ["Identifiziert sich als", "Schwul"],
"preferredRole": ["Bevorzugte Rolle", "Passiv"]
},
"colors": {
"headings": ["Körperteil", "Wert", "Farbe"],
"front": "Front",
"limbs": "Arme, Beine",
"back": "Rücken",
"spine": "Wirbelsäule, Schweif",
"tissue": "Frecklinge, Gewebe",
"spikes": "Stacheln, Keule",
"eyesPrimary": "Augen (Hauptfarbe)",
"eyesSecondary": "Augen (Highlights)"
}
},
"home": {
"heading": "Willkommen auf Viktors Referenzseite",
"paragraphs": [
"Hier erfährst du alles über den Ankylosaurus namens Viktor.",
"Wähle einen Punkt aus der Navigation, um einzusteigen!"
]
},
"general": {
"personality": {
"heading": "Persönlichkeit",
"paragraphs": [
"Viktor ist kein Mann vieler Worte, seine Ausdrucksweise ist einfach und direkt und manchmal auch etwas unverblümt. Diese forsche Art kommt nicht bei jedem gut an und bringt ihn regelmäßig in Schwierigkeiten.",
"Gleichzeitig ist das aber auch Ausdruck seines überschwänglichen Selbstbewusstseins. Er lässt sich von niemandem etwas gefallen und zögert nicht, jemandem die Meinung zu geigen. Wenn das zu Handgreiflichkeiten führt, nimmt er diese in Kauf.",
"All das lässt ihn vielleicht wie einen wirklich unangenehmen Zeitgenossen erscheinen, aber er schätzt die Gesellschaft von Leuten denen er vertraut sehr. Allerdings ist er sehr wählerisch, wen er zu diesem Personenkreis zählt. Er schätzt den persönlichen Kontakt, am liebsten bei ein paar Bier in seiner Lieblingsbar.",
"Er mag es einfach, deshalb ist er auch kein großer Fan von Hightech. Er hat zwar ein Smartphone, aber er hasst es genauso sehr wie die Tatsache, dass er darauf angewiesen ist, eines zu besitzen."
]
},
"sexuality": {
"heading": "Sexualität",
"paragraphs": [
"Als Teenager bemerkte Viktor, dass er über die Jungs in seiner Klasse ein bisschen anders dachte. Vor allem im Sportunterricht neigte er dazu, seine Klassenkameraden länger zu beobachten, wenn sie sich in der Umkleidekabine rauften, wie es pubertierende Jungs eben tun. Er konnte sich jedoch nie ganz mit dem Gedanken anfreunden, dass er vielleicht ein bisschen anders ist als seine Freunde, die sich alle für Mädchen zu interessieren begannen, im Gegensatz zu ihm, der sich mehr für seine Kumpels interessierte.",
"Als er älter wurde, lernte er langsam, sich mit der Tatsache abzufinden, dass er in seinen Kumpels mehr sah als nur Freunde. Trotzdem hielt er sich weiterhin bedeckt.",
"Bis zu einem Abend auf einer Party, als sich einer seiner Freunde darüber beschwerte, dass ihn seine Freundin nicht mehr ranlassen würde. Die beiden waren schon gut angetrunken und Viktor machte ihm lallend den Vorschlag, dass er ihm als sein Kumpel aushelfen würde. Zuerst war sein Freund von dem Angebot irritiert, stimmte aber schließlich zu. Die beiden schlichen sich in ein Schlafzimmer in der Wohnung des Gastgebers, wo er seinen Freund den ganzen aufgestauten Druck in ihm ablassen ließ. Er erinnerte sich nicht mehr an viel von der Party, aber er vergaß nicht, wie gut es sich anfühlte, sich einem anderen Mann hinzugeben."
]
}
},
"anatomy": {
"images": {
"back": { "caption": "Viktor Referenz" },
"front": { "caption": "Viktor Vorderseite" }
},
"paragraphs": [
[
"Viktor ist ein zweibeiniger, plantigrader Ankylosaurus. Seine Haut ist größtenteils zweifarbig, mit verschiedenen Brauntönen.",
"Seine Stirn, Nase, Brust, sein Bauch und sein Schritt haben eine helle Wüstensandfarbe, die sich entlang der Unterseite seines Schwanzes fortsetzt.",
"Seine Wangen, Schultern, Oberschenkel und sein Rücken sind dagegen in einem satten Walnussbraun gehalten, das sich auch an den Seiten seines Schwanzes wiederfindet. Vom Hinterkopf über die Wirbelsäule bis zur Schwanzspitze verläuft ein durchgehender Streifen in tiefdunklem Zedernbraun. Arme und Beine zeichnen sich durch ein helles, erdiges Braun aus."
],
[
"Sein ganzer Körper ist mit aquamarinfarbenen Flecken gesprenkelt, die sich paarweise aus einem großen und einem kleinen Fleck zusammensetzen. Einzige Ausnahmen sind die Flecken auf den Wangen und hinter den unteren Wangenhörnern, die in Dreiergruppen auftreten, und die Oberseite seiner Schnauze, die einen großen einzelnen Fleck aufweist. Auch Maul, Zunge, Nasenlöcher und alle anderen Gewebe seines Körpers weisen diese Farbe auf.",
"Seine Augen leuchten in einer Mischung aus hellem Meeresgrün und elektrisierendem Blau.",
"Die Hörner und Krallen sind von einem typischen Knochenweiß. Angefangen mit dem Doppelhornpaar auf seinem Kopf zieht sich eine parallele Linie von Hörnern über seinen Rücken, mit zusätzlichen Hörnern an den Schultern und Oberschenkeln. Auch sein Schwanz ist auf beiden Seiten über die gesamte Länge mit Hörnern versehen. An der Spitze des Schwanzes befindet sich eine keulenartige Verknöcherung, mit der er Angreifer abwehren kann.",
"Seinen starken, stämmigen Körperbau verdankt er jahrelanger harter körperlicher Arbeit."
]
]
},
"career": {
"paragraphs": [
"Viktor hatte in der Vergangenheit viele verschiedene Jobs, von denen einige sehr prägend waren, während andere nur die Rechnungen bezahlten."
],
"jobs": [
{
"title": "Kneipenwirt",
"desc": "Viktors berufliche Laufbahn begann als Barkeeper in einer Kneipe in seiner Heimatstadt. Dort fungierte er oft auch als Türsteher, wenn ein paar Gäste einen über den Durst getrunken hatten und einen Aufstand machten. Ein entscheidender Moment in diesem Job war, als einer von ihnen über den Tresen kletterte und ihn mit einer zerbrochenen Flasche bedrohte. Als er zu Boden gerungen wurde und eine zerbrochene Flasche vor seinem Gesicht hatte, musste er in Sekundenbruchteilen eine Entscheidung treffen. Mit einem kräftigen Schwung seines Schwanzknochens schlug er den Angreifer nieder. Diese Erfahrung lehrte ihn, wie wichtig es ist, sich effektiv gegen unangenehme Gesellen zu verteidigen und Bedrohungen auszuräumen, bevor sie entstehen."
},
{
"title": "Holzfäller",
"desc": "Nachdem er seine Heimatstadt verlassen hatte, begann Viktor sich als Holzfäller zu verdienen. Die Keule am Ende seines Schwanzes erwies sich dabei als sehr nützlich, denn sie diente ihm als Gegengewicht und ermöglichte es ihm, effizient und kraftvoll mit seiner Axt zuzuschlagen und selbst die größten Bäume mit Leichtigkeit zu fällen. Seine von Natur aus zähen Schuppen schützten ihn vor Splittern aus dem gefällten Holz. Die jahrelange harte körperliche Arbeit hat seinen Körper im Laufe der Jahre gestählt. Bevor er die gefällten Stämme zum Sägewerk brachte, machte Viktor immer eine Pause, um sich in der Stille des Waldes auszuruhen. Obwohl er die Abgeschiedenheit des Landlebens eine Zeit lang genoss, sehnte er sich wieder nach der Geselligkeit des Stadtlebens."
},
{
"title": "Automachaniker",
"desc": "Nach seinem Umzug in eine Vorstadt bewarb sich Viktor in einer Autowerkstatt, wo er lernte, wie man Autos repariert. Er wurde richtig gut darin und hatte Spaß daran, kaputten Fahrzeugen neues mechanisches Leben einzuhauchen. Mit der Zeit geriet die Werkstatt jedoch in finanzielle Schwierigkeiten, da es immer schwieriger wurde, an Ersatzteile zu kommen, da die Autohersteller diese nur an zertifizierte Reparaturpartner abgaben und die Zertifizierungen dafür unbezahlbar waren. Viktor musste zusehen, wie sich das Geschäft langsam verschlechterte, da immer mehr qualifizierte Mitarbeiter entlassen wurden, bis die Werkstatt schließlich für immer geschlossen wurde."
},
{
"title": "Bauarbeiter",
"desc": "Da er Gefallen an körperlich anstrengender Arbeit gefunden hatte, nahm Viktor einen Job als Bauarbeiter an. Mit einem Schutzhelm auf dem Kopf und einem Werkzeuggürtel um die Hüfte war er immer bereit, mit anzupacken (oder zuzuschlagen, wenn Mauern oder Felsbrocken abgetragen werden mussten). Da er durch seine früheren Jobs mehr als genug Krafttraining hatte, wurde er oft mit Tragearbeiten beauftragt, um die Baustelle zu räumen und Baumaterial zu transportieren. Nach getaner Arbeit genoss Viktor die Gesellschaft seiner Kollegen bei einem Feierabendbier bis spät in die Nacht. An einem dieser Abende wurde er im Schutze der Nacht mit einem Kollegen etwas leichtsinnig und beide wurden in flagranti erwischt, als sie auf der Baustelle miteinander rummachten. In den folgenden Wochen hielten die Kollegen Abstand zu Viktor und auch die gemeinsamen Abende lösten sich sehr schnell auf, als er sich zu ihnen gesellen wollte. Die anhaltende Ausgrenzung veranlasste ihn schließlich, sich etwas Neues zu suchen."
},
{
"title": "Schweißer",
"desc": "In seinem Beruf als Schweißer verbrachte Viktor seine Tage im Schichtbetrieb in einer Werkstatt. Neben Spezialwerkzeugen und Plasmaschweißgeräten benutzte er auch seinen kräftigen Schwanzknüppel, um Metallteile in Form zu hämmern. Er eignete sich eine Vielzahl von Schweißtechniken an, um Metallstrukturen zu verbinden oder zu reparieren. Er arbeitete mit einer Vielzahl von Legierungen wie Stahl, Aluminium und Titan. Viktor zeigte bei seiner Arbeit extreme Geschicklichkeit, große Präzision und Liebe zum Detail. Diese Hingabe war jedoch einem eifersüchtigen Kollegen ein Dorn im Auge, der Viktor als Konkurrenten ansah und gegen ihn intrigierte, indem er seine Arbeit sabotierte, was schließlich in seiner Kündigung endete, nachdem ein Kunde die Schweißerei wegen Pfusch verklagt hatte. Viktor hat nie herausgefunden, wer der Schuldige war, und der Verlust dieses Jobs lastet immer noch auf ihm."
},
{
"title": "Lieferant",
"desc": "Als Viktor knapp bei Kasse war, nahm er einen Job in der Gig-Economy als Fahrer an, der Pakete für einen großen Online-Lieferdienst ausfuhr. Durch seine früheren körperlich anstrengenden Jobs konnte er selbst sperrige Lieferungen relativ leicht an ihr Ziel bringen. Hätten ihm die Manager des Versandzentrums nicht ständig im Nacken gesessen, hätte er diesen Job vielleicht sogar noch länger behalten können. Aber als einer der Manager versuchte, ihn vor dem ganzen Team bloßzustellen, platzte ihm der Kragen und er brach ihm mit seiner Keule das Bein. Am nächsten Tag musste er sich natürlich nicht mehr zum Dienst melden."
},
{
"title": "Hafenarbeiter",
"desc": "Derzeit arbeitet Viktor an den Docks in der Hafenstadt, in die er gezogen ist. Zu seinen Hauptaufgaben gehören das Be- und Entladen von Schiffen und der Transport der Ladung zu und von den Lagerhäusern. Das geschäftige Treiben im Hafen geht ihm manchmal auf die Nerven. Vor allem, wenn die Besatzungen der anlegenden Schiffe nach langer Zeit wieder an Land kommen und deutlich machen, dass sie mit \"Landratten\" nicht viel Umgang haben. Viktor lässt sich davon allerdings nicht sonderlich beeindrucken und mault zurück, wenn er den Eindruck bekommt, dass sie auf Ärger aus sind."
}
]
},
"attributions": {
"artwork": {
"heading": "Kunstwerke",
"headings": ["Kunstwerk", "Künstler"]
},
"other": {
"heading": "Sonstiges",
"headings": ["Typ", "Quelle"],
"icons": ["Symbole"],
"headingFont": ["Überschriften", "von"],
"copyFont": ["Fließtext", "von"],
"background": ["Hintergrund", ["Layered Waves aus", "von"]]
}
}
}

View file

@ -0,0 +1,138 @@
{
"welcomeHeader": {
"mainTitle": "Viktor Kraastav",
"subTitle": "Character Reference Page"
},
"nav": {
"home": "Home",
"general": "General",
"anatomy": "Anatomy",
"careerPath": "Career Path",
"attributions": "Attributions"
},
"langswitcher": {
"title": "Language",
"prompt": "View this page in:",
"buttonClose": "Close"
},
"data": {
"general": {
"heading": ["Key", "Value"],
"fullName": ["Full Name", "Viktor Kraastav"],
"dob": ["Date of Birth"],
"gender": ["Sex/Gender", "male ♂️"],
"height": ["Height"],
"weight": ["Weight"]
},
"sexuality": {
"heading": ["Key", "Value"],
"identifiesAs": ["Identifies as", "Gay"],
"preferredRole": ["Preferred role", "Bottom"]
},
"colors": {
"headings": ["Body part", "Value", "Color"],
"front": "Front",
"limbs": "Arms, legs",
"back": "Back Main",
"spine": "Back Spine",
"tissue": "Highlight scales, tissue",
"spikes": "Spikes, tail club",
"eyesPrimary": "Eyes primary",
"eyesSecondary": "Eyes secondary"
}
},
"home": {
"heading": "Welcome to Viktor's Ref Page",
"paragraphs": [
"Here you can learn all about the ankylosaurus named Viktor.",
"Pick an item from the navigation to dive in!"
]
},
"general": {
"personality": {
"heading": "Personality",
"paragraphs": [
"Viktor is not a man of many words, his manner of expression is simple and direct, and sometimes a little blunt. This brash manner doesn't go down well with everyone and regularly gets him into trouble.",
"At the same time, this is also an expression of his exuberant self-confidence. He won't take any crap from anyone and doesn't hesitate to give someone a piece of his mind. If that results in fisticuffs, he'll deal with them.",
"All this may make him look like a really unpleasant fellow, but he greatly appreciates the company of people he trusts. He is very selective about whom he counts among this group of people, though. He appreciates personal contact, preferably over a few beers at his favorite bar.",
"He likes things to be simple, which is why he is not a big fan of high-tech. He does have a smartphone, but he hates using it as much as he dislikes the fact he's dependent on owning one."
]
},
"sexuality": {
"heading": "Sexuality",
"paragraphs": [
"As a teenager, Viktor noticed that he felt a bit differently about the boys in his class. Especially in gym class, his eyes tended to linger longer on his classmates as they rough-housed in the locker room, as pubescent boys do. However, he could never quite come to terms with the idea that he might be a little different from his friends, who were all beginning to take an interest in girls, unlike him, who was more interested in his buddies.",
"As he got older, he slowly learned to come to terms with the fact that he saw more in his buddies than just friends. However, he still kept a low profile.",
"Until one evening at a party, when one of his friends complained that he wouldn't get any from his girlfriend. Both of them were already well intoxicated and Viktor made him the slurred proposal that he would help him out as his bro. At first his buddy was irritated by the offer, but eventually agreed. The two snuck into a bedroom at the host's place, where Viktor would let his buddy unload all the pent-up pressure inside him. He wouldn't remember much of the party, but what he didn't forget how good it felt to give himself to another man."
]
}
},
"anatomy": {
"images": {
"back": { "caption": "Viktor Ref" },
"front": { "caption": "Viktor Frontal Shot" }
},
"paragraphs": [
[
"Viktor is a bipedal plantigrade Ankylosaurus. His skin is mostly bicolored, with several shades of brown.",
"His forehead, nose, chest, belly and crotch are of light desert sand colors that run through the underside of his tail.",
"His cheeks, shoulders, back and thighs, in contrast, stand out with a rich walnut brown, which is also found on the sides of his tail. From the back of his head, down his spine and across the top of his tail is a continuous strip of deep dark cedar brown. Arms and legs are distinguished by a light earthy brown."
],
[
"His entire body is speckled with scattered aquamarine spots, which come in pairs of one large and one small spot. The only exceptions are the spots on his cheeks and behind his lower cheek horns, which come in threes and his the top side of his snout, which sports a big single spot. Mouth, tongue, nostrils and any other tissue of his body also feature this color.",
"His eyes shine with a mixture of light sea green and electric blue.",
"Horns and claws are of a typical bone white. Starting with the double pair of horns on his head, a parallel line of horns continues down his back, with additional horns adorning his shoulders and thighs. His tail is also armed with horns on both sides along its entire length. At the tip of the tail is a club-like ossification that he can use for fending off foes.",
"He owes his strong, stocky physique to years of hard physical labor."
]
]
},
"career": {
"paragraphs": [
"Viktor's had many different jobs in the past, some of which where very formative, other's just paid the bills."
],
"jobs": [
{
"title": "Bartender",
"desc": "Viktor's professional career began as a bartender at a pub in his hometown. There he often doubled as a bouncer when a few guests got too drunk and started making a fuss. A defining moment of that job was when someone climbed over the counter and threatened him with a broken bottle. Wrestled to the ground and with a broken bottle in front of his face, he had to make a split-second decision. With a powerful swing of his tail bone, he knocked the attacker down. This experience taught him the importance of effectively defending himself against unpleasant fellows and to eliminate threats before they get close to him ever again."
},
{
"title": "Lumberjack",
"desc": "After leaving his hometown, Viktor started working as a lumberjack. The club at the end of his tail came very much in handy as a counterweight for each swing of his axe, allowing him to strike efficiently and powerfully, felling even the largest trees with relative ease. His naturally tough scales protected him from splinters from the felled wood. The long-lasting hard physical work toughened his body over the years. Before taking the cut logs to the sawmill, Viktor always took a break to rest in the peace and quiet of the forest. Although he enjoyed the seclusion of the country life for a while, he was longing for the sociability of city life again."
},
{
"title": "Car Mechanic",
"desc": "Moving into a suburban town, Viktor applied at an auto repair shop, where he learned the ins and outs of fixing cars. He became really good at it and enjoyed breathing new mechanical life into broken down vehicles. However, as time went on, the repair shop faced financial troubles, as it became increasingly difficult to come by spare parts as auto makers would only deal them to certified repair partners and certifications were prohibitively expensive. Viktor had to watch business slowly deteriorate, as skilled coworkers kept getting laid off, until the repair shop closed down for good."
},
{
"title": "Construction Worker",
"desc": "Having taken a liking in physically demanding work, Viktor took on a job as a construction worker. Hard hat perched atop his head and belt tool slung around his waist, he was always ready to lend a hand (or tail if walls or boulders needed a good teardown). Since he had more than enough strength training from his previous jobs, he was often assigned carrying jobs to clear the site and haul building materials. After work was done, Viktor enjoyed the company of his colleagues over an after-work beer until late in the evenings. On one of these evenings, he got a little reckless with a colleague under the veil of the night and both were caught in the act of fooling around with each other on the construction site. In the following weeks, the colleagues kept their distance from Viktor and the evenings together also dissolved very quickly when he joined them. The continued ostracization ultimately prompted him to look for something new."
},
{
"title": "Welder",
"desc": "In his job as a welder, Viktor spent his days working shifts in a workshop. In addition to special tools and plasma welders, he also used his powerful tail club to hammer metal parts into shape. He acquired a wide variety of welding techniques to join or repair metal structures. He worked with a wide variety of alloys such as steel, aluminum and titanium. Viktor showed extreme skill in his work with great precision and attention to detail. However, this dedication was a thorn in the side of a jealous colleague who saw Viktor as a rival and schemed against him, sabotaging his work, which ultimately ended in his termination after a customer sued the welding shop for botching the job. Viktor never found out who the culprit was, and the loss of that job still hangs over him."
},
{
"title": "Delivery Driver",
"desc": "When Viktor was strapped for cash, he took on a job in the gig economy as a driver delivering packages for a large online delivery service. His previous physically demanding jobs allowed him to haul even bulky deliveries to their destination with relative ease. If the shipping center managers hadn't been breathing down his neck constantly, he might have held this job even longer. But after one of the managers tried to show him up in front of the whole team, he snapped and broke their leg with his tail club. Of course, he didn't have to report for duty the next day."
},
{
"title": "Docks Werehouse Worker",
"desc": "Currently Viktor works at the docks in the port town he moved to. His main responsibilities include loading and unloading cargo from ships and transporting it to and from warehousing. The hustle and bustle of the port sometimes gets on his nerves. Especially when the crew of docking ships come ashore again after a long time and make it clear that they don't have much contact with \"landlubbers\". Viktor doesn't get particularly impressed by this and foul mouths them right back if he gets the impression they're looking for trouble."
}
]
},
"attributions": {
"artwork": {
"heading": "Artworks",
"headings": ["Artwork", "Artist"]
},
"other": {
"heading": "Other",
"headings": ["Type", "Source"],
"icons": ["Icons"],
"headingFont": ["Heading Font", "by"],
"copyFont": ["Copy Font", "by"],
"background": ["Background", ["Layered Waves aus", "von"]]
}
}
}

View file

@ -1,12 +1,24 @@
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createI18n } from 'vue-i18n'
import messages from './lang'
import "normalize.css";
import "@/scss/main.scss";
import 'normalize.css'
import '@/scss/main.scss'
const app = createApp(App);
type MessageSchema = typeof messages.en
app.use(router);
const i18n = createI18n<[MessageSchema], 'en' | 'de'>({
legacy: false,
locale: 'en',
fallbackLocale: 'en',
messages
})
app.mount("body");
const app = createApp(App)
app.use(router)
app.use(i18n)
app.mount('body')

View file

@ -1,64 +1,68 @@
import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '@/views/HomeView.vue'
import HomeIcon from "@/assets/icons/HomeIcon.vue";
import IdCardIcon from "@/assets/icons/IdCardIcon.vue";
import PaletteIcon from "@/assets/icons/PaletteIcon.vue";
import BriefcaseIcon from "@/assets/icons/BriefcaseIcon.vue";
import CircleInfoIcon from "@/assets/icons/CircleInfoIcon.vue";
import HomeIcon from '@/assets/icons/HomeIcon.vue'
import IdCardIcon from '@/assets/icons/IdCardIcon.vue'
import PaletteIcon from '@/assets/icons/PaletteIcon.vue'
import BriefcaseIcon from '@/assets/icons/BriefcaseIcon.vue'
import CircleInfoIcon from '@/assets/icons/CircleInfoIcon.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
scrollBehavior: () => {
return { top: 0 };
return new Promise((resolve) => {
setTimeout(() => {
resolve({ top: 0 })
}, 500)
})
},
routes: [
{
path: "/",
name: "home",
path: '/',
name: 'home',
component: HomeView,
meta: {
title: "Home",
icon: HomeIcon,
},
title: 'nav.home',
icon: HomeIcon
}
},
{
path: "/general",
name: "general",
component: () => import("@/views/GeneralView.vue"),
path: '/general',
name: 'general',
component: () => import('@/views/GeneralView.vue'),
meta: {
title: "General",
icon: IdCardIcon,
},
title: 'nav.general',
icon: IdCardIcon
}
},
{
path: "/anatomy",
name: "anatomy",
component: () => import("@/views/AnatomyView.vue"),
path: '/anatomy',
name: 'anatomy',
component: () => import('@/views/AnatomyView.vue'),
meta: {
title: "Anatomy",
icon: PaletteIcon,
},
title: 'nav.anatomy',
icon: PaletteIcon
}
},
{
path: "/career-path",
name: "career-path",
component: () => import("@/views/CareerPathView.vue"),
path: '/career-path',
name: 'career-path',
component: () => import('@/views/CareerPathView.vue'),
meta: {
title: "Career Path",
icon: BriefcaseIcon,
},
title: 'nav.careerPath',
icon: BriefcaseIcon
}
},
{
path: "/attributions",
name: "attributions",
component: () => import("@/views/AttributionsView.vue"),
path: '/attributions',
name: 'attributions',
component: () => import('@/views/AttributionsView.vue'),
meta: {
title: "Attributions",
icon: CircleInfoIcon,
},
},
],
});
title: 'nav.attributions',
icon: CircleInfoIcon
}
}
]
})
export default router;
export default router

View file

@ -1,4 +1,4 @@
@import "fontfaces";
@import 'fontfaces';
/* theme colors */
:root {
@ -33,8 +33,8 @@
/* general purpose variables */
:root {
--font-family-copy: "Arvo", sans-serif;
--font-family-headings: "Secular One", serif;
--font-family-copy: 'Arvo', sans-serif;
--font-family-headings: 'Secular One', serif;
--font-size: 18px;
--text-line-height: 1.5;
@ -43,6 +43,8 @@
--page-background-image: url(@/assets/layered-waves-light.svg);
--page-background-image-height: 100vw;
--page-background-image-max-height: 50vh;
--page-transition: opacity 0.5s ease, transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55);
--page-transform: translateY(5em);
--paragraph-margin: 1rem;
@ -54,20 +56,19 @@
--container-spacing-bottom-safe: max(1rem, env(safe-area-inset-bottom));
--container-spacing-left-safe: max(1rem, env(safe-area-inset-left));
--textblock-padding: 0 var(--container-spacing-right-safe) 0
var(--container-spacing-left-safe);
--textblock-padding: 0 var(--container-spacing-right-safe) 0 var(--container-spacing-left-safe);
--welcome-padding: 2rem var(--container-spacing-right-safe) 2rem
var(--container-spacing-left-safe);
--welcome-headings-text-align: center;
--modal-width: 100%;
--modal-border: 0.25rem solid var(--color-modal-border);
--navigation-flow: row nowrap;
--navigation-justify: space-evenly;
--navigation-align: center;
--navigation-position-top: auto;
--navigation-position-right: 0;
--navigation-position-bottom: 0;
--navigation-position-left: 0;
--navigation-position: auto 0 0 0;
--navigation-size: 4rem;
--navigation-width: 100%;
@ -75,19 +76,14 @@
--navigation-height: auto;
--navigation-padding: 0 0 0 env(safe-area-inset-left);
--navigation-cutout: 0 0
calc(var(--navigation-size) + env(safe-area-inset-bottom)) 0;
--navigation-cutout-page-background: calc(
var(--navigation-size) + env(safe-area-inset-bottom)
);
--navigation-cutout: 0 0 calc(var(--navigation-size) + env(safe-area-inset-bottom)) 0;
--navigation-cutout-page-background: calc(var(--navigation-size) + env(safe-area-inset-bottom));
--navigation-cutout-main: 0;
--navigation-link-display: none;
--navigation-link-flex: 1 0 var(--navigation-size);
--navigation-link-justify: center;
--navigation-link-height: calc(
var(--navigation-size) + env(safe-area-inset-bottom)
);
--navigation-link-height: calc(var(--navigation-size) + env(safe-area-inset-bottom));
--navigation-link-padding: 0 0 env(safe-area-inset-bottom) 0;
--navigation-link-icon-size: calc(var(--navigation-size) / 2);
--navigation-link-icon-spacing: calc(var(--navigation-size) * 0.25);
@ -96,8 +92,8 @@
var(--color-router-link);
--navigation-link-last-child-margin: 0;
--timeline-max-width: calc(var(--timeline-circle-size) * 15);
--timeline-circle-size: 4rem;
--timeline-max-width: calc(var(--timeline-circle-size) * 10);
--timeline-circle-size: 3rem;
--timeline-circle-font-size: calc(var(--timeline-circle-size) * 0.2);
--timeline-circle-background-color: var(--theme-c-woody-brown);
--timeline-circle-padding: calc(var(--timeline-circle-font-size));
@ -109,22 +105,33 @@
var(--timeline-circle-size) / 2 - var(--timeline-stroke-thickness) / 2
);
--timeline-stroke-position-odd: var(--timeline-stroke-position-top) auto auto
var(--timeline-stroke-position-horizontal);
--timeline-stroke-position-even: var(--timeline-stroke-position-odd);
--timeline-item-margin: 1.5rem 0;
--timeline-item-margin-odd: 0 0 0 calc(50% - var(--timeline-circle-size) / 2);
--timeline-item-margin-even: 0 calc(50% - var(--timeline-circle-size) / 2) 0 0;
--timeline-item-margin-odd: var(--timeline-item-margin);
--timeline-item-margin-even: var(--timeline-item-margin);
--timeline-item-text-align-even: left;
--timeline-item-content-padding-odd: 0 0 0 var(--container-spacing-left-safe);
--timeline-item-content-padding-even: var(--timeline-item-content-padding-odd);
--table-border-radius: 1rem;
--table-outer-spacing: 0 var(--container-spacing-right-safe) 0
var(--container-spacing-left-safe);
--table-outer-spacing: 0 var(--container-spacing-right-safe) 0 var(--container-spacing-left-safe);
--table-cell-padding: 0.25rem 0.5rem;
}
/* semantic color variables for this project */
:root {
--color-background: var(--theme-c-antique-white);
--color-background-body: var(--theme-c-walnut-brown);
--color-background-body: var(--theme-c-desert-sand);
--color-border: var(--theme-c-crystal-blue);
--color-modal-background: var(--theme-c-desert-sand);
--color-modal-border: var(--theme-c-walnut-brown);
--color-button: var(--theme-c-walnut-brown);
--color-button-box-shadow: var(--theme-c-deep-oak);
--color-button-text: var(--theme-c-text-dark);
--color-heading: var(--theme-c-text-light);
--color-text: var(--theme-c-text-light);
@ -161,8 +168,14 @@
:root {
--page-background-image: url(@/assets/layered-waves-dark.svg);
--color-background: var(--theme-c-dark-grey-blue);
--color-background-body: var(--theme-c-deep-oak);
--color-border: var(--theme-c-desert-sand);
--color-modal-background: var(--theme-c-deep-oak);
--color-modal-border: var(--theme-c-antique-white);
--color-button: var(--theme-c-antique-white);
--color-button-box-shadow: var(--theme-c-desert-sand);
--color-button-text: var(--theme-c-text-light);
--color-heading: var(--theme-c-text-dark);
--color-text: var(--theme-c-text-dark);
@ -194,6 +207,8 @@
:root {
--table-cell-padding: 0.5rem 1rem;
--font-size: 20px;
--timeline-circle-size: 4rem;
--modal-width: 30rem;
}
}
@ -203,25 +218,18 @@
--page-background-image-max-height: 100vh;
--color-table-color-cell-width: 10rem;
--textblock-padding: 0 var(--container-spacing-right-safe) 0
var(--container-spacing-left-safe);
--textblock-padding: 0 var(--container-spacing-right-safe) 0 var(--container-spacing-left-safe);
--navigation-cutout: 0 0 0
calc(var(--navigation-size) + env(safe-area-inset-left));
--navigation-cutout: 0 0 0 calc(var(--navigation-size) + env(safe-area-inset-left));
--navigation-cutout-page-background: 0;
--navigation-cutout-main: var(--navigation-cutout);
--navigation-flow: column nowrap;
--navigation-justify: flex-start;
--navigation-align: stretch;
--navigation-position-top: 0;
--navigation-position-right: auto;
--navigation-position-bottom: 0;
--navigation-position-left: 0;
--navigation-position: 0 auto 0 0;
--navigation-width: calc(
var(--navigation-size) + env(safe-area-inset-left)
);
--navigation-width: calc(var(--navigation-size) + env(safe-area-inset-left));
--navigation-width-expanded: var(--navigation-width);
--navigation-link-display: block;
@ -248,7 +256,18 @@
@media (min-width: 64em) {
:root {
--timeline-max-width: calc(var(--timeline-circle-size) * 15);
--timeline-stroke-length: calc(100% - var(--timeline-circle-size));
--timeline-stroke-position-even: var(--timeline-stroke-position-top)
var(--timeline-stroke-position-horizontal) auto auto;
--timeline-item-margin-odd: 0 0 0 calc(50% - var(--timeline-circle-size) / 2);
--timeline-item-margin-even: 0 calc(50% - var(--timeline-circle-size) / 2) 0 0;
--timeline-item-flex-order: 1;
--timeline-item-content-margin-odd: 0 0 0 1rem;
--timeline-item-content-margin-even: 0 1rem 0 0;
--timeline-item-text-align-even: right;
--timeline-item-content-padding-odd: var(--textblock-padding);
--timeline-item-content-padding-even: var(--textblock-padding);
}
}
@ -264,10 +283,15 @@
}
}
@media (hover: hover) {
@media (hover: hover) and (min-width: 50em) {
:root {
--navigation-width-expanded: calc(
var(--navigation-size) * 3.75 + env(safe-area-inset-left)
);
--navigation-width-expanded: calc(var(--navigation-size) * 3.75 + env(safe-area-inset-left));
}
}
@media (prefers-reduced-motion) {
:root {
--page-transition: opacity 0.5s ease;
--page-transform: none;
}
}

View file

@ -1,2 +1,2 @@
@import "@/assets/fonts/arvo/arvo";
@import "@/assets/fonts/secular-one/secular-one";
@import '@/assets/fonts/arvo/arvo';
@import '@/assets/fonts/secular-one/secular-one';

View file

@ -1,4 +1,4 @@
@import "@/scss/base";
@import '@/scss/base';
:root {
font-family: var(--font-family-copy);
@ -19,7 +19,9 @@ body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background-body);
transition: color 0.5s, background-color 0.5s;
transition:
color 0.5s,
background-color 0.5s;
line-height: var(--text-line-height);
text-rendering: optimizeLegibility;
@ -41,11 +43,9 @@ main {
&:after {
display: block;
content: "";
content: '';
position: sticky;
right: 0;
bottom: var(--navigation-cutout-page-background);
left: 0;
inset: auto 0 var(--navigation-cutout-page-background) 0;
height: var(--page-background-image-height);
max-height: var(--page-background-image-max-height);
@ -84,6 +84,7 @@ h1 {
p {
text-align: justify;
margin: var(--paragraph-margin) 0;
hyphens: auto;
}
a {
@ -96,8 +97,7 @@ a {
&:hover {
color: var(--color-link-text-hover);
box-shadow: inset 0 calc(var(--link-inset-box-shadow) * -1) 0 0
var(--color-link-text-underline);
box-shadow: inset 0 calc(var(--link-inset-box-shadow) * -1) 0 0 var(--color-link-text-underline);
}
}
@ -171,3 +171,14 @@ table {
}
}
}
.fade-enter-active,
.fade-leave-active {
transition: var(--page-transition);
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: var(--page-transform);
}

View file

@ -1,196 +1,165 @@
<script setup lang="ts">
import RefImage from "@/components/RefImage.vue";
import ColorTable from "@/components/ColorTable.vue";
import RefImage from '@/components/RefImage.vue'
import ColorTable from '@/components/ColorTable.vue'
const colors = [
{ name: "Front", value: "#e7c7b1" },
{ name: "Arms, legs", value: "#493428" },
{ name: "Back Main", value: "#422322" },
{ name: "Back Spine", value: "#341c1c" },
{ name: "Highlight scales, tissue", value: "#6bb9db" },
{ name: "Spikes, tail club", value: "#f8ebdd" },
{ name: "Eyes primary", value: "#a7eef1" },
{ name: "Eyes secondary", value: "#6dabd1" },
];
{ name: 'data.colors.front', value: '#e7c7b1' },
{ name: 'data.colors.limbs', value: '#493428' },
{ name: 'data.colors.back', value: '#422322' },
{ name: 'data.colors.spine', value: '#341c1c' },
{ name: 'data.colors.tissue', value: '#6bb9db' },
{ name: 'data.colors.spikes', value: '#f8ebdd' },
{ name: 'data.colors.eyesPrimary', value: '#a7eef1' },
{ name: 'data.colors.eyesSecondary', value: '#6dabd1' }
]
</script>
<template>
<section>
<h1>{{ $route.meta.title }}</h1>
</section>
<article>
<section>
<h1>{{ $t(`${$route.meta.title}`) }}</h1>
</section>
<RefImage dropshadow>
<template v-if="$route.query.nsfw">
<source
srcset="
@/assets/viktor-ref-NSFW-alpha.png?w=375;420;500;750;840;1000;1125;1260;1500&avif&quality=75&withoutEnlargement&srcset
"
sizes="(min-width: 64em) 500px, (min-width: 50em) 420px, 375px"
type="image/avif"
/>
<source
srcset="
@/assets/viktor-ref-NSFW-alpha.png?w=375;420;500;750;840;1000;1125;1260;1500&webp&quality=100&withoutEnlargement&srcset
"
sizes="(min-width: 64em) 500px, (min-width: 50em) 420px, 375px"
type="image/webp"
/>
<img
srcset="
@/assets/viktor-ref-NSFW-alpha.png?w=375;420;500;750;840;1000;1125;1260;1500&png&withoutEnlargement&srcset
"
sizes="(min-width: 64em) 500px, (min-width: 50em) 420px, 375px"
alt="Viktor Ref by sabertoofs"
loading="lazy"
/>
</template>
<RefImage dropshadow>
<template v-if="$route.query.nsfw">
<source
srcset="
@/assets/viktor-ref-NSFW-alpha.png?w=375;420;500;750;840;1000;1125;1260;1500&format=avif&quality=75&withoutEnlargement&as=srcset
"
sizes="(min-width: 64em) 500px, (min-width: 50em) 420px, 375px"
type="image/avif"
/>
<source
srcset="
@/assets/viktor-ref-NSFW-alpha.png?w=375;420;500;750;840;1000;1125;1260;1500&format=webp&quality=100&withoutEnlargement&as=srcset
"
sizes="(min-width: 64em) 500px, (min-width: 50em) 420px, 375px"
type="image/webp"
/>
<img
srcset="
@/assets/viktor-ref-NSFW-alpha.png?w=375;420;500;750;840;1000;1125;1260;1500&format=png&withoutEnlargement&as=srcset
"
sizes="(min-width: 64em) 500px, (min-width: 50em) 420px, 375px"
alt="Viktor Ref by sabertoofs"
loading="lazy"
/>
</template>
<template v-else>
<source
srcset="
@/assets/viktor-ref-SFW-alpha.png?w=375;420;500;750;840;1000;1125;1260;1500&avif&quality=75&withoutEnlargement&srcset
"
sizes="(min-width: 64em) 500px, (min-width: 50em) 420px, 375px"
type="image/avif"
/>
<source
srcset="
@/assets/viktor-ref-SFW-alpha.png?w=375;420;500;750;840;1000;1125;1260;1500&webp&quality=100&withoutEnlargement&srcset
"
sizes="(min-width: 64em) 500px, (min-width: 50em) 420px, 375px"
type="image/webp"
/>
<img
srcset="
@/assets/viktor-ref-SFW-alpha.png?w=375;420;500;750;840;1000;1125;1260;1500&png&withoutEnlargement&srcset
"
sizes="(min-width: 64em) 500px, (min-width: 50em) 420px, 375px"
alt="Viktor Ref by sabertoofs"
loading="lazy"
/>
</template>
<template v-else>
<source
srcset="
@/assets/viktor-ref-SFW-alpha.png?w=375;420;500;750;840;1000;1125;1260;1500&format=avif&quality=75&withoutEnlargement&as=srcset
"
sizes="(min-width: 64em) 500px, (min-width: 50em) 420px, 375px"
type="image/avif"
/>
<source
srcset="
@/assets/viktor-ref-SFW-alpha.png?w=375;420;500;750;840;1000;1125;1260;1500&format=webp&quality=100&withoutEnlargement&as=srcset
"
sizes="(min-width: 64em) 500px, (min-width: 50em) 420px, 375px"
type="image/webp"
/>
<img
srcset="
@/assets/viktor-ref-SFW-alpha.png?w=375;420;500;750;840;1000;1125;1260;1500&format=png&withoutEnlargement&as=srcset
"
sizes="(min-width: 64em) 500px, (min-width: 50em) 420px, 375px"
:alt="`${$t('anatomy.images.back.caption')} by sabertoofs`"
loading="lazy"
/>
</template>
<template #caption>
Viktor Ref &copy; <a href="http://twitter.com/sabertoofs">sabertoofs</a>
</template>
</RefImage>
<template #caption>
{{ $t('anatomy.images.back.caption') }} &copy;
<a href="http://twitter.com/sabertoofs">sabertoofs</a>
</template>
</RefImage>
<ColorTable :colors="colors"></ColorTable>
<ColorTable :colors="colors"></ColorTable>
<section>
<p>
Viktor is a bipedal plantigrade Ankylosaurus. His skin is mostly
bicolored, with several shades of brown.
</p>
<section>
<p v-for="(p, i) in $tm('anatomy.paragraphs[0]')" :key="i">{{ p }}</p>
</section>
<p>
His forehead, nose, chest, belly and crotch are of light desert sand
colors that run through the underside of his tail.
</p>
<p>
His cheeks, shoulders, back and thighs, in contrast, stand out with a rich
walnut brown, which is also found on the sides of his tail. From the back
of his head, down his spine and across the top of his tail is a continuous
strip of deep dark cedar brown. Arms and legs are distinguished by a light
earthy brown.
</p>
</section>
<RefImage dropshadow>
<template v-if="$route.query.nsfw">
<source
srcset="
@/assets/viktor-front-NSFW-alpha.png?w=430;530;590;710;860;1060;1180;1420;1290;1590;1770;2130&avif&quality=75&withoutEnlargement&srcset
"
sizes="(min-width: 64em) 710px,
<RefImage dropshadow>
<template v-if="$route.query.nsfw">
<source
srcset="
@/assets/viktor-front-NSFW-alpha.png?w=430;530;590;710;860;1060;1180;1420;1290;1590;1770;2130&format=avif&quality=75&withoutEnlargement&as=srcset
"
sizes="(min-width: 64em) 710px,
(min-width: 50em) 590px,
(min-width: 27em) 530px,
430px"
type="image/avif"
/>
<source
srcset="
@/assets/viktor-front-NSFW-alpha.png?w=430;530;590;710;860;1060;1180;1420;1290;1590;1770;2130&webp&quality=100&withoutEnlargement&srcset
"
sizes="(min-width: 64em) 710px,
type="image/avif"
/>
<source
srcset="
@/assets/viktor-front-NSFW-alpha.png?w=430;530;590;710;860;1060;1180;1420;1290;1590;1770;2130&format=webp&quality=100&withoutEnlargement&as=srcset
"
sizes="(min-width: 64em) 710px,
(min-width: 50em) 590px,
(min-width: 27em) 530px,
430px"
type="image/webp"
/>
<img
srcset="
@/assets/viktor-front-NSFW-alpha.png?w=430;530;590;710;860;1060;1180;1420;1290;1590;1770;2130&png&withoutEnlargement&srcset
"
sizes="(min-width: 64em) 710px,
type="image/webp"
/>
<img
srcset="
@/assets/viktor-front-NSFW-alpha.png?w=430;530;590;710;860;1060;1180;1420;1290;1590;1770;2130&format=png&withoutEnlargement&as=srcset
"
sizes="(min-width: 64em) 710px,
(min-width: 50em) 590px,
(min-width: 27em) 530px,
430px"
alt="Viktor frontal shot by sabertoofs"
loading="lazy"
/>
</template>
alt="Viktor frontal shot by sabertoofs"
loading="lazy"
/>
</template>
<template v-else>
<source
srcset="
@/assets/viktor-front-SFW-alpha.png?w=430;530;590;710;860;1060;1180;1420;1290;1590;1770;2130&avif&quality=75&withoutEnlargement&srcset
"
sizes="(min-width: 64em) 710px,
<template v-else>
<source
srcset="
@/assets/viktor-front-SFW-alpha.png?w=430;530;590;710;860;1060;1180;1420;1290;1590;1770;2130&format=avif&quality=75&withoutEnlargement&as=srcset
"
sizes="(min-width: 64em) 710px,
(min-width: 50em) 590px,
(min-width: 27em) 530px,
430px"
type="image/avif"
/>
<source
srcset="
@/assets/viktor-front-SFW-alpha.png?w=430;530;590;710;860;1060;1180;1420;1290;1590;1770;2130&webp&quality=100&withoutEnlargement&srcset
"
sizes="(min-width: 64em) 710px,
type="image/avif"
/>
<source
srcset="
@/assets/viktor-front-SFW-alpha.png?w=430;530;590;710;860;1060;1180;1420;1290;1590;1770;2130&format=webp&quality=100&withoutEnlargement&as=srcset
"
sizes="(min-width: 64em) 710px,
(min-width: 50em) 590px,
(min-width: 27em) 530px,
430px"
type="image/webp"
/>
<img
srcset="
@/assets/viktor-front-SFW-alpha.png?w=430;530;590;710;860;1060;1180;1420;1290;1590;1770;2130&png&withoutEnlargement&srcset
"
sizes="(min-width: 64em) 710px,
type="image/webp"
/>
<img
srcset="
@/assets/viktor-front-SFW-alpha.png?w=430;530;590;710;860;1060;1180;1420;1290;1590;1770;2130&format=png&withoutEnlargement&as=srcset
"
sizes="(min-width: 64em) 710px,
(min-width: 50em) 590px,
(min-width: 27em) 530px,
430px"
alt="Viktor frontal shot by sabertoofs"
loading="lazy"
/>
</template>
:alt="`${$t('anatomy.images.front.caption')} by sabertoofs`"
loading="lazy"
/>
</template>
<template #caption>
Viktor frontal shot &copy;
<a href="http://twitter.com/sabertoofs">sabertoofs</a>
</template>
</RefImage>
<template #caption>
{{ $t('anatomy.images.front.caption') }} &copy;
<a href="http://twitter.com/sabertoofs">sabertoofs</a>
</template>
</RefImage>
<section>
<p>
His entire body is speckled with scattered aquamarine spots, which come in
pairs of one large and one small spot. The only exceptions are the spots
on his cheeks and behind his lower cheek horns, which come in threes and
his the top side of his snout, which sports a big single spot. Mouth,
tongue, nostrils and any other tissue of his body also feature this color.
</p>
<p>His eyes shine with a mixture of light sea green and electric blue.</p>
<p>
Horns and claws are of a typical bone white. Starting with the double pair
of horns on his head, a parallel line of horns continues down his back,
with additional horns adorning his shoulders and thighs. His tail is also
armed with horns on both sides along its entire length. At the tip of the
tail is a club-like ossification that he can use for fending off foes.
</p>
<p>He owes his strong, stocky physique to years of hard physical labor.</p>
</section>
<section>
<p v-for="(p, i) in $tm('anatomy.paragraphs[1]')" :key="i">{{ p }}</p>
</section>
</article>
</template>

View file

@ -1,121 +1,103 @@
<script setup lang="ts">
import AttributionTable from "@/components/AttributionTable.vue";
import AttributionTable from '@/components/AttributionTable.vue'
import ViktorRefAlpha from "@/assets/viktor-ref-SFW-alpha.png?w=400&webp&quality=100&imagetools";
import ViktorFront from "@/assets/viktor-front-SFW-alpha.png?w=400&webp&quality=100&imagetools";
import ViktorRefAlpha from '@/assets/viktor-ref-SFW-alpha.png?w=400&format=webp&quality=100&imagetools'
import ViktorFront from '@/assets/viktor-front-SFW-alpha.png?w=400&format=webp&quality=100&imagetools'
const attributions = [
{
artwork: ViktorRefAlpha,
artist: "sabertoofs",
artist: 'sabertoofs',
links: {
furaffinity: "https://www.furaffinity.net/user/sabertoofs",
twitter: "https://twitter.com/sabertoofs",
},
furaffinity: 'https://www.furaffinity.net/user/sabertoofs',
twitter: 'https://twitter.com/sabertoofs'
}
},
{
artwork: ViktorFront,
artist: "sabertoofs",
artist: 'sabertoofs',
links: {
furaffinity: "https://www.furaffinity.net/user/sabertoofs",
twitter: "https://twitter.com/sabertoofs",
},
},
];
furaffinity: 'https://www.furaffinity.net/user/sabertoofs',
twitter: 'https://twitter.com/sabertoofs'
}
}
]
</script>
<template>
<section>
<h1>{{ $route.meta.title }}</h1>
<h2>Artwork</h2>
</section>
<AttributionTable :attributions="attributions" />
<section>
<h2>Other</h2>
</section>
<table>
<thead>
<tr>
<th>Type</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td>Icons</td>
<td>
<a
href="https://fontawesome.com/license/free"
target="_blank"
rel="noopener noreferrer"
>
Font Awesome
</a>
</td>
</tr>
<tr>
<td>Heading Font</td>
<td>
<a
href="https://github.com/MichalSahar/Secular"
target="_blank"
rel="noopener noreferrer"
>
Secular One
</a>
by
<a
href="https://github.com/MichalSahar"
target="_blank"
rel="noopener noreferrer"
>
Michal Sahar
</a>
</td>
</tr>
<tr>
<td>Copy Font</td>
<td>
<a
href="https://antonkoovit.com/typefaces/arvo"
target="_blank"
rel="noopener noreferrer"
>
Arvo
</a>
by
<a
href="https://antonkoovit.com/"
target="_blank"
rel="noopener noreferrer"
>
Anton Koovit
</a>
</td>
</tr>
<tr>
<td>Background</td>
<td>
Layered Waves from
<a
href="https://haikei.app/"
target="_blank"
rel="noopener noreferrer"
>
Haikei
</a>
by
<a
href="https://zcreativelabs.com/"
target="_blank"
rel="noopener noreferrer"
>
z creative labs
</a>
</td>
</tr>
</tbody>
</table>
<article>
<section>
<h1>{{ $t(`${$route.meta.title}`) }}</h1>
<h2>{{ $t('attributions.artwork.heading') }}</h2>
</section>
<AttributionTable :attributions="attributions" />
<section>
<h2>{{ $t('attributions.other.heading') }}</h2>
</section>
<table>
<thead>
<tr>
<th>{{ $t('attributions.other.headings[0]') }}</th>
<th>{{ $t('attributions.other.headings[1]') }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ $t('attributions.other.icons[0]') }}</td>
<td>
<a
href="https://fontawesome.com/license/free"
target="_blank"
rel="noopener noreferrer"
>
Font Awesome
</a>
</td>
</tr>
<tr>
<td>{{ $t('attributions.other.headingFont[0]') }}</td>
<td>
<a
href="https://github.com/MichalSahar/Secular"
target="_blank"
rel="noopener noreferrer"
>
Secular One
</a>
{{ $t('attributions.other.headingFont[1]') }}
<a href="https://github.com/MichalSahar" target="_blank" rel="noopener noreferrer">
Michal Sahar
</a>
</td>
</tr>
<tr>
<td>{{ $t('attributions.other.copyFont[0]') }}</td>
<td>
<a
href="https://antonkoovit.com/typefaces/arvo"
target="_blank"
rel="noopener noreferrer"
>
Arvo
</a>
{{ $t('attributions.other.copyFont[1]') }}
<a href="https://antonkoovit.com/" target="_blank" rel="noopener noreferrer">
Anton Koovit
</a>
</td>
</tr>
<tr>
<td>{{ $t('attributions.other.background[0]') }}</td>
<td>
{{ $t('attributions.other.background[1][0]') }}
<a href="https://haikei.app/" target="_blank" rel="noopener noreferrer">Haikei</a>
{{ $t('attributions.other.background[1][1]') }}
<a href="https://zcreativelabs.com/" target="_blank" rel="noopener noreferrer">
z creative labs
</a>
</td>
</tr>
</tbody>
</table>
</article>
</template>
<style lang="scss"></style>

View file

@ -1,152 +1,47 @@
<script setup lang="ts">
import TimelineList from "@/components/TimelineList.vue";
import TimelineItem from "@/components/TimelineItem.vue";
import WhiskeyGlassIcon from "@/assets/icons/WhiskeyGlassIcon.vue";
import TreeIcon from "@/assets/icons/TreeIcon.vue";
import CarIcon from "@/assets/icons/CarIcon.vue";
import HelmetSafetyIcon from "@/assets/icons/HelmetSafetyIcon.vue";
import IndustryIcon from "@/assets/icons/IndustryIcon.vue";
import TruckIcon from "@/assets/icons/TruckIcon.vue";
import BoxesIcon from "@/assets/icons/BoxesIcon.vue";
import TimelineList from '@/components/TimelineList.vue'
import TimelineItem from '@/components/TimelineItem.vue'
import WhiskeyGlassIcon from '@/assets/icons/WhiskeyGlassIcon.vue'
import TreeIcon from '@/assets/icons/TreeIcon.vue'
import CarIcon from '@/assets/icons/CarIcon.vue'
import HelmetSafetyIcon from '@/assets/icons/HelmetSafetyIcon.vue'
import IndustryIcon from '@/assets/icons/IndustryIcon.vue'
import TruckIcon from '@/assets/icons/TruckIcon.vue'
import BoxesIcon from '@/assets/icons/BoxesIcon.vue'
interface Job {
title: string
desc: string
}
const jobIcons = [
WhiskeyGlassIcon,
TreeIcon,
CarIcon,
HelmetSafetyIcon,
IndustryIcon,
TruckIcon,
BoxesIcon
]
</script>
<template>
<section>
<h1>{{ $route.meta.title }}</h1>
<p>
Viktor's had many different jobs in the past, some of which where very
formative, other's just paid the bills.
</p>
</section>
<article>
<section>
<h1>{{ $t(`${$route.meta.title}`) }}</h1>
<p v-for="(p, i) in $tm('career.paragraphs')" :key="i">{{ p }}</p>
</section>
<TimelineList>
<TimelineItem>
<template #icon><WhiskeyGlassIcon /></template>
<template #headline>Bartender</template>
<template #content>
<p>
Viktor's professional career began as a bartender at a pub in his
hometown. There he often doubled as a bouncer when a few guests got
too drunk and started making a fuss. A defining moment of that job was
when someone climbed over the counter and threatened him with a broken
bottle. Wrestled to the ground and with a broken bottle in front of
his face, he had to make a split-second decision. With a powerful
swing of his tail bone, he knocked the attacker down. This experience
taught him the importance of effectively defending himself against
unpleasant fellows and to eliminate threats before they get close to
him ever again.
</p>
</template>
</TimelineItem>
<TimelineItem>
<template #icon><TreeIcon /></template>
<template #headline>Lumberjack</template>
<template #content>
<p>
After leaving his hometown, Viktor started working as a lumberjack.
The club at the end of his tail came very much in handy as a
counterweight for each swing of his axe, allowing him to strike
efficiently and powerfully, felling even the largest trees with
relative ease. His naturally tough scales protected him from splinters
from the felled wood. The long-lasting hard physical work toughened
his body over the years. Before taking the cut logs to the sawmill,
Viktor always took a break to rest in the peace and quiet of the
forest. Although he enjoyed the seclusion of the country life for a
while, he was longing for the sociability of city life again.
</p>
</template>
</TimelineItem>
<TimelineItem>
<template #icon><CarIcon /></template>
<template #headline>Car Mechanic</template>
<template #content>
<p>
Moving into a suburban town, Viktor applied at an auto repair shop,
where he learned the ins and outs of fixing cars. He became really
good at it and enjoyed breathing new mechanical life into broken down
vehicles. However, as time went on, the repair shop faced financial
troubles, as it became increasingly difficult to come by spare parts
as auto makers would only deal them to certified repair partners and
certifications were prohibitively expensive. Viktor had to watch
business slowly deteriorate, as skilled coworkers kept getting laid
off, until the repair shop closed down for good.
</p>
</template>
</TimelineItem>
<TimelineItem>
<template #icon><HelmetSafetyIcon /></template>
<template #headline>Construction Worker</template>
<template #content>
<p>
Having taken a liking in physically demanding work, Viktor took on a
job as a construction worker. Hard hat perched atop his head and belt
tool slung around his waist, he was always ready to lend a hand (or
tail if walls or boulders needed a good teardown). Since he had more
than enough strength training from his previous jobs, he was often
assigned carrying jobs to clear the site and haul building materials.
After work was done, Viktor enjoyed the company of his colleagues over
an after-work beer until late in the evenings. On one of these
evenings, he got a little reckless with a colleague under the veil of
the night and both were caught in the act of fooling around with each
other on the construction site. In the following weeks, the colleagues
kept their distance from Viktor and the evenings together also
dissolved very quickly when he joined them. The continued
ostracization ultimately prompted him to look for something new.
</p>
</template>
</TimelineItem>
<TimelineItem>
<template #icon><IndustryIcon /></template>
<template #headline>Welder</template>
<template #content>
<p>
In his job as a welder, Viktor spent his days working shifts in a
workshop. In addition to special tools and plasma welders, he also
used his powerful tail club to hammer metal parts into shape. He
acquired a wide variety of welding techniques to join or repair metal
structures. He worked with a wide variety of alloys such as steel,
aluminum and titanium. Viktor showed extreme skill in his work with
great precision and attention to detail. However, this dedication was
a thorn in the side of a jealous colleague who saw Viktor as a rival
and schemed against him, sabotaging his work, which ultimately ended
in his termination after a customer sued the welding shop for botching
the job. Viktor never found out who the culprit was, and the loss of
that job still hangs over him.
</p>
</template>
</TimelineItem>
<TimelineItem>
<template #icon><TruckIcon /></template>
<template #headline>Delivery Driver</template>
<template #content>
<p>
When Viktor was strapped for cash, he took on a job in the gig economy
as a driver delivering packages for a large online delivery service.
His previous physically demanding jobs allowed him to haul even bulky
deliveries to their destination with relative ease. If the shipping
center managers hadn't been breathing down his neck constantly, he
might have held this job even longer. But after one of the managers
tried to show him up in front of the whole team, he snapped and broke
their leg with his tail club. Of course, he didn't have to report for
duty the next day.
</p>
</template>
</TimelineItem>
<TimelineItem>
<template #icon><BoxesIcon /></template>
<template #headline>Docks Werehouse Worker</template>
<template #content>
<p>
Currently Viktor works at the docks in the port town he moved to. His
main responsibilities include loading and unloading cargo from ships
and transporting it to and from warehousing. The hustle and bustle of
the port sometimes gets on his nerves. Especially when the crew of
docking ships come ashore again after a long time and make it clear
that they don't have much contact with "landlubbers". Viktor doesn't
get particularly impressed by this and foul mouths them right back if
he gets the impression they're looking for trouble.
</p>
</template>
</TimelineItem>
</TimelineList>
<TimelineList>
<TimelineItem v-for="(job, idx) in ($tm('career.jobs') as Job[])" :key="idx">
<template #icon>
<component :is="jobIcons[idx as number]"></component>
</template>
<template #headline>{{ job.title }}</template>
<template #content>
<p>{{ job.desc }}</p>
</template>
</TimelineItem>
</TimelineList>
</article>
</template>

View file

@ -1,120 +1,73 @@
<script setup lang="ts">
import DataTable from "@/components/DataTable.vue";
import DataTable from '@/components/DataTable.vue'
const dob = new Date("1987-12-08");
const locale = "en-US";
const dob = new Date('1987-12-08')
const locale = 'en-US'
const dateFormat = new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "long",
day: "2-digit",
});
year: 'numeric',
month: 'long',
day: '2-digit'
})
const height = 227;
const weight = 175;
const height = 227
const weight = 175
const toImperial = (cm: number): string => {
const realFeet = (cm * 0.3937) / 12;
const feet = Math.floor(realFeet);
const inches = Math.round((realFeet - feet) * 12);
const realFeet = (cm * 0.3937) / 12
const feet = Math.floor(realFeet)
const inches = Math.round((realFeet - feet) * 12)
return `${feet}'${inches}"`;
};
return `${feet}'${inches}"`
}
const toInch = (cm: number): string => {
return `${Math.round(cm / 2.45)} in`;
};
return `${Math.round(cm / 2.45)} in`
}
const toLbs = (kg: number): number => {
const nearExact = kg / 0.45359237;
const lbs = Math.floor(nearExact);
const nearExact = kg / 0.45359237
const lbs = Math.floor(nearExact)
return lbs;
};
return lbs
}
const heads = ["Key", "Value"];
const heads = ['data.general.heading[0]', 'data.general.heading[1]']
const data = [
["Full Name", "Viktor Kraastav"],
["Date of Birth", dateFormat.format(dob)],
["Sex/Gender", "male ♂️"],
["Height", `${height} cm (${toImperial(height)})`],
["Weight", `${weight} kg (${toLbs(weight)} lbs)`],
];
['data.general.fullName[0]', 'data.general.fullName[1]'],
['data.general.dob[0]', dateFormat.format(dob)],
['data.general.gender[0]', 'data.general.gender[1]'],
['data.general.height[0]', `${height} cm (${toImperial(height)})`],
['data.general.weight[0]', `${weight} kg (${toLbs(weight)} lbs)`]
]
const sexHeads = ["Key", "Value"];
const sexHeads = ['data.sexuality.heading[0]', 'data.sexuality.heading[1]']
const sexData = [
["Sexuality", "Gay"],
["Preferred position", "Bottom"],
];
['data.sexuality.identifiesAs[0]', 'data.sexuality.identifiesAs[1]'],
['data.sexuality.preferredRole[0]', 'data.sexuality.preferredRole[1]']
]
</script>
<template>
<section>
<h1>{{ $route.meta.title }}</h1>
</section>
<article>
<section>
<h1>{{ $t(`${$route.meta.title}`) }}</h1>
</section>
<DataTable :headings="heads" :data="data"></DataTable>
<DataTable :headings="heads" :data="data"></DataTable>
<section>
<h2>Personality</h2>
<p>
Viktor is not a man of many words, his manner of expression is simple and
direct, and sometimes a little blunt. This brash manner doesn't go down
well with everyone and regularly gets him into trouble.
</p>
<section>
<h2>{{ $t('general.personality.heading') }}</h2>
<p v-for="(p, i) in $tm('general.personality.paragraphs')" :key="i">{{ p }}</p>
</section>
<p>
At the same time, this is also an expression of his exuberant
self-confidence. He won't take any crap from anyone and doesn't hesitate
to give someone a piece of his mind. If that results in fisticuffs, he'll
deal with them.
</p>
<section>
<h2>{{ $t('general.sexuality.heading') }}</h2>
</section>
<p>
All this may make him look like a really unpleasant fellow, but he greatly
appreciates the company of people he trusts. He is very selective about
whom he counts among this group of people, though. He appreciates personal
contact, preferably over a few beers at his favorite bar.
</p>
<DataTable :headings="sexHeads" :data="sexData"></DataTable>
<p>
He likes things to be simple, which is why he is not a big fan of
high-tech. He does have a smartphone, but he hates using it as much as he
dislikes the fact he's dependent on owning one.
</p>
</section>
<section>
<h2>Sexuality</h2>
</section>
<DataTable :headings="sexHeads" :data="sexData"></DataTable>
<section>
<p>
As a teenager, Viktor noticed that he felt a bit differently about the
boys in his class. Especially in gym class, his eyes tended to linger
longer on his classmates as they rough-housed in the locker room, as
pubescent boys do. However, he could never quite come to terms with the
idea that he might be a little different from his friends, who were all
beginning to take an interest in girls, unlike him, who was more
interested in his buddies.
</p>
<p>
As he got older, he slowly learned to come to terms with the fact that he
saw more in his buddies than just friends. However, he still kept a low
profile.
</p>
<p>
Until one evening at a party, when one of his friends complained that he
wouldn't get any from his girlfriend. Both of them were already well
intoxicated and Viktor made him the slurred proposal that he would help
him out as his bro. At first his buddy was irritated by the offer, but
eventually agreed. The two snuck into a bedroom at the host's place, where
Viktor would let his buddy unload all the pent-up pressure inside him. He
wouldn't remember much of the party, but what he didn't forget how good it
felt to give himself to another man.
</p>
</section>
<section>
<p v-for="(p, i) in $tm('general.sexuality.paragraphs')" :key="i">{{ p }}</p>
</section>
</article>
</template>

View file

@ -1,17 +1,18 @@
<script setup lang="ts">
import WelcomeHeader from "@/components/WelcomeHeader.vue";
import WelcomeHeader from '@/components/WelcomeHeader.vue'
</script>
<template>
<WelcomeHeader>
<template #main>Viktor Kraastav</template>
<template #sub>Character Reference Page</template>
</WelcomeHeader>
<article>
<WelcomeHeader>
<template #main>{{ $t('welcomeHeader.mainTitle') }}</template>
<template #sub>{{ $t('welcomeHeader.subTitle') }}</template>
</WelcomeHeader>
<section>
<h3>Welcome to Viktor's Ref Page</h3>
<section>
<h3>{{ $t('home.heading') }}</h3>
<p>Here you can learn all about the ankylosaurus named Viktor.</p>
<p>Pick an item from the navigation to dive in!</p>
</section>
<p v-for="(p, i) in $tm('home.paragraphs')" :key="i">{{ p }}</p>
</section>
</article>
</template>

12
tsconfig.app.json Normal file
View file

@ -0,0 +1,12 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["globals.d.ts", "env.d.ts", "src/**/*", "src/**/*.json", "src/**/*.vue", "package.json"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

View file

@ -1,8 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}

View file

@ -1,16 +1,11 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"files": [],
"references": [
{
"path": "./tsconfig.config.json"
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

17
tsconfig.node.json Normal file
View file

@ -0,0 +1,17 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"include": [
"globals.d.ts",
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

23
vite.config.mts Normal file
View file

@ -0,0 +1,23 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import { imagetools } from 'vite-imagetools'
import autoprefixer from 'autoprefixer'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
base: "/viktor/",
plugins: [vue(), imagetools()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
css: {
devSourcemap: true,
postcss: {
plugins: [autoprefixer({})]
}
}
})

View file

@ -1,19 +0,0 @@
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import { imagetools } from "vite-imagetools";
import vue from "@vitejs/plugin-vue";
// https://vitejs.dev/config/
export default defineConfig({
base: "/viktor/",
plugins: [vue(), imagetools()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
css: {
devSourcemap: true,
},
});