Add dark/lightmode switch

This commit is contained in:
Kars van Velzen 2025-08-21 16:11:08 +02:00
parent 9d18d8c83c
commit 8744b3ce99
18 changed files with 672 additions and 138 deletions

540
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,9 +10,12 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@fortawesome/free-brands-svg-icons": "^7.0.0",
"@fortawesome/free-solid-svg-icons": "^7.0.0",
"@fortawesome/react-fontawesome": "^0.2.3", "@fortawesome/react-fontawesome": "^0.2.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0",
"vite-plugin-svgr": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.33.0", "@eslint/js": "^9.33.0",

View file

@ -43,3 +43,5 @@ p {
justify-content: space-evenly; justify-content: space-evenly;
width: 50%; width: 50%;
} }

View file

@ -1,5 +1,6 @@
import './app.css'; import './app.css';
import ButtonGroup from "./buttonGroup.tsx"; import ButtonGroup from "./buttons/buttonGroup.tsx";
import SocialButtons from "./buttons/Socials.tsx";
function App() { function App() {
return ( return (
@ -15,23 +16,27 @@ function App() {
<div className="buttonGroup"> <div className="buttonGroup">
<ButtonGroup <ButtonGroup
buttons={[["Home", "https://home.octubre.be", "Home Automation Platform using HomeAssistant"], ["Cloud", "https://cloud.octubre.be", "Personal Office Infrastructure using Nextcloud"], ["Media", "https://media.octubre.be", "Multimedia management solution using Immich"], ["Blog", "https://blog.octubre.be", "Blog about this hobby project and it's development roadmap"], ["Me", "https://me.octubre.be", "My portfolio page"], ["Chat", "https://chat.octubre.be", "Federated chat instance using Matrix & Element"], ["Log", "https://log.octubre.be", "Update log linked to the blog - Under construction"], ["Status", "https://status.octubre.be", "External status page of the different Octubre services"], ["Git", "https://git.octubre.be", "Forgejo based gitserver, alternative for my Github account"],["Fandom", "https://fandom.octubre.be", 'Website dedicated to fanart & creations about things I like'], ["Archive", "https://archive.octubre.be", "Separate website to host old, no longer maintained packages & websites"], ["Dev", "https://dev.octubre.be", "Development subdomain for alfa & beta releases"]]}/> buttons={[["Home", "https://home.octubre.be", "Home Automation Platform using Home Assistant"], ["Cloud", "https://cloud.octubre.be", "Personal Office Infrastructure using Nextcloud"], ["Media", "https://media.octubre.be", "Multimedia management solution using Immich"], ["Blog", "https://blog.octubre.be", "Blog about this hobby project and it's development roadmap"], ["Me", "https://me.octubre.be", "My portfolio page including Github Projects"], ["Chat", "https://chat.octubre.be", "Federated chat instance using Matrix & Element"], ["Log 👷", "https://log.octubre.be", "Update log linked to the blog - Under construction"], ["Status", "https://status.octubre.be", "External status page of the different Octubre services"], ["Git", "https://git.octubre.be", "Forgejo based gitserver, alternative for my Github account"],["Fandom", "https://fandom.octubre.be", 'Website dedicated to fanart & creations about things I like'], ["Archive", "https://archive.octubre.be", "Separate website to host old, no longer maintained packages & websites"], ["Dev", "https://dev.octubre.be", "Development subdomain for alfa & beta releases"]]}/>
{/* ["#Soon", "", "Pyros? - Under construction"] */} {/* ["#Soon", "", "Pyros? - Under construction"] */}
</div> </div>
{/*test*/}
<h3>Contact</h3> <h3>Contact</h3>
<p> <p>
Reach out to me using <a Feel free to reach out to me! 📬
href={"https://karsvanvelzen.be"}> this link!</a>
</p> </p>
<SocialButtons/>
<p>
Please register any bugs of this website on <a
href={"https://github.com/JeCheeseSmith/Octubre/issues"}> GitHub</a>. <footer>
</p> <p>
Don't have a good day, have a great day! 😊
</p>
Please register any bugs of this website on <a href={"https://github.com/JeCheeseSmith/Octubre/issues"}> GitHub</a>.
</footer>
<br/>
</> </>
) )
} }

1
src/assets/matrix.svg Normal file
View file

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Matrix</title><path d="M.632.55v22.9H2.28V24H0V0h2.28v.55zm7.043 7.26v1.157h.033c.309-.443.683-.784 1.117-1.024.433-.245.936-.365 1.5-.365.54 0 1.033.107 1.481.314.448.208.785.582 1.02 1.108.254-.374.6-.706 1.034-.992.434-.287.95-.43 1.546-.43.453 0 .872.056 1.26.167.388.11.716.286.993.53.276.245.489.559.646.951.152.392.23.863.23 1.417v5.728h-2.349V11.52c0-.286-.01-.559-.032-.812a1.755 1.755 0 0 0-.18-.66 1.106 1.106 0 0 0-.438-.448c-.194-.11-.457-.166-.785-.166-.332 0-.6.064-.803.189a1.38 1.38 0 0 0-.48.499 1.946 1.946 0 0 0-.231.696 5.56 5.56 0 0 0-.06.785v4.768h-2.35v-4.8c0-.254-.004-.503-.018-.752a2.074 2.074 0 0 0-.143-.688 1.052 1.052 0 0 0-.415-.503c-.194-.125-.476-.19-.854-.19-.111 0-.259.024-.439.074-.18.051-.36.143-.53.282-.171.138-.319.337-.439.595-.12.259-.18.6-.18 1.02v4.966H5.46V7.81zm15.693 15.64V.55H21.72V0H24v24h-2.28v-.55z"/></svg>

After

Width:  |  Height:  |  Size: 939 B

View file

@ -1,11 +1,3 @@
:root {
--foreground-color: rgba(255, 255, 255, 0.87);
--background-color: #242424;
--on-background-color-primary: #1a1a1a;
--on-background-color-secondary: rgb(64, 64, 64);
--primary-color: rgb(255, 102, 0);
/*--secundary-color: #c084fc;*/
}
.controlButtonContainer{ .controlButtonContainer{
position: relative; /* Needed for absolute positioning of the tooltip */ position: relative; /* Needed for absolute positioning of the tooltip */
display: inline-block; display: inline-block;
@ -13,7 +5,7 @@
.controlButton { .controlButton {
border-radius: 8px; border-radius: 8px;
border: 1px solid transparent; border: 2px solid var(--border-color);
margin: 0.5vw; margin: 0.5vw;
padding: 10px 10px; padding: 10px 10px;
@ -25,6 +17,7 @@
font-weight: 500; font-weight: 500;
font-family: inherit; font-family: inherit;
color: var(--foreground-color);
background-color: var(--on-background-color-primary); background-color: var(--on-background-color-primary);
cursor: pointer; cursor: pointer;
transition: border-color 0.25s; transition: border-color 0.25s;
@ -42,11 +35,12 @@
left: 10%; left: 10%;
top: 100%; top: 100%;
background-color: var(--on-background-color-secondary); background-color: var(--on-background-color-secondary);
color: white; color: var(--foreground-color);
padding: 5px 10px; padding: 5px 10px;
border-radius: 5px; border-radius: 5px;
white-space: nowrap; white-space: nowrap;
z-index: 1000; /* Ensure it appears above other elements */ z-index: 1000; /* Ensure it appears above other elements */
border: 2px solid var(--border-color);
} }
.controlButtonNoteRight { .controlButtonNoteRight {
@ -54,7 +48,7 @@
left: 100%; left: 100%;
top: 0; top: 0;
background-color: var(--on-background-color-secondary); background-color: var(--on-background-color-secondary);
color: white; color: var(--foreground-color);
padding: 5px 10px; padding: 5px 10px;
border-radius: 5px; border-radius: 5px;
white-space: nowrap; white-space: nowrap;
@ -68,7 +62,7 @@
align-items: center; align-items: center;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
border-bottom: 1px solid rgba(255, 255, 255, 0.2); border-bottom: 1px solid var(--on-background-color-3);
padding-bottom: 4px; padding-bottom: 4px;
} }
@ -80,7 +74,7 @@
/* Shortcut on the Right */ /* Shortcut on the Right */
.tooltipShortcut { .tooltipShortcut {
background: rgba(255, 255, 255, 0.2); background: var(--on-background-color-3);
padding: 2px 6px; padding: 2px 6px;
border-radius: 4px; border-radius: 4px;
font-size: 12px; font-size: 12px;
@ -90,6 +84,6 @@
.tooltipNote { .tooltipNote {
font-size: 12px; font-size: 12px;
margin-top: 4px; margin-top: 4px;
color: #ddd; color: var(--foreground-color);
font-family: inherit; font-family: inherit;
} }

View file

@ -0,0 +1,24 @@
.socialContainer {
display: inline-block;
gap: 1rem;
justify-content: center;
margin-bottom: 3vh;
}
.socialButton {
color: rgb(64, 64, 64);
transition: color 0.3s;
}
.socialButton:hover {
color: rgb(255, 102, 0); /* = #FF6600 */
}
.matrixIcon {
margin-top: 2vh;
filter: invert(14%) sepia(0%) saturate(4178%) hue-rotate(139deg) brightness(125%) contrast(72%);
}
.matrixIcon:hover {
filter: invert(49%) sepia(84%) saturate(3557%) hue-rotate(0deg) brightness(102%) contrast(105%); /*Thanks to https://codepen.io/sosuke/pen/Pjoqqp for */
}

69
src/buttons/Socials.tsx Normal file
View file

@ -0,0 +1,69 @@
import React from 'react';
import styles from './Socials.module.css'; // Optional CSS
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faEnvelope,
} from '@fortawesome/free-solid-svg-icons';
import {
faLinkedin,
faGithub,
} from '@fortawesome/free-brands-svg-icons';
interface SocialButton {
icon: any;
label: string;
href: string;
}
const socialButtons: SocialButton[] = [
{
icon: faEnvelope,
label: 'Email',
href: 'mailto:kars.van.velzen@gmail.com',
},
{
icon: faLinkedin,
label: 'LinkedIn',
href: 'https://www.linkedin.com/in/kars-van-velzen',
},
{
icon: faGithub,
label: 'GitHub',
href: 'https://github.com/JeCheeseSmith',
},
// {
// icon: "custom-matrix",
// label: 'Matrix',
// href: '',
// },
];
const SocialButtons: React.FC = () => {
return (
<div className={styles.socialContainer}>
{socialButtons.map((btn) => (
<a
key={btn.label}
href={btn.href}
target="_blank"
rel="noopener noreferrer"
aria-label={btn.label}
className={styles.socialButton}
>
<FontAwesomeIcon icon={btn.icon} size="2xl"/>
</a>
))}
<a key={"Matrix"}
href={"https://matrix.to/#/@jecheesesmith:octubre.be"}
target="_blank"
rel="noopener noreferrer"
aria-label={"Matrix"}
className={styles.socialButton}>
<img src="src/assets/matrix.svg" className={styles.matrixIcon} alt={"Matrix Logo"}/>
</a>
</div>
);
};
export default SocialButtons;

View file

@ -2,11 +2,35 @@
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
color-scheme: light dark; color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none; font-synthesis: none;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.light {
background-color: white;
color: #000000;
--foreground-color: #000000;
--background-color: white;
--on-background-color-primary: white;
--on-background-color-secondary: #f9f9f9;
--on-background-color-3: #000000;
--primary-color: rgb(255, 102, 0);
--border-color: rgb(64, 64, 64);
}
.dark {
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
--foreground-color: rgba(255, 255, 255, 0.87);
--background-color: #242424;
--on-background-color-primary: #1a1a1a;
--on-background-color-secondary: rgb(64, 64, 64);
--on-background-color-3: rgba(255, 255, 255, 0.2);
--primary-color: rgb(255, 102, 0);
--border-color: transparent;
}

View file

@ -2,9 +2,11 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import App from './app.tsx' import App from './app.tsx'
import './main.css' import './main.css'
import ThemeToggleButton from "./theme/themeSwitch.tsx";
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<App /> <ThemeToggleButton/>
<App/>
</StrictMode>, </StrictMode>,
) )

View file

@ -0,0 +1,19 @@
.button {
border: 2px solid var(--border-color);
color: var(--foreground-color);
background-color: var(--on-background-color-primary);
padding: 0.5rem 1rem;
font-size: 1rem;
cursor: pointer;
border-radius: 6px;
transition: background 0.3s, color 0.3s;
top: 1vh;
right: 1vw;
position: absolute;
}
.button:hover {
border-color: var(--primary-color);
}

21
src/theme/themeSwitch.tsx Normal file
View file

@ -0,0 +1,21 @@
import { useTheme } from './useTheme.ts';
import styles from './themeSwitch.module.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons';
const ThemeToggleButton = () => {
const [theme, toggleTheme] = useTheme();
return (
<button className={styles.button} onClick={toggleTheme} aria-label="Toggle dark mode">
{theme === 'dark' ? (
<FontAwesomeIcon icon={faSun}/>
) : (
<FontAwesomeIcon icon={faMoon}/>
)}
</button>
);
};
export default ThemeToggleButton;

25
src/theme/useTheme.ts Normal file
View file

@ -0,0 +1,25 @@
import { useEffect, useState } from 'react';
type Theme = 'light' | 'dark';
export const useTheme = (): [Theme, () => void] => {
const getInitialTheme = (): Theme => {
const stored = localStorage.getItem('theme');
if (stored === 'light' || stored === 'dark') return stored;
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
return prefersDark ? 'dark' : 'light';
};
const [theme, setTheme] = useState<Theme>(getInitialTheme);
useEffect(() => {
document.body.classList.remove('light', 'dark');
document.body.classList.add(theme);
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
return [theme, toggleTheme];
};

6
src/vite-env.d.ts vendored
View file

@ -1 +1,7 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare module '*.svg' {
import * as React from 'react';
export const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
const src: string;
export default src;
}

4
styles.d.ts vendored
View file

@ -1,4 +0,0 @@
declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}

View file

@ -1,7 +1,14 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import svgr from 'vite-plugin-svgr';
// https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [
}) react(),
svgr({
svgrOptions: {
icon: true,
}
})
]
});