Menüleisten Prototyp

This commit is contained in:
Sascha 2025-08-13 14:47:55 +02:00
parent 0e64d2707c
commit fec3e62a28
11 changed files with 298 additions and 240 deletions

View File

@ -1,69 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

View File

@ -1,46 +1,20 @@
import { useState } from 'react' import { useState, useEffect } from 'react';
import reactLogo from '../assets/react.svg' import '../scss/App.scss';
import viteLogo from '../assets/vite.svg' import Navbar from '../components/layout/Navigation';
import '../scss/App.scss'
function App() { function PortfolioApp() {
const [count, setCount] = useState(3) const [theme, setTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
return ( return (
<> <>
<div className='navbar'> <Navbar theme={theme} setTheme={setTheme} />
<div className="navbar__name">John Doe</div> <h1>Aktuelles Theme: {theme} </h1>
<div className="navbar__container">
<button /*onClick={() => scrollToSection('about')}*/ className="navbar__container__button">About</button>
<button /*onClick={() => scrollToSection('about')}*/ className="navbar__container__button">Services</button>
<button /*onClick={() => scrollToSection('about')}*/ className="navbar__container__button">Skills</button>
<button /*onClick={() => scrollToSection('about')}*/ className="navbar__container__button">Certifications</button>
<button /*onClick={() => scrollToSection('about')}*/ className="navbar__container__button">Projects</button>
<button /*onClick={() => scrollToSection('about')}*/ className="navbar__container__button">Contact</button>
</div>
</div>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</> </>
) );
} }
export default App export default PortfolioApp;

View File

@ -0,0 +1,16 @@
type Props = {
theme: 'light' | 'dark';
setTheme: React.Dispatch<React.SetStateAction<'light' | 'dark'>>;
};
export default function ThemeToggle({ theme, setTheme }: Props) {
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<button className="navbar__container__button" onClick={toggleTheme}>
{theme === 'light' ? '💡' : '💤'}
</button>
);
}

View File

@ -0,0 +1,86 @@
import { useState } from 'react';
import ThemeToggle from '../ThemeToggle';
type Props = {
theme: 'light' | 'dark';
setTheme: React.Dispatch<React.SetStateAction<'light' | 'dark'>>;
};
export default function Navbar({ theme, setTheme }: Props) {
const [menuOpen, setMenuOpen] = useState(false);
return (
// TODO Centralize strings, create buttons via loops
<div className="navbar">
<div className="navbar__name">Sascha Bach</div>
<div className="navbar__container">
<button
/*onClick={() => scrollToSection('about')}*/ className="navbar__container__button"
>
About
</button>
<button
/*onClick={() => scrollToSection('about')}*/ className="navbar__container__button"
>
Services
</button>
<button
/*onClick={() => scrollToSection('about')}*/ className="navbar__container__button"
>
Skills
</button>
<button
/*onClick={() => scrollToSection('about')}*/ className="navbar__container__button"
>
Certifications
</button>
<button
/*onClick={() => scrollToSection('about')}*/ className="navbar__container__button"
>
Projects
</button>
<button
/*onClick={() => scrollToSection('about')}*/ className="navbar__container__button"
>
Contact
</button>
<ThemeToggle theme={theme} setTheme={setTheme} />
{/* Burger Button für Mobile */}
<div
className={`navbar__burger-button ${menuOpen ? 'active' : ''}`}
aria-label="Menü öffnen"
onClick={() => setMenuOpen(!menuOpen)}
>
<span className="navbar__burger-button__item"> </span>
<span className="navbar__burger-button__item"> </span>
<span className="navbar__burger-button__item"> </span>
</div>
{/* Optional: Mobile Menu Dropdown */}
{menuOpen && (
<div className="navbar__mobile-menu">
<button>About</button>
<button>Services</button>
<button>Skills</button>
<button>Certifications</button>
<button>Projects</button>
<button>Contact</button>
</div>
)}
</div>
</div>
);
}
/*<span className="navbar__container__burger-button__item">About</span>
<span className="navbar__container__burger-button__item">
Services
</span>
<span className="navbar__container__burger-button__item">Skills</span>
<span className="navbar__container__burger-button__item">
Certifications
</span>
<span className="navbar__container__burger-button__item">
Projects
</span>
<span className="navbar__container__burger-button__item">
Contact
</span>*/

View File

@ -1,10 +1,9 @@
import { StrictMode } from 'react' import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client';
import './scss/index.scss' import PortfolioApp from './app/page.tsx';
import App from './app/page.tsx'
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<App /> <PortfolioApp />
</StrictMode>, </StrictMode>
) );

View File

@ -1,63 +1,17 @@
#root { @use 'themes' as t;
width: 100% @use 'topbar';
// Standard Theme (Light)
:root {
@include t.theme-vars(t.$light-theme);
} }
.navbar { // Dark Theme aktiv, wenn `data-theme="dark"` am html oder body
display: flex; [data-theme='dark'] {
justify-content: space-between; @include t.theme-vars(t.$dark-theme);
align-items: center;
padding: 1em;
background-color: #fff;
color: black;
&__container {
&__button {
font-feature-settings: inherit;
font-variation-settings: inherit;
letter-spacing: inherit;
color: inherit;
border-radius: 0;
background-color: transparent;
opacity: 1;
}
}
} }
.logo { body {
height: 6em; background-color: var(--color-background);
padding: 1.5em; color: var(--color-text);
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
/*
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
} */
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
} }

1
src/scss/globals.scss Normal file
View File

@ -0,0 +1 @@
@forward 'variables';

View File

@ -1,67 +0,0 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #999;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
color: red;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

22
src/scss/themes.scss Normal file
View File

@ -0,0 +1,22 @@
@use 'sass:map';
// Light Theme Farben
$light-theme: (
primary: #000000,
background: #ffffff,
text: #000000,
);
// Dark Theme Farben
$dark-theme: (
primary: #f59e0b,
background: #1f2937,
text: #f9fafb,
);
// Mixin: SCSS-Map CSS-Variablen schreiben
@mixin theme-vars($theme) {
--color-primary: #{map.get($theme, primary)};
--color-background: #{map.get($theme, background)};
--color-text: #{map.get($theme, text)};
}

140
src/scss/topbar.scss Normal file
View File

@ -0,0 +1,140 @@
@use 'globals';
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1em;
background: var(--color-background);
color: var(--color-text);
height: 30px;
&__name {
font-weight: bold;
font-size: 1.25rem;
}
// Desktop Bar
&__container {
&__button {
font-feature-settings: inherit;
font-variation-settings: inherit;
letter-spacing: inherit;
color: inherit;
border-radius: 0;
border-color: unset;
background-color: transparent;
opacity: 1;
padding: 0.6rem 1.2rem;
border: none;
border-radius: 8px;
cursor: pointer;
transition: box-shadow 0.2s ease, transform 0.2s ease;
&:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
&:active {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transform: translateY(0);
}
}
}
// Burger-Button für mobile Geräte
&__burger-button {
flex-direction: column;
justify-content: space-between;
width: 20px;
height: 17px;
background: none;
border: none;
cursor: pointer;
padding: 0;
&__item {
display: block;
height: 4px;
width: 100%;
background-color: var(--color-text);
border-radius: 2px;
transition: all 0.3s ease;
margin-top: 2px;
}
&:hover .navbar__container__burger-button__item {
background-color: var(--color-primary);
}
&.active {
.navbar__container__burger-button__item:nth-child(1) {
transform: rotate(45deg) translate(5px, 5px);
}
.navbar__container__burger-button__item:nth-child(2) {
opacity: 0;
}
.navbar__container__burger-button__item:nth-child(3) {
transform: rotate(-45deg) translate(5px, -5px);
}
.navbar__container__burger-button__item:nth-child(n + 4) {
opacity: 0;
}
}
}
/* Mobile Menu Dropdown */
.navbar__mobile-menu {
display: flex;
flex-direction: column;
position: absolute;
right: 0;
background: var(--color-background);
border: 1px solid var(--color-primary);
border-radius: 8px;
padding: 0.5em;
gap: 0.5em;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 10;
button {
background: none;
border: none;
color: var(--color-text);
padding: 0.5em 1em;
text-align: right;
cursor: pointer;
border-radius: 4px;
transition: background 0.2s;
&:hover {
background: rgba(0, 0, 0, 0.05);
}
}
}
}
// Mobile-Only Styles
@media (max-width: globals.$mobile-breakpoint) {
.navbar__container__button {
display: none;
}
}
@media (min-width: globals.$desktop-breakpoint) {
.navbar__burger-button {
display: none;
}
}
.theme-toggle {
background: none;
border: none;
font-size: 1rem;
cursor: pointer;
transition: transform 0.2s ease;
width: 20px;
&:hover {
transform: scale(1.2);
}
}

2
src/scss/variables.scss Normal file
View File

@ -0,0 +1,2 @@
$mobile-breakpoint: 768px;
$desktop-breakpoint: 769px;