Add dark/lightmode switch
This commit is contained in:
parent
9d18d8c83c
commit
8744b3ce99
18 changed files with 672 additions and 138 deletions
540
package-lock.json
generated
540
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -43,3 +43,5 @@ p {
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
21
src/app.tsx
21
src/app.tsx
|
|
@ -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/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<footer>
|
||||||
<p>
|
<p>
|
||||||
Please register any bugs of this website on <a
|
Don't have a good day, have a great day! 😊
|
||||||
href={"https://github.com/JeCheeseSmith/Octubre/issues"}> GitHub</a>.
|
|
||||||
</p>
|
</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
1
src/assets/matrix.svg
Normal 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 |
|
|
@ -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;
|
||||||
}
|
}
|
||||||
24
src/buttons/Socials.module.css
Normal file
24
src/buttons/Socials.module.css
Normal 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
69
src/buttons/Socials.tsx
Normal 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;
|
||||||
28
src/main.css
28
src/main.css
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
<ThemeToggleButton/>
|
||||||
<App/>
|
<App/>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
19
src/theme/themeSwitch.module.css
Normal file
19
src/theme/themeSwitch.module.css
Normal 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
21
src/theme/themeSwitch.tsx
Normal 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
25
src/theme/useTheme.ts
Normal 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
6
src/vite-env.d.ts
vendored
|
|
@ -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
4
styles.d.ts
vendored
|
|
@ -1,4 +0,0 @@
|
||||||
declare module '*.module.css' {
|
|
||||||
const classes: { [key: string]: string };
|
|
||||||
export default classes;
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue