First real commit

This commit is contained in:
zuma 2025-03-12 17:44:13 +01:00
parent 1947b908a6
commit 107d33c908
30 changed files with 2583 additions and 1 deletions

4
.dockerignore Normal file
View file

@ -0,0 +1,4 @@
node_modules
music
dist
server

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
node_modules
dist
music
server

32
Dockerfile Normal file
View file

@ -0,0 +1,32 @@
# Golang builder
FROM golang:1.21.5 AS go_builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY *.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -o /server
# Svelte app builder
FROM node:22 AS svelte_builder
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
# Runner image
FROM alpine:3
WORKDIR /app
RUN mkdir /app/music
RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/*
COPY --from=go_builder /server /app/server
COPY --from=svelte_builder /app/dist /app/dist
EXPOSE 8080
# Run
CMD ["/app/server"]

View file

@ -1,2 +1,34 @@
# mentalaradio
# Radio Menthe à l'eau
![Screenshot de la radio Menthe à l'eau](./assets/screenshot_website.png)
Cette webradio accessible ici [https://menthe.shenanigans.cc](https://menthe.shenanigans.cc) diffuse en continu la playlist [Menthe à l'eau](https://www.youtube.com/playlist?list=PLiMXFqXq5gIo7hLOtbgOiQPAVTk2kSjCE).
# Installation
## Docker
Vous pouvez utiliser l'image docker que je viens mettre à jour à la main :
```
git.shenanigans.cc/globuzma/mentalaradio:latest
```
## Compilation
Si vous voulez l'installer en compilant vous même les sources, voic les étapes à suivre.
Il faut d'abord compiler l'UI.
```
npm install
npm run build
```
Ensuite vous pouvez compiler le serveur.
```
go build server.go
```
Lorsque vous lancerez le serveur avec `./server` le programme viendra chercher tous les mp3 à la racine du dossier **music** et l'UI compilée devra être dans le dossier **dist**. Vous pouvez simplement copier le dossier dist produit par `npm run build` au même endroit que l'executable `server`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

8
go.mod Normal file
View file

@ -0,0 +1,8 @@
module radio
go 1.21.5
require (
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300
)

4
go.sum Normal file
View file

@ -0,0 +1,4 @@
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 h1:XQdibLKagjdevRB6vAjVY4qbSr8rQ610YzTkWcxzxSI=
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300/go.mod h1:FNa/dfN95vAYCNFrIKRrlRo+MBLbwmR9Asa5f2ljmBI=

14
index.html Normal file
View file

@ -0,0 +1,14 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Radio menthe à l'eau 🍹</title>
<meta name="robots" content="noindex nofollow" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

1833
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

25
package.json Normal file
View file

@ -0,0 +1,25 @@
{
"name": "bondsbuddy",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tsconfig/svelte": "^5.0.4",
"svelte": "^5.20.2",
"svelte-check": "^4.1.4",
"typescript": "~5.7.2",
"vite": "^6.2.0"
},
"dependencies": {
"@tailwindcss/vite": "^4.0.9",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.9"
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/fonts/Momentz.woff2 Normal file

Binary file not shown.

1
public/vite.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

295
server.go Normal file
View file

@ -0,0 +1,295 @@
package main
import (
"bytes"
"io"
"log"
"fmt"
"net/http"
"os"
"encoding/base64"
"encoding/json"
"sync"
"time"
"path/filepath"
"github.com/dhowden/tag"
"github.com/tcolgate/mp3"
"math/rand"
"math"
)
const (
BUFFERSIZE = 8192
)
var artworkAsUrl = ""
var title = ""
var url = ""
type Connection struct {
bufferChannel chan []byte
buffer []byte
}
type ConnectionPool struct {
ConnectionMap map[*Connection]struct{}
mu sync.Mutex
}
type MetadataConnection struct {
metadataSent bool
}
type MetadataConnectionPool struct {
MetadataConnectionMap map[*MetadataConnection]struct{}
mu sync.Mutex
}
// Audio connection pool
func (cp *ConnectionPool) AddConnection(connection *Connection) {
defer cp.mu.Unlock()
cp.mu.Lock()
cp.ConnectionMap[connection] = struct{}{}
}
func (cp *ConnectionPool) DeleteConnection(connection *Connection) {
defer cp.mu.Unlock()
cp.mu.Lock()
delete(cp.ConnectionMap, connection)
}
func NewConnectionPool() *ConnectionPool {
connectionMap := make(map[*Connection]struct{})
return &ConnectionPool{ConnectionMap: connectionMap}
}
func (cp *ConnectionPool) Broadcast(buffer []byte) {
defer cp.mu.Unlock()
cp.mu.Lock()
for connection := range cp.ConnectionMap {
copy(connection.buffer, buffer)
select {
case connection.bufferChannel <- connection.buffer:
default:
}
}
}
// Metadata connection pool
func (cp *MetadataConnectionPool) AddConnection(connection *MetadataConnection) {
defer cp.mu.Unlock()
cp.mu.Lock()
cp.MetadataConnectionMap[connection] = struct{}{}
}
func (cp *MetadataConnectionPool) DeleteConnection(connection *MetadataConnection) {
defer cp.mu.Unlock()
cp.mu.Lock()
delete(cp.MetadataConnectionMap, connection)
}
func NewMetadataConnectionPool() *MetadataConnectionPool {
metadataConnectionMap := make(map[*MetadataConnection]struct{})
return &MetadataConnectionPool{MetadataConnectionMap: metadataConnectionMap}
}
func (cp *MetadataConnectionPool) Broadcast() {
defer cp.mu.Unlock()
cp.mu.Lock()
for connection := range cp.MetadataConnectionMap {
connection.metadataSent = false
}
}
func getFileDelay(mp3FilePath string) int64 {
t := 0.0
size := 0
file, err := os.Open(mp3FilePath)
if err != nil {
log.Fatal(err)
}
d := mp3.NewDecoder(file)
var f mp3.Frame
skipped := 0
for {
if err := d.Decode(&f, &skipped); err != nil {
if err == io.EOF {
break
}
log.Println(err)
return 0
}
t = t + f.Duration().Seconds()
size = size + f.Size()
}
track_duration := 1000 * t
delayVal := int64(math.Floor(track_duration)) * BUFFERSIZE / int64(size)
log.Print(delayVal)
return delayVal
}
func streamFolder(connectionPool *ConnectionPool, metadataConnectionPool *MetadataConnectionPool, list []string) {
buffer := make([]byte, BUFFERSIZE)
for _, music := range list {
file, err := os.Open(filepath.Join("./music/", music))
if err != nil {
log.Fatal(err)
}
m, err := tag.ReadFrom(file)
if err != nil {
log.Fatal(err)
}
title = m.Title()
log.Print(title)
rawData := m.Raw()
for _, v := range rawData {
if _, isTagPicture := v.(*tag.Picture); isTagPicture {
artworkMIMEType := v.(*tag.Picture).MIMEType
artworkAsUrl = fmt.Sprintf("data:%s;base64,%s",artworkMIMEType, base64.StdEncoding.EncodeToString(m.Picture().Data))
}
if _, isTagComm := v.(*tag.Comm); isTagComm {
entryDescription := v.(*tag.Comm).Description
if entryDescription == "purl" {
url = v.(*tag.Comm).Text
}
}
}
log.Print(url)
metadataConnectionPool.Broadcast()
ctn, err := io.ReadAll(file)
if err != nil {
log.Fatal(err)
}
// clear() is a new builtin function introduced in go 1.21. Just reinitialize the buffer if on a lower version.
clear(buffer)
tempfile := bytes.NewReader(ctn)
delay := getFileDelay(filepath.Join("./music/", music))
ticker := time.NewTicker(time.Millisecond * time.Duration(delay))
// Send one buffer in advance to avoid client choking
for range ticker.C {
_, err := tempfile.Read(buffer)
if err == io.EOF {
ticker.Stop()
break
}
connectionPool.Broadcast(buffer)
}
}
}
func main() {
connPool := NewConnectionPool()
metadataConnPool := NewMetadataConnectionPool()
music_files := []string{}
entries, err := os.ReadDir("./music")
if err != nil {
log.Fatal(err)
}
for _, e := range entries {
if filepath.Ext(e.Name()) == ".mp3" {
music_files = append(music_files, e.Name())
}
}
log.Printf("%d Music file found.", len(music_files))
rand.Seed(time.Now().UnixNano())
rand.Shuffle(len(music_files), func(i, j int) { music_files[i], music_files[j] = music_files[j], music_files[i] })
go streamFolder(connPool, metadataConnPool, music_files)
http.Handle("/", http.FileServer(http.Dir("./dist")))
http.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "audio/mp3")
w.Header().Add("Connection", "keep-alive")
flusher, ok := w.(http.Flusher)
if !ok {
log.Println("Could not create flusher")
}
connection := &Connection{bufferChannel: make(chan []byte), buffer: make([]byte, BUFFERSIZE)}
connPool.AddConnection(connection)
log.Printf("%s has connected to the audio stream\n", r.Host)
for {
buf := <-connection.bufferChannel
if _, err := w.Write(buf); err != nil {
connPool.DeleteConnection(connection)
log.Printf("%s's connection to the audio stream has been closed\n", r.Host)
return
}
flusher.Flush()
clear(connection.buffer)
}
})
http.HandleFunc("/metadata", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Expose-Headers", "Content-Type")
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
connection := &MetadataConnection{metadataSent: false}
metadataConnPool.AddConnection(connection)
log.Printf("%s has connected to the metadata stream\n", r.Host)
// Simulate sending events (you can replace this with real data)
for {
if(connection.metadataSent == false) {
data := map[string]string{ "title":"", "url":"", "artwork":"" }
data["title"] = title
data["url"] = url
data["artwork"] = artworkAsUrl
finalData, err := json.Marshal(data)
if err != nil {
log.Fatal(err)
}
fmt.Fprintf(w, "data: %s\n\n", fmt.Sprintf("%s", finalData))
w.(http.Flusher).Flush()
connection.metadataSent = true
}
time.Sleep(time.Second * 1)
}
})
log.Println("Listening on port 8080...")
log.Fatal(http.ListenAndServe(":8080", nil))
}

137
src/App.svelte Normal file
View file

@ -0,0 +1,137 @@
<script>
import { basePath } from './lib/router.tsx'
import menthealeauLogo from './assets/menthealeau.svg'
let isPlaying = $state(false)
let title = $state("")
let url = $state("")
let imageData = $state("")
let loadingSound = $state(false)
let firstTimePlaying = $state(true)
let startedAudio = $state(new Date())
let audioPlayer
const eventSource = new EventSource(basePath + '/metadata')
eventSource.onmessage = function(event) {
let receivedData = JSON.parse(event.data)
console.log(receivedData)
title = receivedData.title
url = receivedData.url
imageData = receivedData.artwork
}
async function buttonClick() {
console.log(audioPlayer.duration)
if(isPlaying) {
audioPlayer.pause()
isPlaying = false
} else {
if(firstTimePlaying) {
loadingSound = true
audioPlayer.muted = true
audioPlayer.play()
startedAudio = new Date()
await new Promise(r => setTimeout(r, 3000))
audioPlayer.pause()
audioPlayer.muted = false
loadingSound = false
firstTimePlaying = false
audioPlayer.currentTime = 0
} else {
audioPlayer.currentTime = ((new Date().getTime() - startedAudio.getTime()) / 1000) - 3
}
audioPlayer.play()
isPlaying = true
}
}
</script>
<div class="min-w-screen min-h-screen overflow-y-hidden flex flex-col items-center justify-center p-4">
<div class="flex -mt-8 md:mt-32 flex-row justify-between w-full max-w-96">
<div class="w-0 flex flex-col invisible md:w-fit md:visible items-end gap-4 -rotate-12 md:mb-6 md:-ml-36 md:-mt-16">
<span class="speech-bubble arrow-left">Et t'écoutes quoi comme musique ?</span>
<span class="speech-bubble arrow-right">Oh ... de tout</span>
</div>
<img class="w-96 -mb-48 md:w-xl md:-mr-56 md:-mt-64 md:-mb-80" src={menthealeauLogo} alt="Logo d'une menthe à l'eau" />
</div>
<div class="p-4 bg-[#f97095] rounded-md border-white border-5 drop-shadow-md flex flex-col gap-4 max-w-96 w-full">
<h1 class="font-[Momentz] text-white font-bold text-4xl">Menthe à l'eau</h1>
<p class="text-white font-[Inter] font-bold">Créée à l'origine pour être un énorme dump de musique en tout genre. Cette playlist est contre toute attente devenue un énorme dump de musique en tout genre. Enjoy the radio. 📻</p>
{#if imageData == ""}
<div class="block w-full pb-[26%] pt-[26%] flex flex-col items-center justify-center bg-[#ff99b4] rounded-md">
<svg class="mr-3 -ml-1 size-5 animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
</div>
{:else}
<a href={url} target="_blank">
<img class="w-full rounded" src={imageData} alt="current song artwork" />
</a>
{/if}
<span class="text-white font-[Inter] font-bold w-full text-ellipsis truncate">{#if title == ""}Loading...{:else}{title}{/if}</span>
<audio bind:this={audioPlayer}>
<source src={basePath + "/stream"} type="audio/mp3">
</audio>
<div class="flex flex-col items-center w-full">
<div class="bg-[#ff99b4] w-32 rounded-full -mb-12">
{#if loadingSound}
<svg class="mr-3 -ml-1 w-full animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
{:else}
{#if isPlaying}
<svg xmlns="http://www.w3.org/2000/svg" onclick={buttonClick} class="text-white w-full" viewBox="0 0 24 24"><path fill="currentColor" d="M6 16V8q0-.825.588-1.412T8 6h8q.825 0 1.413.588T18 8v8q0 .825-.587 1.413T16 18H8q-.825 0-1.412-.587T6 16"/></svg>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" onclick={buttonClick} class="text-white w-full" viewBox="0 0 24 24"><path fill="currentColor" d="M8 17.175V6.825q0-.425.3-.713t.7-.287q.125 0 .263.037t.262.113l8.15 5.175q.225.15.338.375t.112.475t-.112.475t-.338.375l-8.15 5.175q-.125.075-.262.113T9 18.175q-.4 0-.7-.288t-.3-.712"/></svg>
{/if}
{/if}
</div>
</div>
</div>
<div class="w-fit h-fit flex mt-12 mb-24 md:mb-12 flex-col visible md:w-0 md:h-0 md:invisible items-end gap-4">
<span class="speech-bubble arrow-left">Et t'écoutes quoi comme musique ?</span>
<span class="speech-bubble arrow-right">Oh ... De tout.</span>
</div>
<div class="w-full opacity-50">
<span>Crée par Zuma · <a href="#">Source</a></span>
</div>
</div>
<style>
.speech-bubble {
width: fit-content;
padding: 1rem;
position: relative;
background: #FFF;
border-radius: .4em;
}
.arrow-right:after {
content: '';
position: absolute;
right: 0;
top: 50%;
width: 0;
height: 0;
border: 0.969em solid transparent;
border-left-color: #FFF;
border-right: 0;
margin-top: -0.969em;
margin-right: -0.7em;
}
.arrow-left:after {
content: '';
position: absolute;
left: 0;
top: 50%;
width: 0;
height: 0;
border: 0.969em solid transparent;
border-right-color: #FFF;
border-left: 0;
margin-top: -0.969em;
margin-left: -0.7em;
}
:global(body) {
background-size: cover;
background-image: url('data:image/svg+xml,<svg width="1920" xmlns="http://www.w3.org/2000/svg" height="1080" fill="none"><defs><clipPath id="a" class="frame-clip frame-clip-def"><rect rx="0" ry="0" width="1920" height="1080" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)"/></clipPath></defs><g clip-path="url(%23a)"><g class="fills"><rect width="1920" height="1080" class="frame-background" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" style="fill: rgb(16, 180, 182); fill-opacity: 1;" ry="0" rx="0"/></g><g class="frame-children"><path d="M-407.000,424.309C-127.000,173.309,162.125,206.159,425.000,263.663C553.000,291.663,803.000,413.663,1012.000,391.663C1338.616,357.282,1986.000,270.000,2173.000,444.000C2360.000,618.000,1955.000,946.309,1955.000,946.309L-109.000,949.309L-407.000,424.309Z" style="fill: rgb(18, 187, 190); fill-opacity: 1;" class="fills"/><path d="M-534.000,480.309C-254.000,229.309,35.125,262.159,298.000,319.663C426.000,347.663,676.000,469.663,885.000,447.663C1211.616,413.282,1859.000,326.000,2046.000,500.000C2233.000,674.000,1828.000,1002.309,1828.000,1002.309L-236.000,1005.309L-534.000,480.309Z" style="fill: rgb(52, 191, 196); fill-opacity: 1;" class="fills"/><path d="M-338.000,572.309C-58.000,321.309,231.125,354.159,494.000,411.663C622.000,439.663,872.000,561.663,1081.000,539.663C1407.616,505.282,1886.000,379.309,2073.000,553.309C2260.000,727.309,2024.000,1094.309,2024.000,1094.309L-40.000,1097.309L-338.000,572.309Z" style="fill: rgb(71, 194, 202); fill-opacity: 1;" class="fills"/><path d="M-469.000,629.309C-189.000,378.309,100.125,411.159,363.000,468.663C491.000,496.663,741.000,618.663,950.000,596.663C1276.616,562.282,1755.000,436.309,1942.000,610.309C2129.000,784.309,1893.000,1151.309,1893.000,1151.309L-171.000,1154.309L-469.000,629.309Z" style="fill: rgb(112, 204, 215); fill-opacity: 1;" class="fills"/><path d="M-373.000,741.309C-93.000,490.309,196.125,523.159,459.000,580.663C587.000,608.663,837.000,730.663,1046.000,708.663C1372.616,674.282,1851.000,548.309,2038.000,722.309C2225.000,896.309,1989.000,1263.309,1989.000,1263.309L-75.000,1266.309L-373.000,741.309Z" style="fill: rgb(255, 255, 255); fill-opacity: 1;" class="fills"/><path d="M-350.000,798.646C-70.000,547.646,193.000,571.646,452.000,644.646C711.000,717.646,881.000,886.646,1189.000,772.646C1497.000,658.646,1874.000,605.646,2061.000,779.646C2248.000,953.646,2012.000,1320.646,2012.000,1320.646L-52.000,1323.646L-350.000,798.646Z" style="fill: rgb(253, 196, 127); fill-opacity: 1;" class="fills"/><path d="M-344.000,865.646C-64.000,614.646,199.000,638.646,458.000,711.646C717.000,784.646,887.000,953.646,1195.000,839.646C1503.000,725.646,1880.000,672.646,2067.000,846.646C2254.000,1020.646,2018.000,1387.646,2018.000,1387.646L-46.000,1390.646L-344.000,865.646Z" style="fill: rgb(254, 202, 132); fill-opacity: 1;" class="fills"/><path d="M-372.000,935.000C-92.000,684.000,171.000,708.000,430.000,781.000C689.000,854.000,859.000,1023.000,1167.000,909.000C1475.000,795.000,1852.000,742.000,2039.000,916.000C2226.000,1090.000,1990.000,1457.000,1990.000,1457.000L-74.000,1460.000L-372.000,935.000Z" style="fill: rgb(254, 206, 142); fill-opacity: 1;" class="fills"/></g></g></svg>');
}
</style>

27
src/app.css Normal file
View file

@ -0,0 +1,27 @@
@import "tailwindcss";
@font-face{
font-family: 'Momentz';
src: url('/fonts/Momentz.woff2') format('woff2');
}
@font-face{
font-family: 'Inter';
font-style: normal;
font-weight: normal;
src: url('/fonts/Inter-Regular.woff2') format('woff2');
}
@font-face{
font-family: 'Inter';
font-style: italic;
font-weight: normal;
src: url('/fonts/Inter-Italic.woff2') format('woff2');
}
@font-face{
font-family: 'Inter';
font-style: normal;
font-weight: bold;
src: url('/fonts/Inter-Bold.woff2') format('woff2');
}

View file

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="593.60071"
height="667.54163"
viewBox="-32 -32 593.60071 667.54161"
fill="none"
version="1.1"
id="svg6"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs6" />
<g
filter="url(#a)"
id="g6"
transform="translate(-27.199546,-90.924425)">
<defs
id="defs3">
<filter
id="a"
x="-0.0082758516"
y="-0.0073401053"
width="1.0234482"
height="1.020797"
filterUnits="objectBoundingBox"
color-interpolation-filters="sRGB">
<feFlood
flood-opacity="0"
result="BackgroundImageFix"
id="feFlood1" />
<feColorMatrix
in="SourceAlpha"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
id="feColorMatrix1" />
<feOffset
dx="4"
dy="4"
id="feOffset1" />
<feGaussianBlur
stdDeviation="2"
id="feGaussianBlur1" />
<feColorMatrix
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"
id="feColorMatrix2" />
<feBlend
in2="BackgroundImageFix"
result="filter_265b8c7a-1d1a-80dd-8005-dbe7b8ea37ba"
id="feBlend2"
mode="normal" />
<feBlend
in="SourceGraphic"
in2="filter_265b8c7a-1d1a-80dd-8005-dbe7b8ea37ba"
result="shape"
id="feBlend3"
mode="normal" />
</filter>
</defs>
<path
d="M 74,79.666 229.489,63.764 c 0,0 14.498,-1.017 21.131,7.26 6.285,7.842 6.769,26.2 6.769,26.2 l 30.51,307.195 -31.228,2.19 -27.145,-297.568 c 0,0 -0.528,-10.723 -5.478,-14.186 -3.887,-2.72 -20.662,-0.569 -20.662,-0.569 l -125.462,13.73 z"
style="fill:#ffb700;fill-opacity:1"
class="fills"
id="path3" />
<path
d="m 372.205,271.622 c 17.68,-76.098 73.026,-129.905 83.018,-126.83 9.993,3.074 4.613,88.396 -2.306,112.225 -0.26,0.896 -2.697,5.717 2.306,3.075 24.106,-12.731 119.029,7.399 124.528,20.754 5.38,13.067 -77.638,43.045 -121.837,37.28 -1.876,-0.245 -2.12,0.267 0,1.922 16.878,13.175 25.751,101.463 16.527,106.075 -11.277,5.638 -83.787,-61.493 -86.093,-84.553 -2.306,-23.06 -13.068,-20.754 -13.837,-24.597 -0.768,-3.843 -4.227,-39.971 -2.306,-45.351 z m 0,0"
style="fill:#20a541;fill-opacity:1"
class="fills"
id="path4" />
<path
d="m 140.831,223.964 c 0,0 -164.766,396.433 -137.862,412.575 26.904,16.142 150.663,96.851 179.104,78.403 C 210.515,696.494 421.904,338.296 406.53,302.938 391.156,267.579 195.141,169.19 170.543,186.869 c -24.598,17.679 -37.666,57.65 -29.712,37.095 z m 0,0"
style="fill:#ffffff;fill-opacity:1"
class="fills"
id="path5" />
<path
d="m 150.129,258.548 c 0,0 -146.245,349.224 -123.795,362.705 22.45,13.481 125.72,80.886 149.453,65.479 23.732,-15.407 200.125,-314.558 187.297,-344.088 -4.739,-10.908 -58.204,8.205 -89.271,-9.529 -27.411,-15.648 -7.611,-66.952 -33.822,-76.867 -24.502,-9.268 -67.572,-15.038 -73.826,-10.539 -20.526,14.765 -16.036,12.839 -16.036,12.839 z"
style="fill:#3aff94;fill-opacity:1"
class="fills"
id="path6" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

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

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

1
src/lib/index.ts Normal file
View file

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

1
src/lib/router.tsx Normal file
View file

@ -0,0 +1 @@
export const basePath = (import.meta.env.MODE == "development") ? "http://localhost:8080" : (window.BASE_PATH || "")

6
src/lib/utils.ts Normal file
View file

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

9
src/main.ts Normal file
View file

@ -0,0 +1,9 @@
import { mount } from 'svelte'
import './app.css'
import App from './App.svelte'
const app = mount(App, {
target: document.getElementById('app')!,
})
export default app

2
src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

7
svelte.config.js Normal file
View file

@ -0,0 +1,7 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
}

20
tsconfig.app.json Normal file
View file

@ -0,0 +1,20 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"resolveJsonModule": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"isolatedModules": true,
"moduleDetection": "force"
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
}

7
tsconfig.json Normal file
View file

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

24
tsconfig.node.json Normal file
View file

@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

9
vite.config.ts Normal file
View file

@ -0,0 +1,9 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [svelte(), tailwindcss()],
base: './'
})