diff --git a/Dockerfile b/Dockerfile index 34f7d08..18bc33b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,16 @@ -FROM node:lts AS node +FROM node:lts AS builder +ENV NODE_ENV production WORKDIR /app -COPY package*.json . -RUN npm install --production + +COPY ./package*.json ./ + +RUN npm install COPY . . RUN npm run build -FROM nginx:alpine -COPY --from=node /app/build /usr/share/nginx/html +FROM nginx +COPY --from=builder /app/build /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf -EXPOSE 80 443 \ No newline at end of file +EXPOSE 80 443 +CMD [ "nginx", "-g", "daemon off;" ] \ No newline at end of file diff --git a/compose.yaml b/compose.yaml index b12f65a..69722d1 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,11 +1,5 @@ services: - www: + personal-website: build: . container_name: personal-website - restart: always - networks: - - cloudflare - -networks: - cloudflare: # adapt networks to your configuration - external: true \ No newline at end of file + restart: always \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..fd5aefe --- /dev/null +++ b/nginx.conf @@ -0,0 +1,9 @@ +server { + listen 80; + + location / { + root /usr/share/nginx/html/; + include /etc/nginx/mime.types; + try_files $uri $uri/ /index.html; + } +} \ No newline at end of file diff --git a/public/icons/countries/pl.svg b/public/icons/countries/pl.svg new file mode 100644 index 0000000..2fb2b3f --- /dev/null +++ b/public/icons/countries/pl.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/public/icons/countries/uk.svg b/public/icons/countries/uk.svg new file mode 100644 index 0000000..817c459 --- /dev/null +++ b/public/icons/countries/uk.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/icons/countries/us.svg b/public/icons/countries/us.svg new file mode 100644 index 0000000..f5a7a01 --- /dev/null +++ b/public/icons/countries/us.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index bb1614c..b327666 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,7 @@ const App: React.FC = () => { } const Container = styled.div` + min-width: 75%; transition: 0.2s background-color ease-in-out; background-color: ${({ theme }) => theme.primary}; color: ${({ theme }) => theme.text}; diff --git a/src/components/button.tsx b/src/components/button.tsx index c66ff3b..df7cd4f 100644 --- a/src/components/button.tsx +++ b/src/components/button.tsx @@ -7,6 +7,7 @@ export type Props = { link?: string, primary?: boolean, disabled?: boolean, + noeffects?: boolean } & React.ComponentProps<'div'>; const Button: React.FC = ({ @@ -15,10 +16,16 @@ const Button: React.FC = ({ link, primary, children, + noeffects = false, disabled = false, ...props }) => { - const item = + const item = {icon && Button icon} {content || children} ; @@ -46,7 +53,7 @@ const ButtonStyled = styled.div` ${({ theme }) => theme.type === 'dark' && 'filter: invert();'} } - &:hover, &.primary { + &:not(.noeffects):hover, &.primary { background: ${({ theme }) => theme.text}; color: ${({ theme }) => theme.secondary}; box-shadow: 0 0 5px ${({ theme }) => theme.text}; @@ -56,7 +63,7 @@ const ButtonStyled = styled.div` } } - &.primary:hover { + &:not(.noeffects).primary:hover { background: transparent; color: ${({ theme }) => theme.text}; diff --git a/src/index.tsx b/src/index.tsx index 3f6e14e..fc9d9e3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,13 +3,17 @@ import React from "react"; import App from "./App"; import { ThemeProvider } from "./style/themes"; +import { LanguageProvider } from "./language/context"; + import "./style/index.css"; const root = ReactDOM.createRoot(document.getElementById("root")!!); root.render( - - - + + + + + ); diff --git a/src/language/context.tsx b/src/language/context.tsx new file mode 100644 index 0000000..1105dd3 --- /dev/null +++ b/src/language/context.tsx @@ -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({ + language: 'en-US', + setLanguage: () => {}, + $: new LanguageManager('en-US'), +}); + +type LanguageProviderProps = { + children: React.ReactNode; +}; + +const LanguageProvider: React.FC = ({ children }) => { + const [language, setLanguageState] = useState(() => { + 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 + {children} + ; +}; + +const useLanguage = () => useContext(LanguageContext); + +export { LanguageProvider, useLanguage }; \ No newline at end of file diff --git a/src/language/en-GB.json b/src/language/en-GB.json new file mode 100644 index 0000000..c9254af --- /dev/null +++ b/src/language/en-GB.json @@ -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" + } +} \ No newline at end of file diff --git a/src/language/en-US.json b/src/language/en-US.json new file mode 100644 index 0000000..a3d2f9e --- /dev/null +++ b/src/language/en-US.json @@ -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" + } +} \ No newline at end of file diff --git a/src/language/manager.ts b/src/language/manager.ts new file mode 100644 index 0000000..c986fa2 --- /dev/null +++ b/src/language/manager.ts @@ -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 = { + '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 { + 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; + } +} \ No newline at end of file diff --git a/src/language/pl-PL.json b/src/language/pl-PL.json new file mode 100644 index 0000000..2ed8bcb --- /dev/null +++ b/src/language/pl-PL.json @@ -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" + } +} \ No newline at end of file diff --git a/src/sections/about.tsx b/src/sections/about.tsx index b4d5840..5743ed6 100644 --- a/src/sections/about.tsx +++ b/src/sections/about.tsx @@ -1,40 +1,53 @@ 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"; 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 { $, setLanguage } = useLanguage(); return <> - Hello. -

My name is Franek. I'm {years} years, {months} months and {days} days old.

+ {$.tr("hello")}. +

{$.tr("about", age)}

{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", primary: theme.type === "light", 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') } ])} -

Some facts about me

- {asCardStack([ - "introvert", - "IT enthusiast", - "automotive enthusiast" - ])} +

{$.tr("headers.facts")}

+ {asCardStack($.raw("about_me") as string[])} -

I can...

- {asCardStack([ - "play keyboard/organ", - "mess with PCs from software to hardware", - "code various apps and scripts" - ])} +

{$.tr("headers.skills")}

+ {asCardStack($.raw("skills") as string[])} -

My favourite music genres are:

+

{$.tr("headers.music")}

{asCardStack([ "black metal", "doom metal", @@ -43,7 +56,8 @@ const About: React.FC = () => { "hard rock" ])} -

I like these bands/singers:

+

{$.tr("headers.bands")}

+

{$.tr("broad_music_spectrum")}

{asCardStack([ "Mgła", "Behemoth", @@ -60,7 +74,6 @@ const About: React.FC = () => { "ABBA", "Evanescence" ])} -

yeah I know, I listen to a broad spectrum of music

; } diff --git a/src/sections/links.tsx b/src/sections/links.tsx index a0e7810..41f2eb6 100644 --- a/src/sections/links.tsx +++ b/src/sections/links.tsx @@ -1,81 +1,80 @@ -import styled from "styled-components"; import { asCardStack } from "../components/card-stack"; +import { useLanguage } from "../language/context"; +import LanguageManager from "../language/manager"; const Links: React.FC = () => { + const { $ } = useLanguage(); + return <> -

Fediverse...

- {asCardStack(FEDIVERSE_LINKS)} +

{$.tr("section.fediverse")}

+ {asCardStack(FEDIVERSE_LINKS($))} -

Contact ways...

- {asCardStack(CONTACT_WAYS)} +

{$.tr("section.contact")}

+ {asCardStack(CONTACT_WAYS($))} -

Some links...

- {asCardStack(LINKS)} +

{$.tr("section.links")}

+ {asCardStack(LINKS($))} ; } -const Heading = styled.h1` - font-family: "Pacifico", serif; -`; - -const FEDIVERSE_LINKS = [ +const FEDIVERSE_LINKS = ($: LanguageManager) => [ { - content: "Pleroma", + content: $.tr("fediverse.pleroma"), icon: "/icons/fediverse/pleroma.svg", link: "https://social.sador.me/@boss" }, { - content: "Pixelfed", + content: $.tr("fediverse.pixelfed"), icon: "/icons/fediverse/pixelfed.svg", link: "https://pix.sador.me/boss" }, { - content: "PeerTube", + content: $.tr("fediverse.peertube"), icon: "/icons/fediverse/peertube.svg", link: "https://tube.sador.me/c/sador" } ].map(link => Object.assign(link, { primary: true })); -const CONTACT_WAYS = [ +const CONTACT_WAYS = ($: LanguageManager) => [ { - content: "Matrix", + content: $.tr("contact.matrix"), icon: "/icons/matrix.svg", link: "https://matrix.to/#/@boss:sador.me" }, { - content: "@sadorowo.66", + content: $.tr("contact.signal"), icon: "/icons/signal.svg" } ]; -const LINKS = [ +const LINKS = ($: LanguageManager) => [ { - content: "PGP", + content: $.tr("link.pgp"), icon: "/icons/pgp.svg", link: "/pgp.asc", primary: true }, { - content: "Uptime of my services", + content: $.tr("link.uptime"), icon: "/icons/uptime.svg", link: "https://health.sador.me/status/aio" }, { - content: "E-mail", + content: $.tr("link.email"), icon: "/icons/email.svg", link: "mailto:contact@sador.me?subject=[sador.me] ..." }, { - content: "Gitea", + content: $.tr("link.gitea"), icon: "/icons/gitea.svg", link: "https://git.sador.me" }, { - content: "Instagram (deactivated)", + content: $.tr("link.instagram"), icon: "/icons/instagram.svg", disabled: true, link: "https://instagram.com/sadorowo" } ]; -export default Links; +export default Links; \ No newline at end of file diff --git a/src/sections/tools.tsx b/src/sections/tools.tsx index 3dcb961..0e11167 100644 --- a/src/sections/tools.tsx +++ b/src/sections/tools.tsx @@ -1,11 +1,14 @@ import { asCardStack } from "../components/card-stack"; +import { useLanguage } from "../language/context"; const Tools: React.FC = () => { + const { $ } = useLanguage(); + return <> -

Languages that I use the most:

+

{$.tr("most_used_languages")}

{asCardStack(LANGUAGES)} -

My favourite tools...

+

{$.tr("favourite_tools")}

{asCardStack(TOOLS)} ; } diff --git a/src/style/index.css b/src/style/index.css index da28eba..8fc8be9 100644 --- a/src/style/index.css +++ b/src/style/index.css @@ -12,4 +12,8 @@ body { h1, h2, h3, h4, h5, h6 { text-transform: lowercase; +} + +p { + margin-top: 0; } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 7ed3b84..95e69d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "module": "commonjs", "esModuleInterop": true, "noEmit": true, + "resolveJsonModule": true, "allowImportingTsExtensions": true, "forceConsistentCasingInFileNames": true, "strict": true,