initial commit

Signed-off-by: ale <ale@manalejandro.com>
This commit is contained in:
ale 2025-05-28 01:34:20 +02:00
commit 7528d2d4b8
Signed by: ale
GPG Key ID: 244A9C4DAB1C0C81
92 changed files with 22103 additions and 0 deletions

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
*.lock
*-lock.json

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# 📻 Stream Radio
## A simple radio streaming react app

41
package.json Normal file
View File

@ -0,0 +1,41 @@
{
"name": "stream-radio",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"materialize-css": "^1.0.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-materialize": "^3.10.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

9085
public/css/materialize.css vendored Normal file

File diff suppressed because it is too large Load Diff

13
public/css/materialize.min.css vendored Normal file

File diff suppressed because one or more lines are too long

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

50
public/index.html Normal file
View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Stream Radio is a web application that allows you to listen to your favorite radio stations online."
/>
<meta
name="keywords"
content="Stream Radio, radio, online radio, music, streaming"
/>
<meta name="author" content="manalejandro.com" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<script src="js/materialize.min.js" defer></script>
<link rel="stylesheet" href="css/materialize.min.css" />
<title>Stream Radio</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

12374
public/js/materialize.js vendored Normal file

File diff suppressed because it is too large Load Diff

6
public/js/materialize.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

26
public/manifest.json Normal file
View File

@ -0,0 +1,26 @@
{
"short_name": "Stream Radio",
"name": "Stream Radio",
"description": "A simple radio streaming app",
"icons": [
{
"src": "favicon.ico",
"sizes": "32x32",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,67 @@
pexels-baskincreativeco-1480807.jpg
pexels-belle-co-99483-847393.jpg
pexels-bess-hamiti-83687-36487.jpg
pexels-carlos-oliva-1966452-3586966.jpg
pexels-christian-heitz-285904-842711.jpg
pexels-dreamypixel-547115.jpg
pexels-eberhardgross-1062249.jpg
pexels-eberhardgross-1287075.jpg
pexels-eberhardgross-1287089.jpg
pexels-eberhardgross-1301976.jpg
pexels-eberhardgross-1367192(1).jpg
pexels-eberhardgross-1367192.jpg
pexels-eberhardgross-1612351.jpg
pexels-eberhardgross-1612360.jpg
pexels-eberhardgross-1612362.jpg
pexels-eberhardgross-1612371.jpg
pexels-eberhardgross-1624255.jpg
pexels-eberhardgross-534164.jpg
pexels-eberhardgross-629167.jpg
pexels-eberhardgross-691668.jpg
pexels-eberhardgross-707344.jpg
pexels-eberhardgross-730981.jpg
pexels-esan-2085998.jpg
pexels-fotios-photos-109260.jpg
pexels-francesco-ungaro-1525041.jpg
pexels-gochrisgoxyz-1477166.jpg
pexels-johnnoibn-1448136.jpg
pexels-joshkjack-135018.jpg
pexels-jplenio-1110656.jpg
pexels-jplenio-1146708.jpg
pexels-jplenio-1435075.jpg
pexels-kasperphotography-1042423.jpg
pexels-katja-79053-592077.jpg
pexels-lastly-937782.jpg
pexels-lilartsy-1213447.jpg
pexels-maxfrancis-2246476.jpg
pexels-mdsnmdsnmdsn-1831234.jpg
pexels-mdx014-814499.jpg
pexels-michal-pech-213601-1632044.jpg
pexels-no-name-14543-66997.jpg
pexels-pixabay-158063.jpg
pexels-pixabay-33109(1).jpg
pexels-pixabay-33109.jpg
pexels-pixabay-33545.jpg
pexels-pixabay-358532.jpg
pexels-pixabay-41004.jpg
pexels-pixabay-414144.jpg
pexels-pixabay-416160.jpg
pexels-pixabay-459203.jpg
pexels-pixabay-462162.jpg
pexels-pixabay-50594.jpg
pexels-pixabay-50686.jpg
pexels-pixabay-52500.jpg
pexels-rpnickson-2559941.jpg
pexels-rpnickson-2647990.jpg
pexels-samandgos-709552.jpg
pexels-samkolder-2387873.jpg
pexels-sebastian-312105.jpg
pexels-simon73-1183099.jpg
pexels-souvenirpixels-1519088.jpg
pexels-souvenirpixels-417074.jpg
pexels-stefanstefancik-919606.jpg
pexels-stywo-1054289.jpg
pexels-stywo-1668246.jpg
pexels-therato-1933239.jpg
pexels-todd-trapani-488382-1198817.jpg
pexels-umaraffan499-21787.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 KiB

101
src/App.css Normal file
View File

@ -0,0 +1,101 @@
body {
text-align: center;
color: white;
background-position: center center;
background-repeat: no-repeat;
background-attachment: fixed;
background-size: cover;
}
h1 {
padding-top: 16vh;
font-size: 4rem;
}
h4 {
margin: 0 auto;
overflow: hidden;
background-color: rgba(255, 255, 255, 0.3);
transition: 0.3s;
width: 85%;
font-size: x-large;
}
h4:hover {
width: 95%;
}
.bounce a {
border-bottom: 1px solid white;
}
.bounce {
animation: marquee 10s linear infinite;
}
.bounce:hover {
animation-play-state: paused;
}
a,
a:visited,
a:active,
a:hover {
color: white;
text-shadow: 0 0 5px #000;
text-decoration: none;
cursor: pointer;
}
@keyframes marquee {
0% {
transform: translateX(100vw);
}
100% {
transform: translateX(-100%);
}
}
audio {
display: none;
}
.player {
width: 26rem;
margin: 0 auto;
padding: 1rem;
border-radius: 10px;
}
input[type="range"],
input[type="range"]::-moz-range-thumb,
input[type="range"]::-webkit-slider-runnable-track,
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
cursor: pointer;
border: none;
margin: 0;
}
input[type="range"]::-moz-range-thumb,
input[type="range"]::-webkit-slider-thumb,
input[type=range]+.thumb,
.thumb {
background-color: #795548 !important;
}
input[type="range"],
input[type="range"]::-webkit-slider-runnable-track {
background-color: #4e342e !important;
}
.range-field {
vertical-align: middle;;
margin: 0 auto;
display: inline-block;
width: 6rem;
border: 0;
color: white;
}

167
src/App.js Normal file
View File

@ -0,0 +1,167 @@
import "./App.css";
import { useCallback, useEffect, useRef, useState } from "react";
import WaveForm from "./WaveForm";
import { Button, Icon, Range } from "react-materialize";
const App = () => {
const [audioUrl, setAudioUrl] = useState(null),
[analyzerData, setAnalyzerData] = useState(null),
[bounce, setBounce] = useState(''),
[json, setJson] = useState({}),
[currentVolume, setCurrentVolume] = useState(0.5),
[muted, setMuted] = useState(false),
[link, setLink] = useState(''),
[title, setTitle] = useState('Stream Radio'),
[images, setImages] = useState([]),
[currentListeners, setCurrentListeners] = useState(0),
[maxListeners, setMaxListeners] = useState(0),
[paused, setPaused] = useState(true),
audioElmRef = useRef(null),
once = useRef(false),
audioAnalyzer = () => {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)(),
source = audioCtx.createMediaElementSource(audioElmRef.current),
analyzer = audioCtx.createAnalyser()
analyzer.fftSize = 2048
const bufferLength = analyzer.frequencyBinCount,
dataArray = new Uint8Array(bufferLength)
source.connect(analyzer)
source.connect(audioCtx.destination)
source.onended = () => {
source.disconnect()
}
setAnalyzerData({ analyzer, bufferLength, dataArray })
},
loadData = () => {
fetch('/stream.json').then(response => response.json()).then(json => {
setJson(json)
}).catch(err => {
console.error('Error fetching data: ' + err.message)
})
},
loadImages = () => {
fetch('/wallpapers/list.txt').then(response => response.text()).then(text => {
setImages(text.split('\n').filter(line => line.length > 0))
}).catch(err => {
console.error('Error fetching data: ' + err.message)
})
},
loadListeners = () => {
fetch('/status.xsl').then(response => response.text()).then(data => {
const parser = new DOMParser(),
xmlDoc = parser.parseFromString(data, 'text/html'),
listeners = xmlDoc.getElementsByTagName('td')
for (let i = 0; i < listeners.length; i++) {
if (i === 9) {
setCurrentListeners(listeners[i].textContent)
} else if (i === 11) {
setMaxListeners(listeners[i].textContent)
}
}
}).catch(err => {
console.error('Error fetching data: ' + err.message)
})
},
play = useCallback(async () => {
if (!once.current) {
audioAnalyzer()
once.current = true
}
setPaused(false)
setMuted(false)
await audioElmRef.current?.play()
})
useEffect(() => {
if (json?.media?.track[0] && bounce.search(json?.media?.track[0].Title) === -1) {
setBounce('Now Playing: ')
setLink(json?.media['@ref']?.replace('/musica', 'https://manalejandro.com'))
Object.keys(json.media.track[0]).map((key) => {
if (key === 'Title' || key === 'Performer' || key === 'Album') {
if (key === 'Title') {
setTitle(json.media.track[0][key])
}
setBounce(data => data + json.media.track[0][key] + ' - ')
} else if (key === 'Filesize') {
setBounce(data => data + Math.round(parseInt(json.media.track[0][key]) / 1024 / 1024) + 'Mb - ')
} else if (key === 'Bitrate') {
setBounce(data => data + Math.floor(parseInt(json.media.track[0][key]) / 1000) + 'kbps - ')
} else if (key === 'Duration') {
setBounce(data => data + Math.floor(parseInt(json.media.track[0][key])) + 's - ')
} else if (key === 'Recorded_Date') {
setBounce(data => data + json.media.track[0][key])
}
})
} else if (!json?.media && bounce.search('Now Playing: null') === -1) {
setLink('')
setTitle('Stream Radio')
setBounce('Now Playing: null')
}
}, [json])
useEffect(() => {
document.body.style.backgroundImage = `url('/wallpapers/${images[Math.floor(Math.random() * images.length)]}')`
}, [images])
useEffect(() => {
loadData()
loadImages()
loadListeners()
setAudioUrl('/stream.mp3')
document.querySelector('input[type="range"]').addEventListener('change', (e) => {
setCurrentVolume(e.target.value / 100)
})
const inter = setInterval(() => {
loadData()
loadListeners()
}, (Math.floor(Math.random() * 20) + 10) * 1000),
interback = setInterval(() => {
loadImages()
}, (Math.floor(Math.random() * 60) + 90) * 1000)
return () => {
clearInterval(inter)
clearInterval(interback)
}
}, [])
useEffect(() => {
if (audioElmRef.current && audioElmRef.current.volume !== currentVolume) {
if (currentVolume > 0 && currentVolume <= 1) {
setMuted(false)
} else if (currentVolume === 0) {
setMuted(true)
}
audioElmRef.current.volume = currentVolume
}
}, [currentVolume])
return (
<>
<h1><a href="https://git.manalejandro.com/ale/stream-radio" title="Stream Radio Git Repository" alt="Stream Radio Git Repository" target="_blank"><Icon className="medium">hearing</Icon> Stream Radio</a></h1>
{analyzerData && <WaveForm analyzerData={analyzerData} />}
<h4><p className="bounce"><a href={link} target="_blank" title={title} alt={title}>{bounce}</a></p></h4>
<br /><br />
<div className="brown darken-3 player">
{paused ?
<Button node="button" className="brown" onClick={() => play()} waves="light" floating><Icon>play_arrow</Icon></Button>
:
<Button node="button" className="brown" onClick={() => {
audioElmRef.current?.pause()
setPaused(true)
}} waves="light" floating><Icon>pause</Icon></Button>
}&nbsp;
{muted ?
<Button node="button" className="brown" onClick={() => setMuted(false)} waves="light" floating><Icon>volume_off</Icon></Button>
:
<Button node="button" className="brown" onClick={() => setMuted(true)} waves="light" floating><Icon>volume_up</Icon></Button>
}&nbsp;&nbsp;
<Range className="brown" waves="light"
min={0}
max={100}
step={1}
/>&nbsp;&nbsp;
<span>
<Icon tiny>people</Icon>&nbsp;{currentListeners} / {maxListeners}
</span>
</div >
<audio src={audioUrl} ref={audioElmRef} volume={Math.log(currentVolume)} preload={"none"} muted={muted} controls={false} />
</>
)
}
export default App

8
src/App.test.js Normal file
View File

@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

55
src/WaveForm.jsx Normal file
View File

@ -0,0 +1,55 @@
import { useRef, useEffect } from "react";
import useSize from "./useSize";
const animateBars = (analyser, canvas, canvasCtx, dataArray, bufferLength) => {
analyser.getByteFrequencyData(dataArray)
canvasCtx.fillStyle = "#000"
const HEIGHT = canvas.height / 2
var barWidth = Math.ceil(canvas.width / bufferLength) * 2.5
let barHeight
let x = 0
for (var i = 0; i < bufferLength; i++) {
barHeight = (dataArray[i] / 255) * HEIGHT
const blueShade = Math.floor((dataArray[i] / 255) * 4) // generate a shade of blue based on the audio input
const blueHex = ['rgba(255,255,255,0.5)', 'rgba(255,255,255,0.4)', 'rgba(255,255,255,0.3)', 'rgba(255,255,255,0.2)'][blueShade]; // use react logo blue shades
canvasCtx.fillStyle = blueHex
canvasCtx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight)
x += barWidth + 1
}
},
WaveForm = ({ analyzerData }) => {
const canvasRef = useRef(null),
{ dataArray, analyzer, bufferLength } = analyzerData,
[width, height] = useSize(),
draw = (dataArray, analyzer, bufferLength) => {
const canvas = canvasRef.current
if (!canvas || !analyzer) return
const canvasCtx = canvas.getContext("2d"),
animate = () => {
requestAnimationFrame(animate)
// eslint-disable-next-line no-self-assign
canvas.width = canvas.width
canvasCtx.translate(0, canvas.offsetHeight / 2)
animateBars(analyzer, canvas, canvasCtx, dataArray, bufferLength)
}
animate()
}
useEffect(() => {
draw(dataArray, analyzer, bufferLength)
}, [dataArray, analyzer, bufferLength])
return (
<canvas
style={{
position: "absolute",
top: "0",
left: "0",
zIndex: "-10"
}}
ref={canvasRef}
width={width}
height={height}
/>
)
}
export default WaveForm

26
src/index.css Normal file
View File

@ -0,0 +1,26 @@
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(./material-icons.ttf) format('truetype');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
}
body {
font-family: monospace, monospace;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

17
src/index.js Normal file
View File

@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

1
src/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
src/material-icons.ttf Normal file

Binary file not shown.

13
src/reportWebVitals.js Normal file
View File

@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

5
src/setupTests.js Normal file
View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

17
src/useSize.js Normal file
View File

@ -0,0 +1,17 @@
import { useCallback, useEffect, useState } from "react";
const useSize = () => {
const [width, setWidth] = useState(0),
[height, setHeight] = useState(0),
setSizes = useCallback(() => {
setWidth(window.innerWidth)
setHeight(window.innerHeight)
}, [setWidth, setHeight])
useEffect(() => {
window.addEventListener("resize", setSizes)
setSizes()
}, [setSizes])
return [width, height]
}
export default useSize