add: multilanguage support, Dockerfile for production

This commit is contained in:
Franek 2025-05-16 19:38:37 +02:00
parent 8e48e40a31
commit 3c019ba1cf
19 changed files with 423 additions and 68 deletions

View File

@ -1,11 +1,16 @@
FROM node:lts AS node FROM node:lts AS builder
ENV NODE_ENV production
WORKDIR /app WORKDIR /app
COPY package*.json .
RUN npm install --production COPY ./package*.json ./
RUN npm install
COPY . . COPY . .
RUN npm run build RUN npm run build
FROM nginx:alpine FROM nginx
COPY --from=node /app/build /usr/share/nginx/html COPY --from=builder /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80 443 EXPOSE 80 443
CMD [ "nginx", "-g", "daemon off;" ]

View File

@ -1,11 +1,5 @@
services: services:
www: personal-website:
build: . build: .
container_name: personal-website container_name: personal-website
restart: always restart: always
networks:
- cloudflare
networks:
cloudflare: # adapt networks to your configuration
external: true

9
nginx.conf Normal file
View File

@ -0,0 +1,9 @@
server {
listen 80;
location / {
root /usr/share/nginx/html/;
include /etc/nginx/mime.types;
try_files $uri $uri/ /index.html;
}
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--twemoji" preserveAspectRatio="xMidYMid meet"><path fill="#EEE" d="M32 5H4a4 4 0 0 0-4 4v9h36V9a4 4 0 0 0-4-4z"></path><path fill="#DC143C" d="M0 27a4 4 0 0 0 4 4h28a4 4 0 0 0 4-4v-9H0v9z"></path></svg>

After

Width:  |  Height:  |  Size: 506 B

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<path style="fill:#41479B;" d="M473.655,88.276H38.345C17.167,88.276,0,105.443,0,126.621V385.38
c0,21.177,17.167,38.345,38.345,38.345h435.31c21.177,0,38.345-17.167,38.345-38.345V126.621
C512,105.443,494.833,88.276,473.655,88.276z"/>
<path style="fill:#F5F5F5;" d="M511.469,120.282c-3.022-18.159-18.797-32.007-37.814-32.007h-9.977l-163.54,107.147V88.276h-88.276
v107.147L48.322,88.276h-9.977c-19.017,0-34.792,13.847-37.814,32.007l139.778,91.58H0v88.276h140.309L0.531,391.717
c3.022,18.159,18.797,32.007,37.814,32.007h9.977l163.54-107.147v107.147h88.276V316.577l163.54,107.147h9.977
c19.017,0,34.792-13.847,37.814-32.007l-139.778-91.58H512v-88.276H371.691L511.469,120.282z"/>
<g>
<polygon style="fill:#FF4B55;" points="282.483,88.276 229.517,88.276 229.517,229.517 0,229.517 0,282.483 229.517,282.483
229.517,423.724 282.483,423.724 282.483,282.483 512,282.483 512,229.517 282.483,229.517 "/>
<path style="fill:#FF4B55;" d="M24.793,421.252l186.583-121.114h-32.428L9.224,410.31
C13.377,415.157,18.714,418.955,24.793,421.252z"/>
<path style="fill:#FF4B55;" d="M346.388,300.138H313.96l180.716,117.305c5.057-3.321,9.277-7.807,12.287-13.075L346.388,300.138z"
/>
<path style="fill:#FF4B55;" d="M4.049,109.475l157.73,102.387h32.428L15.475,95.842C10.676,99.414,6.749,104.084,4.049,109.475z"/>
<path style="fill:#FF4B55;" d="M332.566,211.862l170.035-110.375c-4.199-4.831-9.578-8.607-15.699-10.86L300.138,211.862H332.566z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -4 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_503_3486)">
<rect width="28" height="20" rx="2" fill="white"/>
<mask id="mask0_503_3486" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="28" height="20">
<rect width="28" height="20" rx="2" fill="white"/>
</mask>
<g mask="url(#mask0_503_3486)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M28 0H0V1.33333H28V0ZM28 2.66667H0V4H28V2.66667ZM0 5.33333H28V6.66667H0V5.33333ZM28 8H0V9.33333H28V8ZM0 10.6667H28V12H0V10.6667ZM28 13.3333H0V14.6667H28V13.3333ZM0 16H28V17.3333H0V16ZM28 18.6667H0V20H28V18.6667Z" fill="#D02F44"/>
<rect width="12" height="9.33333" fill="#46467F"/>
<g filter="url(#filter0_d_503_3486)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.66665 1.99999C2.66665 2.36818 2.36817 2.66666 1.99998 2.66666C1.63179 2.66666 1.33331 2.36818 1.33331 1.99999C1.33331 1.63181 1.63179 1.33333 1.99998 1.33333C2.36817 1.33333 2.66665 1.63181 2.66665 1.99999ZM5.33331 1.99999C5.33331 2.36818 5.03484 2.66666 4.66665 2.66666C4.29846 2.66666 3.99998 2.36818 3.99998 1.99999C3.99998 1.63181 4.29846 1.33333 4.66665 1.33333C5.03484 1.33333 5.33331 1.63181 5.33331 1.99999ZM7.33331 2.66666C7.7015 2.66666 7.99998 2.36818 7.99998 1.99999C7.99998 1.63181 7.7015 1.33333 7.33331 1.33333C6.96512 1.33333 6.66665 1.63181 6.66665 1.99999C6.66665 2.36818 6.96512 2.66666 7.33331 2.66666ZM10.6666 1.99999C10.6666 2.36818 10.3682 2.66666 9.99998 2.66666C9.63179 2.66666 9.33331 2.36818 9.33331 1.99999C9.33331 1.63181 9.63179 1.33333 9.99998 1.33333C10.3682 1.33333 10.6666 1.63181 10.6666 1.99999ZM3.33331 3.99999C3.7015 3.99999 3.99998 3.70152 3.99998 3.33333C3.99998 2.96514 3.7015 2.66666 3.33331 2.66666C2.96512 2.66666 2.66665 2.96514 2.66665 3.33333C2.66665 3.70152 2.96512 3.99999 3.33331 3.99999ZM6.66665 3.33333C6.66665 3.70152 6.36817 3.99999 5.99998 3.99999C5.63179 3.99999 5.33331 3.70152 5.33331 3.33333C5.33331 2.96514 5.63179 2.66666 5.99998 2.66666C6.36817 2.66666 6.66665 2.96514 6.66665 3.33333ZM8.66665 3.99999C9.03484 3.99999 9.33331 3.70152 9.33331 3.33333C9.33331 2.96514 9.03484 2.66666 8.66665 2.66666C8.29846 2.66666 7.99998 2.96514 7.99998 3.33333C7.99998 3.70152 8.29846 3.99999 8.66665 3.99999ZM10.6666 4.66666C10.6666 5.03485 10.3682 5.33333 9.99998 5.33333C9.63179 5.33333 9.33331 5.03485 9.33331 4.66666C9.33331 4.29847 9.63179 3.99999 9.99998 3.99999C10.3682 3.99999 10.6666 4.29847 10.6666 4.66666ZM7.33331 5.33333C7.7015 5.33333 7.99998 5.03485 7.99998 4.66666C7.99998 4.29847 7.7015 3.99999 7.33331 3.99999C6.96512 3.99999 6.66665 4.29847 6.66665 4.66666C6.66665 5.03485 6.96512 5.33333 7.33331 5.33333ZM5.33331 4.66666C5.33331 5.03485 5.03484 5.33333 4.66665 5.33333C4.29846 5.33333 3.99998 5.03485 3.99998 4.66666C3.99998 4.29847 4.29846 3.99999 4.66665 3.99999C5.03484 3.99999 5.33331 4.29847 5.33331 4.66666ZM1.99998 5.33333C2.36817 5.33333 2.66665 5.03485 2.66665 4.66666C2.66665 4.29847 2.36817 3.99999 1.99998 3.99999C1.63179 3.99999 1.33331 4.29847 1.33331 4.66666C1.33331 5.03485 1.63179 5.33333 1.99998 5.33333ZM3.99998 5.99999C3.99998 6.36819 3.7015 6.66666 3.33331 6.66666C2.96512 6.66666 2.66665 6.36819 2.66665 5.99999C2.66665 5.6318 2.96512 5.33333 3.33331 5.33333C3.7015 5.33333 3.99998 5.6318 3.99998 5.99999ZM5.99998 6.66666C6.36817 6.66666 6.66665 6.36819 6.66665 5.99999C6.66665 5.6318 6.36817 5.33333 5.99998 5.33333C5.63179 5.33333 5.33331 5.6318 5.33331 5.99999C5.33331 6.36819 5.63179 6.66666 5.99998 6.66666ZM9.33331 5.99999C9.33331 6.36819 9.03484 6.66666 8.66665 6.66666C8.29846 6.66666 7.99998 6.36819 7.99998 5.99999C7.99998 5.6318 8.29846 5.33333 8.66665 5.33333C9.03484 5.33333 9.33331 5.6318 9.33331 5.99999ZM9.99998 8C10.3682 8 10.6666 7.70152 10.6666 7.33333C10.6666 6.96514 10.3682 6.66666 9.99998 6.66666C9.63179 6.66666 9.33331 6.96514 9.33331 7.33333C9.33331 7.70152 9.63179 8 9.99998 8ZM7.99998 7.33333C7.99998 7.70152 7.7015 8 7.33331 8C6.96512 8 6.66665 7.70152 6.66665 7.33333C6.66665 6.96514 6.96512 6.66666 7.33331 6.66666C7.7015 6.66666 7.99998 6.96514 7.99998 7.33333ZM4.66665 8C5.03484 8 5.33331 7.70152 5.33331 7.33333C5.33331 6.96514 5.03484 6.66666 4.66665 6.66666C4.29846 6.66666 3.99998 6.96514 3.99998 7.33333C3.99998 7.70152 4.29846 8 4.66665 8ZM2.66665 7.33333C2.66665 7.70152 2.36817 8 1.99998 8C1.63179 8 1.33331 7.70152 1.33331 7.33333C1.33331 6.96514 1.63179 6.66666 1.99998 6.66666C2.36817 6.66666 2.66665 6.96514 2.66665 7.33333Z" fill="url(#paint0_linear_503_3486)"/>
</g>
</g>
</g>
<defs>
<filter id="filter0_d_503_3486" x="1.33331" y="1.33333" width="9.33331" height="7.66667" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_503_3486"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_503_3486" result="shape"/>
</filter>
<linearGradient id="paint0_linear_503_3486" x1="1.33331" y1="1.33333" x2="1.33331" y2="7.99999" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#F0F0F0"/>
</linearGradient>
<clipPath id="clip0_503_3486">
<rect width="28" height="20" rx="2" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -18,6 +18,7 @@ const App: React.FC = () => {
} }
const Container = styled.div` const Container = styled.div`
min-width: 75%;
transition: 0.2s background-color ease-in-out; transition: 0.2s background-color ease-in-out;
background-color: ${({ theme }) => theme.primary}; background-color: ${({ theme }) => theme.primary};
color: ${({ theme }) => theme.text}; color: ${({ theme }) => theme.text};

View File

@ -7,6 +7,7 @@ export type Props = {
link?: string, link?: string,
primary?: boolean, primary?: boolean,
disabled?: boolean, disabled?: boolean,
noeffects?: boolean
} & React.ComponentProps<'div'>; } & React.ComponentProps<'div'>;
const Button: React.FC<Props> = ({ const Button: React.FC<Props> = ({
@ -15,10 +16,16 @@ const Button: React.FC<Props> = ({
link, link,
primary, primary,
children, children,
noeffects = false,
disabled = false, disabled = false,
...props ...props
}) => { }) => {
const item = <ButtonStyled {...props} className={classes({ link, primary, disabled })}> const item = <ButtonStyled {...props} className={classes({
link: link || typeof props.onClick === 'function',
primary,
disabled,
noeffects
})}>
{icon && <img src={icon} alt="Button icon" />} {icon && <img src={icon} alt="Button icon" />}
{content || children} {content || children}
</ButtonStyled>; </ButtonStyled>;
@ -46,7 +53,7 @@ const ButtonStyled = styled.div`
${({ theme }) => theme.type === 'dark' && 'filter: invert();'} ${({ theme }) => theme.type === 'dark' && 'filter: invert();'}
} }
&:hover, &.primary { &:not(.noeffects):hover, &.primary {
background: ${({ theme }) => theme.text}; background: ${({ theme }) => theme.text};
color: ${({ theme }) => theme.secondary}; color: ${({ theme }) => theme.secondary};
box-shadow: 0 0 5px ${({ theme }) => theme.text}; box-shadow: 0 0 5px ${({ theme }) => theme.text};
@ -56,7 +63,7 @@ const ButtonStyled = styled.div`
} }
} }
&.primary:hover { &:not(.noeffects).primary:hover {
background: transparent; background: transparent;
color: ${({ theme }) => theme.text}; color: ${({ theme }) => theme.text};

View File

@ -3,13 +3,17 @@ import React from "react";
import App from "./App"; import App from "./App";
import { ThemeProvider } from "./style/themes"; import { ThemeProvider } from "./style/themes";
import { LanguageProvider } from "./language/context";
import "./style/index.css"; import "./style/index.css";
const root = ReactDOM.createRoot(document.getElementById("root")!!); const root = ReactDOM.createRoot(document.getElementById("root")!!);
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<ThemeProvider> <LanguageProvider>
<App /> <ThemeProvider>
</ThemeProvider> <App />
</ThemeProvider>
</LanguageProvider>
</React.StrictMode> </React.StrictMode>
); );

50
src/language/context.tsx Normal file
View File

@ -0,0 +1,50 @@
import LanguageManager, { Language } from './manager';
import React, {
createContext,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
type LanguageContextProps = {
language: Language;
$: LanguageManager;
setLanguage: (lang: Language) => void;
};
const LanguageContext = createContext<LanguageContextProps>({
language: 'en-US',
setLanguage: () => {},
$: new LanguageManager('en-US'),
});
type LanguageProviderProps = {
children: React.ReactNode;
};
const LanguageProvider: React.FC<LanguageProviderProps> = ({ children }) => {
const [language, setLanguageState] = useState<Language>(() => {
const saved = localStorage.getItem('language');
return (saved as Language) || 'en-US';
});
useEffect(() => {
localStorage.setItem('language', language);
}, [language]);
const setLanguage = (lang: Language) => setLanguageState(lang);
const $ = useMemo(() =>
new LanguageManager(language),
[language]
);
return <LanguageContext.Provider value={{ language, setLanguage, $ }}>
{children}
</LanguageContext.Provider>;
};
const useLanguage = () => useContext(LanguageContext);
export { LanguageProvider, useLanguage };

50
src/language/en-GB.json Normal file
View File

@ -0,0 +1,50 @@
{
"hello": "Hello",
"about": "My name is Franek. I'm {%years%} years, {%months%} months and {%days%} days old.",
"switch_theme": "Switch theme to {%theme%}",
"broad_music_spectrum": "Yeah, I know, I listen to a broad spectrum of music",
"headers": {
"facts": "Some facts about me",
"skills": "I can...",
"music": "My favourite music genres are:",
"bands": "I like these bands/singers:"
},
"skills": [
"play the keyboard/organ",
"tinker with PCs from software to hardware",
"code various apps and scripts"
],
"about_me": [
"introvert",
"IT enthusiast",
"automotive enthusiast"
],
"section": {
"fediverse": "Fediverse...",
"contact": "Contact methods...",
"links": "Some links..."
},
"fediverse": {
"pleroma": "Pleroma",
"pixelfed": "Pixelfed",
"peertube": "PeerTube"
},
"contact": {
"matrix": "Matrix",
"signal": "@sadorowo.66"
},
"link": {
"pgp": "PGP",
"uptime": "Uptime of my services",
"email": "E-mail",
"gitea": "Gitea",
"instagram": "Instagram (deactivated)"
},
"most_used_languages": "Languages that I use the most:",
"favourite_tools": "My favourite tools...",
"languages": {
"pl": "Polish",
"en-US": "United States",
"en-GB": "United Kingdom"
}
}

50
src/language/en-US.json Normal file
View File

@ -0,0 +1,50 @@
{
"hello": "Hello",
"about": "My name is Franek. I'm {%years%} years, {%months%} months and {%days%} days old.",
"switch_theme": "Switch theme to {%theme%}",
"broad_music_spectrum": "Yeah I know, I listen to a broad spectrum of music",
"headers": {
"facts": "Some facts about me",
"skills": "I can...",
"music": "My favourite music genres are:",
"bands": "I like these bands/singers:"
},
"skills": [
"play keyboard/organ",
"mess with PCs from software to hardware",
"code various apps and scripts"
],
"about_me": [
"introvert",
"IT enthusiast",
"automotive enthusiast"
],
"section": {
"fediverse": "Fediverse...",
"contact": "Contact ways...",
"links": "Some links..."
},
"fediverse": {
"pleroma": "Pleroma",
"pixelfed": "Pixelfed",
"peertube": "PeerTube"
},
"contact": {
"matrix": "Matrix",
"signal": "@sadorowo.66"
},
"link": {
"pgp": "PGP",
"uptime": "Uptime of my services",
"email": "E-mail",
"gitea": "Gitea",
"instagram": "Instagram (deactivated)"
},
"most_used_languages": "Languages that I use the most:",
"favourite_tools": "My favourite tools...",
"languages": {
"pl": "Polski",
"en-US": "United States",
"en-GB": "United Kingdom"
}
}

56
src/language/manager.ts Normal file
View File

@ -0,0 +1,56 @@
import enUS from './en-US.json';
import plPL from './pl-PL.json';
import enGB from './en-GB.json';
export type Language = 'pl-PL' | 'en-GB' | 'en-US';
type ToStringable = { toString(): string };
interface Translations {
[key: string]: ToStringable | Translations;
}
const ALL_TRANSLATIONS: Record<Language, Translations> = {
'en-US': enUS,
'pl-PL': plPL,
'en-GB': enGB,
};
export default class LanguageManager {
private translations: Translations = {};
public language: Language;
private readonly supportedLanguages: Language[] = ['pl-PL', 'en-GB', 'en-US'];
private readonly fallbackLanguage: Language = 'en-US';
public constructor(language: string) {
this.language = this.isSupported(language) ? (language as Language) : this.fallbackLanguage;
this.translations = ALL_TRANSLATIONS[this.language];
}
private isSupported(lang: string): lang is Language {
return this.supportedLanguages.includes(lang as Language);
}
private getNestedValue(obj: any, path: string): unknown {
return path
.split('.')
.reduce((acc, key) => (acc && key in acc ? acc[key] : undefined), obj);
}
public raw(key: string): unknown {
return this.getNestedValue(this.translations, key);
}
public tr(key: string, map?: Record<string, ToStringable>): string {
const raw = this.raw(key);
let value = (raw as ToStringable)?.toString?.() ?? `N/A`;
if (map) {
for (const [placeholder, replacement] of Object.entries(map)) {
value = value.replace(new RegExp(`{%${placeholder}%}`, 'g'), replacement.toString());
}
}
return value;
}
}

50
src/language/pl-PL.json Normal file
View File

@ -0,0 +1,50 @@
{
"hello": "Cześć",
"about": "Nazywam się Franek. Mam {%years%} lat, {%months%} miesięcy i {%days%} dni.",
"switch_theme": "Zmień motyw na {%theme%}",
"broad_music_spectrum": "Tak, zdaję sobie sprawę z tego, że słucham szerokiego spektrum muzyki",
"headers": {
"facts": "Kilka faktów o mnie",
"skills": "Potrafię...",
"music": "Moje ulubione gatunki muzyczne to:",
"bands": "Lubię tych wykonawców:"
},
"skills": [
"grać na pianinie/organach",
"majstrować przy komputerach — od oprogramowania po sprzęt",
"tworzyć różne aplikacje i skrypty"
],
"about_me": [
"introwertyk",
"entuzjasta IT",
"pasjonat motoryzacji"
],
"section": {
"fediverse": "Fediverse...",
"contact": "Skontaktuj się ze mną...",
"links": "Kilka linków..."
},
"fediverse": {
"pleroma": "Pleroma",
"pixelfed": "Pixelfed",
"peertube": "PeerTube"
},
"contact": {
"matrix": "Matrix",
"signal": "@sadorowo.66"
},
"link": {
"pgp": "PGP",
"uptime": "Uptime moich usług",
"email": "E-mail",
"gitea": "Gitea",
"instagram": "Instagram (dezaktywowany)"
},
"most_used_languages": "Języki, których najczęściej używam:",
"favourite_tools": "Świetne narzędzia...",
"languages": {
"pl": "Polski",
"en-US": "Stany Zjednoczone",
"en-GB": "Wielka Brytania"
}
}

View File

@ -1,40 +1,53 @@
import styled from "styled-components"; import styled from "styled-components";
import { useTheme } from "../style/themes";
import { calculateAge } from "../utils/math";
import { useTheme } from "../style/themes";
import { useLanguage } from "../language/context";
import { calculateAge } from "../utils/math";
import { asCardStack } from "../components/card-stack"; import { asCardStack } from "../components/card-stack";
const About: React.FC = () => { const About: React.FC = () => {
const { years, months, days } = calculateAge(new Date(2007, 6, 25)); const age = calculateAge(new Date(2007, 6, 25));
const { theme, toggleTheme } = useTheme(); const { theme, toggleTheme } = useTheme();
const { $, setLanguage } = useLanguage();
return <> return <>
<Heading>Hello.</Heading> <Heading>{$.tr("hello")}.</Heading>
<p>My name is Franek. I'm {years} years, {months} months and {days} days old.</p> <p>{$.tr("about", age)}</p>
{asCardStack([ {asCardStack([
{ {
content: `switch theme to ${theme.type === "light" ? "dark" : "light"}`, content: $.tr("switch_theme", { theme: theme.type === "light" ? "dark" : "light" }),
icon: "/icons/" + (theme.type === "light" ? "moon" : "sun") + ".svg", icon: "/icons/" + (theme.type === "light" ? "moon" : "sun") + ".svg",
primary: theme.type === "light", primary: theme.type === "light",
onClick: toggleTheme onClick: toggleTheme
},
{
content: $.tr("languages.pl"),
icon: "/icons/countries/pl.svg",
noeffects: true,
onClick: () => setLanguage('pl-PL')
},
{
content: $.tr("languages.en-US"),
icon: "/icons/countries/us.svg",
noeffects: true,
onClick: () => setLanguage('en-US')
},
{
content: $.tr("languages.en-GB"),
icon: "/icons/countries/uk.svg",
noeffects: true,
onClick: () => setLanguage('en-GB')
} }
])} ])}
<h2>Some facts about me</h2> <h2>{$.tr("headers.facts")}</h2>
{asCardStack([ {asCardStack($.raw("about_me") as string[])}
"introvert",
"IT enthusiast",
"automotive enthusiast"
])}
<h2>I can...</h2> <h2>{$.tr("headers.skills")}</h2>
{asCardStack([ {asCardStack($.raw("skills") as string[])}
"play keyboard/organ",
"mess with PCs from software to hardware",
"code various apps and scripts"
])}
<h2>My favourite music genres are:</h2> <h2>{$.tr("headers.music")}</h2>
{asCardStack([ {asCardStack([
"black metal", "black metal",
"doom metal", "doom metal",
@ -43,7 +56,8 @@ const About: React.FC = () => {
"hard rock" "hard rock"
])} ])}
<h2>I like these bands/singers:</h2> <h2>{$.tr("headers.bands")}</h2>
<p><i>{$.tr("broad_music_spectrum")}</i></p>
{asCardStack([ {asCardStack([
"Mgła", "Mgła",
"Behemoth", "Behemoth",
@ -60,7 +74,6 @@ const About: React.FC = () => {
"ABBA", "ABBA",
"Evanescence" "Evanescence"
])} ])}
<p>yeah I know, I listen to a broad spectrum of music</p>
</>; </>;
} }

View File

@ -1,81 +1,80 @@
import styled from "styled-components";
import { asCardStack } from "../components/card-stack"; import { asCardStack } from "../components/card-stack";
import { useLanguage } from "../language/context";
import LanguageManager from "../language/manager";
const Links: React.FC = () => { const Links: React.FC = () => {
const { $ } = useLanguage();
return <> return <>
<h2>Fediverse...</h2> <h2>{$.tr("section.fediverse")}</h2>
{asCardStack(FEDIVERSE_LINKS)} {asCardStack(FEDIVERSE_LINKS($))}
<h2>Contact ways...</h2> <h2>{$.tr("section.contact")}</h2>
{asCardStack(CONTACT_WAYS)} {asCardStack(CONTACT_WAYS($))}
<h2>Some links...</h2> <h2>{$.tr("section.links")}</h2>
{asCardStack(LINKS)} {asCardStack(LINKS($))}
</>; </>;
} }
const Heading = styled.h1` const FEDIVERSE_LINKS = ($: LanguageManager) => [
font-family: "Pacifico", serif;
`;
const FEDIVERSE_LINKS = [
{ {
content: "Pleroma", content: $.tr("fediverse.pleroma"),
icon: "/icons/fediverse/pleroma.svg", icon: "/icons/fediverse/pleroma.svg",
link: "https://social.sador.me/@boss" link: "https://social.sador.me/@boss"
}, },
{ {
content: "Pixelfed", content: $.tr("fediverse.pixelfed"),
icon: "/icons/fediverse/pixelfed.svg", icon: "/icons/fediverse/pixelfed.svg",
link: "https://pix.sador.me/boss" link: "https://pix.sador.me/boss"
}, },
{ {
content: "PeerTube", content: $.tr("fediverse.peertube"),
icon: "/icons/fediverse/peertube.svg", icon: "/icons/fediverse/peertube.svg",
link: "https://tube.sador.me/c/sador" link: "https://tube.sador.me/c/sador"
} }
].map(link => Object.assign(link, { primary: true })); ].map(link => Object.assign(link, { primary: true }));
const CONTACT_WAYS = [ const CONTACT_WAYS = ($: LanguageManager) => [
{ {
content: "Matrix", content: $.tr("contact.matrix"),
icon: "/icons/matrix.svg", icon: "/icons/matrix.svg",
link: "https://matrix.to/#/@boss:sador.me" link: "https://matrix.to/#/@boss:sador.me"
}, },
{ {
content: "@sadorowo.66", content: $.tr("contact.signal"),
icon: "/icons/signal.svg" icon: "/icons/signal.svg"
} }
]; ];
const LINKS = [ const LINKS = ($: LanguageManager) => [
{ {
content: "PGP", content: $.tr("link.pgp"),
icon: "/icons/pgp.svg", icon: "/icons/pgp.svg",
link: "/pgp.asc", link: "/pgp.asc",
primary: true primary: true
}, },
{ {
content: "Uptime of my services", content: $.tr("link.uptime"),
icon: "/icons/uptime.svg", icon: "/icons/uptime.svg",
link: "https://health.sador.me/status/aio" link: "https://health.sador.me/status/aio"
}, },
{ {
content: "E-mail", content: $.tr("link.email"),
icon: "/icons/email.svg", icon: "/icons/email.svg",
link: "mailto:contact@sador.me?subject=[sador.me] ..." link: "mailto:contact@sador.me?subject=[sador.me] ..."
}, },
{ {
content: "Gitea", content: $.tr("link.gitea"),
icon: "/icons/gitea.svg", icon: "/icons/gitea.svg",
link: "https://git.sador.me" link: "https://git.sador.me"
}, },
{ {
content: "Instagram (deactivated)", content: $.tr("link.instagram"),
icon: "/icons/instagram.svg", icon: "/icons/instagram.svg",
disabled: true, disabled: true,
link: "https://instagram.com/sadorowo" link: "https://instagram.com/sadorowo"
} }
]; ];
export default Links; export default Links;

View File

@ -1,11 +1,14 @@
import { asCardStack } from "../components/card-stack"; import { asCardStack } from "../components/card-stack";
import { useLanguage } from "../language/context";
const Tools: React.FC = () => { const Tools: React.FC = () => {
const { $ } = useLanguage();
return <> return <>
<h2>Languages that I use the most:</h2> <h2>{$.tr("most_used_languages")}</h2>
{asCardStack(LANGUAGES)} {asCardStack(LANGUAGES)}
<h2>My favourite tools...</h2> <h2>{$.tr("favourite_tools")}</h2>
{asCardStack(TOOLS)} {asCardStack(TOOLS)}
</>; </>;
} }

View File

@ -12,4 +12,8 @@ body {
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
text-transform: lowercase; text-transform: lowercase;
}
p {
margin-top: 0;
} }

View File

@ -5,6 +5,7 @@
"module": "commonjs", "module": "commonjs",
"esModuleInterop": true, "esModuleInterop": true,
"noEmit": true, "noEmit": true,
"resolveJsonModule": true,
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"strict": true, "strict": true,