splits components into more managable chunks #8

Merged
tristonarmstrong merged 4 commits from 1-split_components into main 2024-03-17 20:28:27 +00:00
11 changed files with 261 additions and 212 deletions
Showing only changes of commit 8426d8acd7 - Show all commits

BIN
bun.lockb

Binary file not shown.

View File

@ -12,7 +12,8 @@
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "^1", "@tauri-apps/api": "^1",
"kaioken": "^0.10.0" "kaioken": "^0.10.5",
"vite-plugin-kaioken": "^0.0.7"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^1", "@tauri-apps/cli": "^1",
@ -21,8 +22,7 @@
"postcss": "^8.4.35", "postcss": "^8.4.35",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5.0.2", "typescript": "^5.0.2",
"vite": "^5.0.0", "vite": "^5.0.0"
"vite-plugin-kaioken": "^0.0.7"
}, },
"config": { "config": {
"commitizen": { "commitizen": {

View File

@ -1,212 +1,31 @@
import { exists, readTextFile, writeTextFile } from "@tauri-apps/api/fs"
import { Navs, useNavigator } from "./hooks/useNavigator" import { Navs, useNavigator } from "./hooks/useNavigator"
import { appDataDir } from "@tauri-apps/api/path" import { useEffect } from "kaioken"
import { useEffect, useModel, useRef, useState } from "kaioken" import { useStationsProvider } from "./providers/StationsProvider"
import Main from "./pages/Main"
import Player from "./pages/Player"
import { useStorageContext } from "./providers/StorageProvider"
import Add from "./pages/Add"
interface Station {
url: string
avatar: string
title: string
}
export function App() { export function App() {
const { nav, setNavitation } = useNavigator() const { setStations } = useStationsProvider()
const [stations, setStations] = useState<Station[] | null>(null) const { getStationsFile } = useStorageContext()
const [titleRef, title,] = useModel<HTMLInputElement, string>('') const { nav } = useNavigator()
const [streamRef, streamUrl,] = useModel<HTMLInputElement, string>('')
const [avatarRef, avatarUrl,] = useModel<HTMLInputElement, string>('')
const [selectedStation, setSelectedStation] = useState<Station | null>(null)
const appDataDirRef = useRef<string>(null)
useEffect(() => { useEffect(() => {
appDataDir() getStationsFile()
.then(async (res) => { .then(res => res && setStations(res))
const path = `${res}/stations.json` .catch()
if (await exists(path)) {
console.log("file exists: ", path);
const jsonString = await readTextFile(path)
const json = JSON.parse(jsonString) as Station[]
console.log(json)
setStations(json)
appDataDirRef.current = path
return
}
console.error("File does not exist... creating")
writeTextFile(path, "[]", { append: false })
})
.catch(err => console.error(err))
}, []) }, [])
function _handleStationAdd() {
console.log({ title, streamUrl, avatarUrl })
const data: Station = {
url: streamUrl,
avatar: avatarUrl,
title: title
}
setStations(prev => {
const newStations = [...(prev ?? []), data]
// write file
writeTextFile(appDataDirRef.current!, JSON.stringify(newStations))
return newStations
})
console.log("Added station: ", data)
setNavitation(Navs.MAIN)
}
function _handleStationClick(station: Station) { switch (nav) {
return function() { case Navs.MAIN:
setSelectedStation(station) return <Main />
setNavitation(Navs.PLAYER) case Navs.ADD:
return <Add />
case Navs.PLAYER:
return <Player />
default:
return <h1>404 Not Found</h1>
} }
}
if (nav === Navs.MAIN) {
if (!stations?.length) {
return (
<div className="p-4 flex flex-col align-between h-full gap-2">
<h2 className="text-black text-opacity-50 font-bold text-center">No Stations Added</h2>
<button className="bg-white rounded-xl p-2" onclick={() => setNavitation(1)}>+</button>
</div>
)
}
return (
<div>
<div className="flex justify-end">
<button className="mr-4 px-2 py-1 hover:bg-gray-300 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
</button>
</div>
<div className="p-4 flex flex-col align-between h-full gap-2">
{stations.map((s) => (
<button onclick={_handleStationClick(s)} className="paper p-2 flex flex-row border gap-4">
<img src={s.avatar} width={40} height={40} className="w-20 rounded-xl" />
<div className="flex flex-col gap-2 w-full">
<h2 className="text-xl">{s.title}</h2>
<button className="border border-red-500 rounded w-full text-red-500 px-4">Delete</button>
</div>
</button>
))}
<button className="bg-white rounded-xl p-2" onclick={() => setNavitation(1)}>+</button>
</div>
</div>
)
}
if (nav === Navs.ADD) {
return (
<div >
<button onclick={() => setNavitation(0)} className="ml-2 px-2 py-1 hover:bg-gray-300 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
</svg>
</button>
<div className="flex flex-col gap-3 p-2">
<input ref={titleRef} className="rounded p-1" ariaLabel={"title"} placeholder="Title" />
<input ref={streamRef} className="rounded p-1" ariaLabel={"url"} placeholder="Stream URL" />
<input ref={avatarRef} className="rounded p-1" ariaLabel={"avatar"} placeholder="Image URL" />
<button onclick={_handleStationAdd} className="rounded bg-blue-400 p-1">Save</button>
</div>
</div>
)
}
function _handlePlayerBackClick() {
setNavitation(Navs.MAIN)
setSelectedStation(null)
}
if (nav === Navs.PLAYER) {
return (
<div >
<button onclick={_handlePlayerBackClick} className="ml-2 px-2 py-1 hover:bg-gray-300 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
</svg>
</button>
<div className="flex justify-center">
<div className="p-8 w-80">
{/*<!-- Album Cover -->*/}
<img src={selectedStation?.avatar} alt={selectedStation?.title} className="w-64 h-64 mx-auto rounded-lg mb-4 shadow-md shadow-black-200" />
{/*<!-- Song Title -->*/}
<h2 className="text-xl font-semibold text-center">{selectedStation?.title}</h2>
{/*<!-- Artist Name -->*/}
<p className="text-gray-600 text-sm text-center">Live Radio</p>
<br />
<audio autoplay controls src={selectedStation?.url}></audio>
</div>
</div>
</div>
)
}
return (
<div>Not a navigation</div>
)
} }
// <div className="flex flex-col gap-3 p-2">
// <button onclick={() => setNavitation(0)}>{"<"}</button>
// <div>
// <img src={selectedStation?.avatar} />
// <h2>{selectedStation?.title}</h2>
// <p>Live</p>
// <div>
// {/* progress bar */}
// </div>
//
// <div>
// {/* play pause button */}
// </div>
// </div>
// </div>
//
//
//
//{/*<!-- Music Controls -->*/}
// <div className="mt-6 flex justify-center items-center">
// <button className="p-3 rounded-full bg-gray-200 hover:bg-gray-300 focus:outline-none">
// <svg width="64px" height="64px" viewBox="0 0 24 24" className="w-4 h-4 text-gray-600" fill="none" xmlns="http://www.w3.org/2000/svg" transform="matrix(-1, 0, 0, 1, 0, 0)">
// <g id="SVGRepo_bgCarrier" stroke-width="0"></g>
// <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
// <g id="SVGRepo_iconCarrier">
// <path d="M16.6598 14.6474C18.4467 13.4935 18.4467 10.5065 16.6598 9.35258L5.87083 2.38548C4.13419 1.26402 2 2.72368 2 5.0329V18.9671C2 21.2763 4.13419 22.736 5.87083 21.6145L16.6598 14.6474Z" fill="#000000"></path>
// <path d="M22.75 5C22.75 4.58579 22.4142 4.25 22 4.25C21.5858 4.25 21.25 4.58579 21.25 5V19C21.25 19.4142 21.5858 19.75 22 19.75C22.4142 19.75 22.75 19.4142 22.75 19V5Z" fill="#000000"></path>
// </g>
// </svg>
// </button>
// <button className="p-4 rounded-full bg-gray-200 hover:bg-gray-300 focus:outline-none mx-4">
// <svg width="64px" height="64px" viewBox="0 0 24 24" className="w-6 h-6 text-gray-600" fill="none" xmlns="http://www.w3.org/2000/svg">
// <g id="SVGRepo_bgCarrier" stroke-width="0"></g>
// <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
// <g id="SVGRepo_iconCarrier">
// <path d="M2 6C2 4.11438 2 3.17157 2.58579 2.58579C3.17157 2 4.11438 2 6 2C7.88562 2 8.82843 2 9.41421 2.58579C10 3.17157 10 4.11438 10 6V18C10 19.8856 10 20.8284 9.41421 21.4142C8.82843 22 7.88562 22 6 22C4.11438 22 3.17157 22 2.58579 21.4142C2 20.8284 2 19.8856 2 18V6Z" fill="#000000"></path>
// <path d="M14 6C14 4.11438 14 3.17157 14.5858 2.58579C15.1716 2 16.1144 2 18 2C19.8856 2 20.8284 2 21.4142 2.58579C22 3.17157 22 4.11438 22 6V18C22 19.8856 22 20.8284 21.4142 21.4142C20.8284 22 19.8856 22 18 22C16.1144 22 15.1716 22 14.5858 21.4142C14 20.8284 14 19.8856 14 18V6Z" fill="#000000"></path>
// </g>
// </svg>
// </button>
// <button className="p-3 rounded-full bg-gray-200 hover:bg-gray-300 focus:outline-none">
// <svg width="64px" height="64px" viewBox="0 0 24 24" className="w-4 h-4 text-gray-600" fill="none" xmlns="http://www.w3.org/2000/svg">
// <g id="SVGRepo_bgCarrier" stroke-width="0"></g>
// <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
// <g id="SVGRepo_iconCarrier">
// <path d="M16.6598 14.6474C18.4467 13.4935 18.4467 10.5065 16.6598 9.35258L5.87083 2.38548C4.13419 1.26402 2 2.72368 2 5.0329V18.9671C2 21.2763 4.13419 22.736 5.87083 21.6145L16.6598 14.6474Z" fill="#000000"></path>
// <path d="M22.75 5C22.75 4.58579 22.4142 4.25 22 4.25C21.5858 4.25 21.25 4.58579 21.25 5V19C21.25 19.4142 21.5858 19.75 22 19.75C22.4142 19.75 22.75 19.4142 22.75 19V5Z" fill="#000000"></path>
// </g>
// </svg>
// </button>
// </div>
// {/*<!-- Progress Bar -->*/}
// <div className="mt-6 bg-gray-200 h-2 rounded-full">
// <div className="bg-teal-500 h-2 rounded-full w-1/2"></div>
// </div>
// {/*<!-- Time Information -->*/}
// <div className="flex justify-between mt-2 text-sm text-gray-600">
// <span>1:57</span>
// <span>3:53</span>
// </div>

13
src/ProviderWrapper.tsx Normal file
View File

@ -0,0 +1,13 @@
import { App } from "./App";
import { StationsContextProvider } from "./providers/StationsProvider";
import { StorageContextProvider } from "./providers/StorageProvider";
export default function ProviderWrapper() {
return (
<StationsContextProvider>
<StorageContextProvider>
<App />
</StorageContextProvider>
</StationsContextProvider>
)
}

View File

@ -7,9 +7,11 @@ export enum Navs {
export function useNavigator() { export function useNavigator() {
const [nav, setNav] = useState<Navs>(Navs.MAIN) const [nav, setNav] = useState<Navs>(Navs.MAIN)
function _setNavitation(newNav: Navs) { function _setNavitation(newNav: Navs) {
setNav(newNav) setNav(newNav)
} }
return { return {
nav, nav,
setNavitation: _setNavitation, setNavitation: _setNavitation,

View File

@ -1,6 +1,6 @@
import "./styles.css"; import "./styles.css"
import { mount } from "kaioken"; import { mount } from "kaioken"
import { App } from "./App"; import ProviderWrapper from "./ProviderWrapper"
const root = document.getElementById("root")!; const root = document.getElementById("root")!
mount(App, root); mount(ProviderWrapper, root)

45
src/pages/Add.tsx Normal file
View File

@ -0,0 +1,45 @@
import { writeTextFile } from "@tauri-apps/api/fs"
import { Navs, useNavigator } from "../hooks/useNavigator"
import { Station, useStationsProvider } from "../providers/StationsProvider"
import { useModel } from "kaioken"
import { useStorageContext } from "../providers/StorageProvider"
export default function Add() {
const { setNavitation } = useNavigator()
const { setStations } = useStationsProvider()
const { appDataDirRef } = useStorageContext()
const [titleRef, title,] = useModel<HTMLInputElement, string>('')
const [streamRef, streamUrl,] = useModel<HTMLInputElement, string>('')
const [avatarRef, avatarUrl,] = useModel<HTMLInputElement, string>('')
function _handleStationAdd() {
const data: Station = {
url: streamUrl,
avatar: avatarUrl,
title: title
}
setStations(prev => {
const newStations = [...(prev ?? []), data]
// write file
writeTextFile(appDataDirRef.current!, JSON.stringify(newStations))
return newStations
})
setNavitation(Navs.MAIN)
}
return (
<div >
<button onclick={() => setNavitation(0)} className="ml-2 px-2 py-1 hover:bg-gray-300 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
</svg>
</button>
<div className="flex flex-col gap-3 p-2">
<input ref={titleRef} className="rounded p-1" ariaLabel={"title"} placeholder="Title" />
<input ref={streamRef} className="rounded p-1" ariaLabel={"url"} placeholder="Stream URL" />
<input ref={avatarRef} className="rounded p-1" ariaLabel={"avatar"} placeholder="Image URL" />
<button onclick={_handleStationAdd} className="rounded bg-blue-400 p-1">Save</button>
</div>
</div>
)
}

47
src/pages/Main.tsx Normal file
View File

@ -0,0 +1,47 @@
import { Navs, useNavigator } from "../hooks/useNavigator"
import { Station, useStationsProvider } from "../providers/StationsProvider"
export default function Main() {
const { setSelectedStation, stations } = useStationsProvider()
const { setNavitation } = useNavigator()
function _handleStationClick(station: Station) {
setSelectedStation(station)
setNavitation(Navs.PLAYER)
}
if (!stations?.length) {
return (
<div className="p-4 flex flex-col align-between h-full gap-2">
<h2 className="text-black text-opacity-50 font-bold text-center">No Stations Added</h2>
<button className="bg-white rounded-xl p-2" onclick={() => setNavitation(1)}>+</button>
</div>
)
}
return (
<div>
<div className="flex justify-end">
<button className="mr-4 px-2 py-1 hover:bg-gray-300 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
</button>
</div>
<div className="p-4 flex flex-col align-between h-full gap-2">
{stations.map((s) => (
<button onclick={() => _handleStationClick(s)} className="paper p-2 flex flex-row border gap-4">
<img src={s.avatar} width={40} height={40} className="w-20 rounded-xl" />
<div className="flex flex-col gap-2 w-full">
<h2 className="text-xl">{s.title}</h2>
<button className="border border-red-500 rounded w-full text-red-500 px-4">Delete</button>
</div>
</button>
))}
<button className="bg-white rounded-xl p-2" onclick={() => setNavitation(1)}>+</button>
</div>
</div>
)
}

34
src/pages/Player.tsx Normal file
View File

@ -0,0 +1,34 @@
import { Navs, useNavigator } from "../hooks/useNavigator"
import { useStationsProvider } from "../providers/StationsProvider"
export default function Player() {
const { setNavitation } = useNavigator()
const { setSelectedStation, selectedStation } = useStationsProvider()
function _handlePlayerBackClick() {
setNavitation(Navs.MAIN)
setSelectedStation(null)
}
return (
<div >
<button onclick={_handlePlayerBackClick} className="ml-2 px-2 py-1 hover:bg-gray-300 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
</svg>
</button>
<div className="flex justify-center">
<div className="p-8 w-80">
{/*<!-- Album Cover -->*/}
<img src={selectedStation?.avatar} alt={selectedStation?.title} className="w-64 h-64 mx-auto rounded-lg mb-4 shadow-md shadow-black-200" />
{/*<!-- Song Title -->*/}
<h2 className="text-xl font-semibold text-center">{selectedStation?.title}</h2>
{/*<!-- Artist Name -->*/}
<p className="text-gray-600 text-sm text-center">Live Radio</p>
<br />
<audio autoplay controls src={selectedStation?.url}></audio>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,37 @@
import { createContext, useContext, useState } from 'kaioken';
interface StationsContextType {
stations: Station[] | null
setStations: (value: Kaioken.StateSetter<Station[] | null>) => void
selectedStation: Station | null
setSelectedStation: (value: Kaioken.StateSetter<Station | null>) => void
}
const StationsContext = createContext<StationsContextType>({} as StationsContextType);
export const useStationsProvider = () => useContext(StationsContext)
interface MyContextProviderProps {
children?: Kaioken.VNode | Kaioken.VNode[]
}
export function StationsContextProvider(props: MyContextProviderProps) {
const [stations, setStations] = useState<Station[] | null>(null)
const [selectedStation, setSelectedStation] = useState<Station | null>(null)
const value = {
stations, setStations,
selectedStation, setSelectedStation
};
return (
<StationsContext.Provider value={value}>
{props.children}
</StationsContext.Provider>
);
}
export interface Station {
url: string
avatar: string
title: string
}

View File

@ -0,0 +1,52 @@
import { exists, readTextFile, writeTextFile } from '@tauri-apps/api/fs';
import { appDataDir } from '@tauri-apps/api/path';
import { createContext, useContext, useRef } from 'kaioken';
import { Station } from './StationsProvider';
interface StorageContextType {
appDataDirRef: Kaioken.Ref<string>,
getStationsFile: () => Promise<Station[] | undefined>
}
const StorageContext = createContext<StorageContextType>({} as StorageContextType);
export const useStorageContext = () => useContext(StorageContext)
export function StorageContextProvider(props: any) {
const appDataDirRef = useRef<string>(null)
async function _getStationsFile(): Promise<Station[] | undefined> {
let dir: null | string = null
try {
dir = await appDataDir()
} catch (err) {
console.error(err)
return undefined
}
if (!dir) return undefined
const path = `${dir}/stations.json`
if (!(await exists(path)))
return await _createStationsFile(path)
const jsonString = await readTextFile(path)
const json = JSON.parse(jsonString) as Station[]
appDataDirRef.current = path
return json
}
async function _createStationsFile(path: string): Promise<Station[]> {
await writeTextFile(path, "[]", { append: false })
return []
}
const value: StorageContextType = {
appDataDirRef,
getStationsFile: _getStationsFile
};
return (
<StorageContext.Provider value={value}>
{props.children}
</StorageContext.Provider>
);
}