Compare commits

..

2 Commits

Author SHA1 Message Date
8e418021ae Create Home
All checks were successful
Publish Docker image / Push Docker image to Docker Hub (push) Successful in 1m16s
2025-02-28 18:38:24 +01:00
7d5813b1c2 Migrate to components 2025-02-28 18:25:12 +01:00
15 changed files with 501 additions and 425 deletions

View File

@ -1,13 +1,22 @@
import Home from "./pages/Home";
import Game from "./pages/Game";
import {useContext} from "react";
import {StateContext} from "@/common/contexts/StateContext";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faMusic} from "@fortawesome/free-solid-svg-icons";
import Home from "@/pages/Home";
const App = () => {
const {currentState} = useContext(StateContext);
return (
<>
<div className="background-elements">
<FontAwesomeIcon icon={faMusic} className="music-note note-1" />
<FontAwesomeIcon icon={faMusic} className="music-note note-2" />
<FontAwesomeIcon icon={faMusic} className="music-note note-3" />
</div>
{currentState === "Home" && <Home />}
{currentState === "Game" && <Game />}
</>
)
}

View File

@ -0,0 +1,6 @@
$background: rgba(255, 255, 255, 0.14)
$border: rgba(255, 255, 255, 0.35)
$white: #ECECEC
$green: #26EE5E

View File

@ -3,15 +3,58 @@
*
box-sizing: border-box
body, html
body
margin: 0
padding: 0
background-color: #232529
letter-spacing: 0.2rem
color: #E0E0E0
height: 100%
font-family: 'Bangers', sans-serif
height: 100vh
background: linear-gradient(135deg, #0f2027, #203a43, #2c5364)
user-select: none
overflow: hidden
position: relative
.glassy
background: $background
backdrop-filter: blur(10px)
border: 2px solid $border
border-radius: 0.8rem
::-webkit-scrollbar
width: 10px
width: 10px
.background-elements
position: absolute
top: 0
left: 0
width: 100%
height: 100%
overflow: hidden
z-index: 0
.music-note
position: absolute
font-size: 24pt
color: rgba(255, 255, 255, 0.5)
animation: float-notes 5s infinite ease-in-out
&.note-1
top: 20%
left: 30%
&.note-2
top: 60%
left: 80%
&.note-3
top: 90%
left: 50%
@keyframes float-notes
0%
transform: translateY(0)
50%
transform: translateY(-10px)
100%
transform: translateY(0)

View File

@ -0,0 +1,66 @@
import "./styles.sass";
import {StateContext} from "@/common/contexts/StateContext";
import {useContext, useState, useEffect, useRef} from "react";
import MusicSlider from "@/pages/Game/components/MusicSlider";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faMessage} from "@fortawesome/free-solid-svg-icons";
export const Game = () => {
const {setCurrentState} = useContext(StateContext);
const [messages, setMessages] = useState([{sender: "Marco", text: "Hallo!"},]);
const [inputValue, setInputValue] = useState("");
const messageEndRef = useRef(null);
useEffect(() => {
messageEndRef.current?.scrollIntoView({behavior: "smooth"});
}, [messages]);
const handleSendMessage = () => {
if (inputValue.trim()) {
setMessages([...messages, {sender: "User", text: inputValue}]);
setInputValue("");
}
};
return (
<div className="game-page">
<div className="main-content">
<div className="song-display">
<h2>ToneGuessr</h2>
<div className="song-card">
<img
src="https://mir-s3-cdn-cf.behance.net/project_modules/1400/fe529a64193929.5aca8500ba9ab.jpg"
alt="Song"/>
<div className="song-info">
<div className="song-names">Black Steam</div>
<div className="song-description">von Carrot Quest GmbH</div>
</div>
</div>
</div>
<div className="chat-window">
<div className="chat-header">
<FontAwesomeIcon icon={faMessage} />
<div className="chat-title">Chat</div>
</div>
<div className="chat-messages">
{messages.map((message, index) => (
<div key={index} className="message">
<span className="message-sender">{message.sender}:</span>
<span className="message-text">{message.text}</span>
</div>
))}
<div ref={messageEndRef}></div>
</div>
<div className="chat-input">
<input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
placeholder="Gib eine Nachricht ein..."
/>
<button onClick={handleSendMessage}>Send</button>
</div>
</div>
</div>
<MusicSlider/>
</div>
);
}

View File

@ -0,0 +1,108 @@
import {useEffect, useRef, useState} from "react";
import "./styles.sass";
export const MusicSlider = () => {
const [frequency, setFrequency] = useState(440);
const [isPlaying, setIsPlaying] = useState(false);
const [dragging, setDragging] = useState(false);
const audioContextRef = useRef(null);
const oscillatorRef = useRef(null);
const gainNodeRef = useRef(null);
const sliderRef = useRef(null);
const handleMouseDown = (e) => {
setDragging(true);
startAudio();
handleFrequencyChange(e);
};
const handleMouseUp = () => {
setDragging(false);
stopAudio();
};
const handleFrequencyChange = (e) => {
const rect = sliderRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const clampedX = Math.max(0, Math.min(x, rect.width));
const newFrequency = 20 + (clampedX / rect.width) * 1980;
setFrequency(newFrequency);
};
useEffect(() => {
const handleMouseMove = (e) => dragging && handleFrequencyChange(e);
const handleMouseUpGlobal = () => dragging && handleMouseUp();
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUpGlobal);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUpGlobal);
};
}, [dragging]);
useEffect(() => {
if (isPlaying) {
if (oscillatorRef.current) {
oscillatorRef.current.frequency.setValueAtTime(frequency, audioContextRef.current.currentTime);
}
}
}, [frequency, isPlaying]);
const startAudio = () => {
if (!audioContextRef.current) {
audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)();
}
if (!oscillatorRef.current) {
oscillatorRef.current = audioContextRef.current.createOscillator();
oscillatorRef.current.type = 'sine';
oscillatorRef.current.frequency.setValueAtTime(frequency, audioContextRef.current.currentTime);
gainNodeRef.current = audioContextRef.current.createGain();
gainNodeRef.current.gain.setValueAtTime(0.5, audioContextRef.current.currentTime);
oscillatorRef.current.connect(gainNodeRef.current);
gainNodeRef.current.connect(audioContextRef.current.destination);
oscillatorRef.current.start();
} else {
oscillatorRef.current.frequency.setValueAtTime(frequency, audioContextRef.current.currentTime);
}
setIsPlaying(true);
};
const stopAudio = () => {
if (oscillatorRef.current) {
oscillatorRef.current.stop();
oscillatorRef.current.disconnect();
oscillatorRef.current = null;
}
setIsPlaying(false);
};
return (
<div className="otamatone-container">
<div className="otamatone" onMouseDown={handleMouseDown}>
<div className="otamatone-neck" ref={sliderRef}>
<div className="frequency-indicator" style={{left: `${(frequency - 20) / 1980 * 100}%`}}></div>
<div className="note-marker" style={{left: '10%', pointerEvents: 'none'}}></div>
<div className="note-marker" style={{left: '50%', pointerEvents: 'none'}}></div>
<div className="note-marker" style={{left: '90%', pointerEvents: 'none'}}></div>
</div>
<div className="otamatone-face">
<div
className="otamatone-mouth"
style={{
height: `${10 + (frequency / 2000) * 40}px`,
width: `${10 + (frequency / 2000) * 40}px`
}}
></div>
<div className="otamatone-eyes">
<div className="eye left-eye"></div>
<div className="eye right-eye"></div>
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1 @@
export {MusicSlider as default} from "./MusicSlider.jsx";

View File

@ -0,0 +1,82 @@
.otamatone-container
display: flex
justify-content: center
align-items: center
position: fixed
left: 0
right: 0
bottom: 0
padding: 20px
background-color: rgba(30, 30, 30, 0.5)
backdrop-filter: blur(10px)
border-radius: 20px
margin: 20px
z-index: 1
.otamatone
display: flex
flex-direction: row
align-items: center
cursor: pointer
padding: 10px
width: 100%
.otamatone-neck
flex: 1
height: 20px
background: linear-gradient(135deg, #000, #444)
border-radius: 10px
position: relative
.frequency-indicator
position: absolute
top: -10px
width: 20px
height: 20px
background-color: #ff0000
border-radius: 50%
transform: translateX(-50%)
.note-marker
position: absolute
top: -30px
font-size: 24pt
color: #fff
.otamatone-face
width: 100px
height: 100px
background: radial-gradient(circle, #fff, #ddd)
border-radius: 50%
display: flex
flex-direction: column
align-items: center
justify-content: center
position: absolute
left: 0
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2)
.otamatone-eyes
display: flex
justify-content: space-between
width: 60%
position: absolute
top: 30px
.eye
width: 15px
height: 15px
background-color: #000
border-radius: 50%
.left-eye
margin-right: 10px
.right-eye
margin-left: 10px
.otamatone-mouth
background-color: #000
border-radius: 50%
position: absolute
bottom: 10px

View File

@ -0,0 +1 @@
export {Game as default} from "./Game.jsx";

View File

@ -0,0 +1,129 @@
.game-page
display: flex
flex-direction: column
align-items: center
justify-content: center
height: 100vh
padding: 20px
.main-content
display: flex
flex-direction: row
align-items: flex-start
justify-content: center
width: 100%
padding: 20px
z-index: 1
.song-display
display: flex
flex-direction: column
align-items: center
justify-content: center
width: 50%
margin-right: 20px
color: #fff
text-align: center
h2
font-size: 36pt
color: #fff
margin-bottom: 20px
.song-card
display: flex
flex-direction: row
align-items: center
background-color: rgba(255, 255, 255, 0.1)
padding: 20px
border-radius: 20px
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5)
backdrop-filter: blur(10px)
border: 1px solid rgba(255, 255, 255, 0.2)
max-width: 500px
img
width: 100px
height: 100px
border-radius: 10px
margin-right: 20px
.song-info
display: flex
flex-direction: column
align-items: flex-start
.song-names
font-size: 24pt
color: #fff
margin-bottom: 10px
.song-description
font-size: 14pt
color: #aaa
.chat-window
width: 50%
height: 400px
background: rgba(255, 255, 255, 0.1)
backdrop-filter: blur(10px)
border-radius: 10px
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1)
border: 1px solid rgba(255, 255, 255, 0.2)
display: flex
flex-direction: column
overflow: hidden
.chat-header
display: flex
align-items: center
padding: 10px
background: rgba(30, 30, 30, 0.5)
border-bottom: 1px solid rgba(255, 255, 255, 0.2)
.chat-title
margin-left: 10px
font-size: 16pt
color: #fff
.chat-messages
flex: 1
padding: 10px
overflow-y: auto
color: #fff
.message
margin-bottom: 10px
.message-sender
font-weight: bold
.message-text
margin-left: 10px
.chat-input
display: flex
padding: 10px
background: rgba(30, 30, 30, 0.5)
border-top: 1px solid rgba(255, 255, 255, 0.2)
input
flex: 1
padding: 10px
border: none
border-radius: 5px
outline: none
background: rgba(255, 255, 255, 0.2)
color: #fff
button
margin-left: 10px
padding: 10px
background-color: #203a43
color: #fff
border: none
border-radius: 5px
cursor: pointer
&:hover
background-color: #1e1e1e

View File

@ -1,179 +1,18 @@
import "./styles.sass";
import ActionCard from "@/pages/Home/components/ActionCard";
import {faArrowRightToBracket, faPlusSquare} from "@fortawesome/free-solid-svg-icons";
import {StateContext} from "@/common/contexts/StateContext";
import {useContext, useState, useEffect, useRef} from "react";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faMusic } from '@fortawesome/free-solid-svg-icons';
import {useContext} from "react";
export const Home = () => {
const {setCurrentState} = useContext(StateContext);
const [frequency, setFrequency] = useState(440);
const [isPlaying, setIsPlaying] = useState(false);
const [dragging, setDragging] = useState(false);
const audioContextRef = useRef(null);
const oscillatorRef = useRef(null);
const gainNodeRef = useRef(null);
const sliderRef = useRef(null);
const [messages, setMessages] = useState([{ sender: "Marco", text: "Hallo!" },]);
const [inputValue, setInputValue] = useState("");
const messageEndRef = useRef(null);
const startAudio = () => {
if (!audioContextRef.current) {
audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)();
}
if (!oscillatorRef.current) {
oscillatorRef.current = audioContextRef.current.createOscillator();
oscillatorRef.current.type = 'sine';
oscillatorRef.current.frequency.setValueAtTime(frequency, audioContextRef.current.currentTime);
gainNodeRef.current = audioContextRef.current.createGain();
gainNodeRef.current.gain.setValueAtTime(0.5, audioContextRef.current.currentTime);
oscillatorRef.current.connect(gainNodeRef.current);
gainNodeRef.current.connect(audioContextRef.current.destination);
oscillatorRef.current.start();
} else {
oscillatorRef.current.frequency.setValueAtTime(frequency, audioContextRef.current.currentTime);
}
setIsPlaying(true);
};
const stopAudio = () => {
if (oscillatorRef.current) {
oscillatorRef.current.stop();
oscillatorRef.current.disconnect();
oscillatorRef.current = null;
}
setIsPlaying(false);
};
const handleMouseDown = (e) => {
setDragging(true);
startAudio();
handleFrequencyChange(e);
};
const handleMouseUp = () => {
setDragging(false);
stopAudio();
};
const handleFrequencyChange = (e) => {
const rect = sliderRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const clampedX = Math.max(0, Math.min(x, rect.width));
const newFrequency = 20 + (clampedX / rect.width) * 1980;
setFrequency(newFrequency);
};
useEffect(() => {
const handleMouseMove = (e) => {
if (dragging) {
handleFrequencyChange(e);
}
};
const handleMouseUpGlobal = () => {
if (dragging) {
handleMouseUp();
}
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUpGlobal);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUpGlobal);
};
}, [dragging]);
useEffect(() => {
if (isPlaying) {
if (oscillatorRef.current) {
oscillatorRef.current.frequency.setValueAtTime(frequency, audioContextRef.current.currentTime);
}
}
}, [frequency, isPlaying]);
useEffect(() => {
messageEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const handleSendMessage = () => {
if (inputValue.trim()) {
setMessages([...messages, { sender: "User", text: inputValue }]);
setInputValue("");
}
};
return (
<div className="home-page">
<div className="background-elements">
<FontAwesomeIcon icon={faMusic} className="music-note note-1" />
<FontAwesomeIcon icon={faMusic} className="music-note note-2" />
<FontAwesomeIcon icon={faMusic} className="music-note note-3" />
</div>
<div className="main-content">
<div className="song-display">
<h2>ToneGuessr</h2>
<div className="song-card">
<img src="https://mir-s3-cdn-cf.behance.net/project_modules/1400/fe529a64193929.5aca8500ba9ab.jpg" alt="Song" />
<div className="song-info">
<div className="song-name">Black Steam</div>
<div className="song-description">von Carrot Quest GmbH</div>
</div>
</div>
</div>
<div className="chat-window">
<div className="chat-header">
<div className="account-icon"></div>
<div className="chat-title">Chat</div>
</div>
<div className="chat-messages">
{messages.map((message, index) => (
<div key={index} className="message">
<span className="message-sender">{message.sender}:</span>
<span className="message-text">{message.text}</span>
</div>
))}
<div ref={messageEndRef}></div>
</div>
<div className="chat-input">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
placeholder="Gib eine Nachricht ein..."
/>
<button onClick={handleSendMessage}>Send</button>
</div>
</div>
</div>
<div className="otamatone-container">
<div
className="otamatone"
onMouseDown={handleMouseDown}
>
<div className="otamatone-neck" ref={sliderRef}>
<div className="frequency-indicator" style={{ left: `${(frequency - 20) / 1980 * 100}%` }}></div>
<div className="note-marker" style={{left: '10%', pointerEvents: 'none'}}></div>
<div className="note-marker" style={{left: '50%', pointerEvents: 'none'}}></div>
<div className="note-marker" style={{left: '90%', pointerEvents: 'none'}}></div>
</div>
<div className="otamatone-face">
<div
className="otamatone-mouth"
style={{ height: `${10 + (frequency / 2000) * 40}px`, width: `${10 + (frequency / 2000) * 40}px` }}
></div>
<div className="otamatone-eyes">
<div className="eye left-eye"></div>
<div className="eye right-eye"></div>
</div>
</div>
</div>
<div className="action-area">
<ActionCard title="Raum beitreten" icon={faArrowRightToBracket} onClick={() => {}} />
<ActionCard title="Raum erstellen" icon={faPlusSquare} onClick={() => setCurrentState("Game")} />
</div>
</div>
);
}
}

View File

@ -0,0 +1,11 @@
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import "./styles.sass";
export const ActionCard = ({ title, icon, onClick }) => {
return (
<div className="glassy action-card" onClick={onClick}>
<FontAwesomeIcon icon={icon} />
<h1>{title}</h1>
</div>
);
}

View File

@ -0,0 +1 @@
export {ActionCard as default} from "./ActionCard.jsx";

View File

@ -0,0 +1,23 @@
@use "@/common/styles/colors" as *
.action-card
display: flex
flex-direction: column
padding: 2rem 0
width: 13rem
text-align: center
gap: 2rem
transition: all 0.3s ease-in-out
cursor: pointer
user-select: none
svg
font-size: 52pt
color: $white
h1
margin: 0
color: $white
&:hover
transform: scale(1.1) translate(0, -0.5rem) rotate(0.5deg)

View File

@ -1 +1 @@
export {Home as default} from "./Home";
export {Home as default} from "./Home.jsx";

View File

@ -3,256 +3,13 @@
flex-direction: column
align-items: center
justify-content: center
height: 100vh
background: linear-gradient(135deg, #0f2027, #203a43, #2c5364)
padding: 20px
user-select: none
overflow: hidden
position: relative
height: 100%
width: 100%
.background-elements
position: absolute
top: 0
left: 0
width: 100%
height: 100%
overflow: hidden
z-index: 0
.music-note
position: absolute
font-size: 24pt
color: rgba(255, 255, 255, 0.5)
animation: float-notes 5s infinite ease-in-out
&.note-1
top: 20%
left: 30%
&.note-2
top: 60%
left: 80%
&.note-3
top: 90%
left: 50%
@keyframes float-notes
0%
transform: translateY(0)
50%
transform: translateY(-10px)
100%
transform: translateY(0)
.main-content
display: flex
flex-direction: row
align-items: flex-start
justify-content: center
width: 100%
padding: 20px
z-index: 1
.song-display
display: flex
flex-direction: column
align-items: center
justify-content: center
width: 50%
margin-right: 20px
color: #fff
text-align: center
h2
font-size: 36pt
color: #fff
margin-bottom: 20px
.song-card
display: flex
flex-direction: row
align-items: center
background-color: rgba(255, 255, 255, 0.1)
padding: 20px
border-radius: 20px
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5)
backdrop-filter: blur(10px)
border: 1px solid rgba(255, 255, 255, 0.2)
max-width: 500px
img
width: 100px
height: 100px
border-radius: 10px
margin-right: 20px
.song-info
display: flex
flex-direction: column
align-items: flex-start
.song-name
font-size: 24pt
color: #fff
margin-bottom: 10px
.song-description
font-size: 14pt
color: #aaa
.chat-window
width: 50%
height: 400px
background: rgba(255, 255, 255, 0.1)
backdrop-filter: blur(10px)
border-radius: 10px
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1)
border: 1px solid rgba(255, 255, 255, 0.2)
display: flex
flex-direction: column
overflow: hidden
.chat-header
display: flex
align-items: center
padding: 10px
background: rgba(30, 30, 30, 0.5)
border-bottom: 1px solid rgba(255, 255, 255, 0.2)
.account-icon
width: 30px
height: 30px
background: url('https://place-hold.it/100x100') no-repeat center
background-size: cover
border-radius: 50%
margin-right: 10px
.chat-title
font-size: 16pt
color: #fff
.chat-messages
flex: 1
padding: 10px
overflow-y: auto
color: #fff
.message
margin-bottom: 10px
.message-sender
font-weight: bold
.message-text
margin-left: 10px
.chat-input
display: flex
padding: 10px
background: rgba(30, 30, 30, 0.5)
border-top: 1px solid rgba(255, 255, 255, 0.2)
input
flex: 1
padding: 10px
border: none
border-radius: 5px
outline: none
background: rgba(255, 255, 255, 0.2)
color: #fff
button
margin-left: 10px
padding: 10px
background-color: #203a43
color: #fff
border: none
border-radius: 5px
cursor: pointer
&:hover
background-color: #1e1e1e
.otamatone-container
display: flex
justify-content: center
align-items: center
position: fixed
left: 0
right: 0
bottom: 0
padding: 20px
background-color: rgba(30, 30, 30, 0.5)
backdrop-filter: blur(10px)
border-radius: 20px
margin: 20px
z-index: 1
.otamatone
display: flex
flex-direction: row
align-items: center
cursor: pointer
padding: 10px
width: 100%
.otamatone-neck
flex: 1
height: 20px
background: linear-gradient(135deg, #000, #444)
border-radius: 10px
position: relative
.frequency-indicator
position: absolute
top: -10px
width: 20px
height: 20px
background-color: #ff0000
border-radius: 50%
transform: translateX(-50%)
.note-marker
position: absolute
top: -30px
font-size: 24pt
color: #fff
.otamatone-face
width: 100px
height: 100px
background: radial-gradient(circle, #fff, #ddd)
border-radius: 50%
display: flex
flex-direction: column
align-items: center
justify-content: center
position: absolute
left: 0
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2)
.otamatone-eyes
display: flex
justify-content: space-between
width: 60%
position: absolute
top: 30px
.eye
width: 15px
height: 15px
background-color: #000
border-radius: 50%
.left-eye
margin-right: 10px
.right-eye
margin-left: 10px
.otamatone-mouth
background-color: #000
border-radius: 50%
position: absolute
bottom: 10px
.action-area
margin-top: 5rem
display: flex
gap: 3rem
flex-wrap: wrap
justify-content: center