feat: version v0.0.2

This commit is contained in:
Gabe Farrell 2025-06-14 19:14:30 -04:00
parent 0dceaf017a
commit 7ff317756f
36 changed files with 336 additions and 160 deletions

View file

@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query"
import { getActivity, type getActivityArgs } from "api/api"
import { getActivity, type getActivityArgs, type ListenActivityItem } from "api/api"
import Popup from "./Popup"
import { useEffect, useState } from "react"
import { useTheme } from "~/hooks/useTheme"
@ -142,44 +142,55 @@ export default function ActivityGrid({
}
}
const dotSize = 12;
return (
<div className="flex flex-col items-start">
<h2>Activity</h2>
{configurable ?
<ActivityOptsSelector rangeSetter={setRange} currentRange={rangeState} stepSetter={setStep} currentStep={stepState} />
:
const mobileDotSize = 10
const normalDotSize = 12
let vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
let dotSize = vw > 768 ? normalDotSize : mobileDotSize
return (<div className="flex flex-col items-start">
<h2>Activity</h2>
{configurable ? (
<ActivityOptsSelector
rangeSetter={setRange}
currentRange={rangeState}
stepSetter={setStep}
currentStep={stepState}
/>
) : (
''
}
<div className="grid grid-flow-col grid-rows-7 gap-[5px]">
{data.map((item) => (
<div
key={new Date(item.start_time).toString()}
style={{ width: dotSize, height: dotSize }}
)}
<div className="flex flex-row flex-wrap w-[94px] md:w-auto md:grid md:grid-flow-col md:grid-cols-7 md:grid-rows-7 gap-[4px] md:gap-[5px]">
{data.map((item) => (
<div
key={new Date(item.start_time).toString()}
style={{ width: dotSize, height: dotSize }}
>
<Popup
position="top"
space={dotSize}
extraClasses="left-2"
inner={`${new Date(item.start_time).toLocaleDateString()} ${item.listens} plays`}
>
<Popup
position="top"
space={dotSize}
extraClasses="left-2"
inner={`${new Date(item.start_time).toLocaleDateString()} ${item.listens} plays`}
>
<div
style={{
width: dotSize,
height: dotSize,
display: 'inline-block',
background:
item.listens > 0
? LightenDarkenColor(color, getDarkenAmount(item.listens, 100))
: 'var(--color-bg-secondary)',
}}
className={`rounded-[3px] ${item.listens > 0 ? '' : 'border-[0.5px] border-(--color-bg-tertiary)'}`}
></div>
</Popup>
</div>
))}
</div>
<div
style={{
display: 'inline-block',
width: dotSize,
height: dotSize,
background:
item.listens > 0
? LightenDarkenColor(color, getDarkenAmount(item.listens, 100))
: 'var(--color-bg-secondary)',
}}
className={`rounded-[2px] md:rounded-[3px] ${item.listens > 0 ? '' : 'border-[0.5px] border-(--color-bg-tertiary)'}`}
></div>
</Popup>
</div>
))}
</div>
</div>
);
}

View file

@ -5,7 +5,7 @@ export default function Footer() {
return (
<div className="mx-auto py-10 pt-20 color-fg-tertiary text-sm">
<ul className="flex flex-col items-center w-sm justify-around">
<li>Koito {pkg.version}</li>
<li>Koito {import.meta.env.VITE_KOITO_VERSION || pkg.version}</li>
<li><a href="https://github.com/gabehf/koito" target="_blank" className="link-underline">View the source on GitHub <ExternalLinkIcon className='inline mb-1' size={14}/></a></li>
</ul>
</div>

View file

@ -21,7 +21,7 @@ export default function LastPlays(props: Props) {
if (isPending) {
return (
<div className="w-[500px]">
<div className="w-[400px] sm:w-[500px]">
<h2>Last Played</h2>
<p>Loading...</p>
</div>
@ -43,8 +43,8 @@ export default function LastPlays(props: Props) {
<tbody>
{data.items.map((item) => (
<tr key={`last_listen_${item.time}`}>
<td className="color-fg-tertiary pr-4 text-sm" title={new Date(item.time).toString()}>{timeSince(new Date(item.time))}</td>
<td className="text-ellipsis overflow-hidden max-w-[600px]">
<td className="color-fg-tertiary pr-2 sm:pr-4 text-sm whitespace-nowrap w-0" title={new Date(item.time).toString()}>{timeSince(new Date(item.time))}</td>
<td className="text-ellipsis overflow-hidden max-w-[400px] sm:max-w-[600px]">
{props.hideArtists ? <></> : <><ArtistLinks artists={item.track.artists} /> - </>}
<Link className="hover:text-(--color-fg-secondary)" to={`/track/${item.track.id}`}>{item.track.title}</Link>
</td>

View file

@ -25,8 +25,10 @@ export default function Account() {
setLoading(false)
}
const updateHandler = () => {
setError('')
setSuccess('')
if (password != "" && confirmPw === "") {
setError("confirm your password before submitting")
setError("confirm your new password before submitting")
return
}
setError('')
@ -58,37 +60,44 @@ export default function Account() {
<AsyncButton loading={loading} onClick={logoutHandler}>Logout</AsyncButton>
</div>
<h2>Update User</h2>
<div className="flex flex gap-4">
<input
name="koito-update-username"
type="text"
placeholder="Update username"
className="w-full mx-auto fg bg rounded p-2"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="flex flex gap-4">
<input
name="koito-update-password"
type="password"
placeholder="Update password"
className="w-full mx-auto fg bg rounded p-2"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<input
name="koito-confirm-password"
type="password"
placeholder="Confirm new password"
className="w-full mx-auto fg bg rounded p-2"
value={confirmPw}
onChange={(e) => setConfirmPw(e.target.value)}
/>
</div>
<div className="w-sm">
<AsyncButton loading={loading} onClick={updateHandler}>Submit</AsyncButton>
</div>
<form action="#" onSubmit={(e) => e.preventDefault()} className="flex flex-col gap-4">
<div className="flex flex gap-4">
<input
name="koito-update-username"
type="text"
placeholder="Update username"
className="w-full mx-auto fg bg rounded p-2"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="w-sm">
<AsyncButton loading={loading} onClick={updateHandler}>Submit</AsyncButton>
</div>
</form>
<form action="#" onSubmit={(e) => e.preventDefault()} className="flex flex-col gap-4">
<div className="flex flex gap-4">
<input
name="koito-update-password"
type="password"
placeholder="Update password"
className="w-full mx-auto fg bg rounded p-2"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<input
name="koito-confirm-password"
type="password"
placeholder="Confirm new password"
className="w-full mx-auto fg bg rounded p-2"
value={confirmPw}
onChange={(e) => setConfirmPw(e.target.value)}
/>
</div>
<div className="w-sm">
<AsyncButton loading={loading} onClick={updateHandler}>Submit</AsyncButton>
</div>
</form>
{success != "" && <p className="success">{success}</p>}
{error != "" && <p className="error">{error}</p>}
</div>

View file

@ -16,16 +16,25 @@ export default function SettingsModal({ open, setOpen } : Props) {
const { user } = useAppContext()
const triggerClasses = "px-4 py-2 w-full hover-bg-secondary rounded-md text-start data-[state=active]:bg-[var(--color-bg-secondary)]"
const contentClasses = "w-full px-10 overflow-y-auto"
const contentClasses = "w-full px-2 mt-8 sm:mt-0 sm:px-10 overflow-y-auto"
return (
<Modal h={600} isOpen={open} onClose={() => setOpen(false)} maxW={900}>
<Tabs defaultValue="Appearance" orientation="vertical" className="flex justify-between h-full">
<TabsList className="w-full flex flex-col gap-1 items-start max-w-1/4 rounded-md bg p-2 grow-0">
<Tabs
defaultValue="Appearance"
orientation="vertical" // still vertical, but layout is responsive via Tailwind
className="flex flex-col sm:flex-row h-full"
>
<TabsList className="flex flex-row sm:flex-col gap-1 w-full sm:max-w-1/4 rounded-md bg p-2">
<TabsTrigger className={triggerClasses} value="Appearance">Appearance</TabsTrigger>
<TabsTrigger className={triggerClasses} value="Account">Account</TabsTrigger>
{ user && <TabsTrigger className={triggerClasses} value="API Keys">API Keys</TabsTrigger>}
{user && (
<TabsTrigger className={triggerClasses} value="API Keys">
API Keys
</TabsTrigger>
)}
</TabsList>
<TabsContent value="Account" className={contentClasses}>
<AccountPage />
</TabsContent>

View file

@ -1,22 +1,36 @@
import { ExternalLink, Home, Info } from "lucide-react";
import { ExternalLink, Home, Info } from "lucide-react";
import SidebarSearch from "./SidebarSearch";
import SidebarItem from "./SidebarItem";
import SidebarSettings from "./SidebarSettings";
export default function Sidebar() {
const iconSize = 20;
return (
<div className="z-50 flex flex-col justify-between h-screen border-r-1 border-(--color-bg-tertiary) p-1 py-10 sticky left-0 top-0 bg-(--color-bg)">
<div className="flex flex-col gap-4">
<SidebarItem space={10} to="/" name="Home" onClick={() => {}} modal={<></>}><Home size={iconSize} /></SidebarItem>
<SidebarSearch size={iconSize} />
</div>
<div className="flex flex-col gap-4">
<SidebarItem icon keyHint={<ExternalLink size={14} />} space={22} externalLink to="https://koito.io" name="About" onClick={() => {}} modal={<></>}><Info size={iconSize} /></SidebarItem>
<SidebarSettings size={iconSize} />
<div className="overflow-x-hidden w-full sm:w-auto">
<div className="z-50 flex sm:flex-col justify-between sm:h-screen h-auto sm:w-auto w-full border-b sm:border-b-0 sm:border-r border-(--color-bg-tertiary) pt-2 sm:py-10 sm:px-1 px-4 sticky top-0 sm:left-0 bg-(--color-bg)">
<div className="flex gap-4 sm:flex-col">
<SidebarItem space={10} to="/" name="Home" onClick={() => {}} modal={<></>}>
<Home size={iconSize} />
</SidebarItem>
<SidebarSearch size={iconSize} />
</div>
<div className="flex gap-4 sm:flex-col">
<SidebarItem
icon
keyHint={<ExternalLink size={14} />}
space={22}
externalLink
to="https://koito.io"
name="About"
onClick={() => {}}
modal={<></>}
>
<Info size={iconSize} />
</SidebarItem>
<SidebarSettings size={iconSize} />
</div>
</div>
</div>
);
}
}

View file

@ -12,8 +12,8 @@ export default function ThemeOption({ theme, setTheme }: Props) {
}
return (
<div onClick={() => setTheme(theme.name)} className="rounded-md p-5 hover:cursor-pointer flex gap-4 items-center border-2" style={{background: theme.bg, color: theme.fg, borderColor: theme.bgSecondary}}>
{capitalizeFirstLetter(theme.name)}
<div onClick={() => setTheme(theme.name)} className="rounded-md p-3 sm:p-5 hover:cursor-pointer flex gap-4 items-center border-2" style={{background: theme.bg, color: theme.fg, borderColor: theme.bgSecondary}}>
<div className="text-xs sm:text-sm">{capitalizeFirstLetter(theme.name)}</div>
<div className="w-[50px] h-[30px] rounded-md" style={{background: theme.bgSecondary}}></div>
<div className="w-[50px] h-[30px] rounded-md" style={{background: theme.fgSecondary}}></div>
<div className="w-[50px] h-[30px] rounded-md" style={{background: theme.primary}}></div>