Skip to content

Commit

Permalink
12/24 mode; select from keyboard; timeline colors; theme update
Browse files Browse the repository at this point in the history
  • Loading branch information
vladkens committed Jan 23, 2024
1 parent 4d74cb2 commit 1a699b4
Show file tree
Hide file tree
Showing 9 changed files with 374 additions and 171 deletions.
9 changes: 4 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
"dependencies": {
"@heroicons/react": "^2.1.1",
"@vvo/tzdb": "^6.125.0",
"countries-and-timezones": "^3.6.0",
"clsx": "^2.1.0",
"fuse.js": "^7.0.0",
"jotai": "^2.6.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^2.2.0",
"timezones.json": "^1.7.1"
"react-dom": "^18.2.0"
},
"devDependencies": {
"timezones.json": "^1.7.1",
"countries-and-timezones": "^3.6.0",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react-swc": "^3.5.0",
Expand All @@ -29,7 +29,6 @@
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.4.1",
"tw-colors": "^3.3.1",
"typescript": "^5.2.2",
"vite": "^5.0.8",
"vite-plugin-bundlesize": "^0.0.7"
Expand Down
85 changes: 70 additions & 15 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,103 @@
import { MoonIcon, SunIcon } from "@heroicons/react/16/solid"
import clsx from "clsx"
import { Provider, useAtom } from "jotai"
import { FC } from "react"
import { FC, useState } from "react"
import { SelectPlace } from "./components/SelectPlace"
import { Timeline } from "./components/Timeline"
import { TzListState } from "./state"
import { TzListState, TzModeState } from "./state"
import { getPlaceByTzName } from "./utils/places"

const ChangeTheme: FC = () => {
const [dark, setDark] = useState(() => localStorage.getItem("dark") === "true")

const change = () => {
const isDark = !document.body.classList.contains("dark")
document.body.classList.toggle("dark", isDark)
localStorage.setItem("dark", isDark.toString())
setDark(isDark)
}

return <button onClick={change}>ChangeTheme</button>
const Icon = dark ? SunIcon : MoonIcon

return (
<button onClick={change}>
<Icon className="h-5 w-5 hover:text-blue-500 dark:hover:text-yellow-500" />
</button>
)
}

const Index: FC = () => {
const ChangeTimeView: FC = () => {
const [value, setValue] = useAtom(TzModeState)

const buttons: { value: typeof value; text: string; cls: string }[] = [
{ value: "12" as const, text: "am\npm", cls: "text-[10px]" },
{ value: "24" as const, text: "24", cls: "text-[13px]" },
// { value: "MX" as const, text: "MX", cls: "text-[12px]" },
]

return (
<div className="flex items-center rounded border border-black leading-none dark:border-white">
{buttons.map((x) => (
<button
key={x.value}
onClick={() => setValue(x.value)}
disabled={x.value === value}
className={clsx(
"flex h-[24px] w-[26px] items-center justify-center font-medium",
"border-r border-black last:border-r-0 dark:border-white",
x.cls,
x.value === value && "bg-black text-white dark:bg-white dark:text-black",
)}
>
{x.text}
</button>
))}
</div>
)
}

const Head: FC = () => {
return (
<header className="flex h-[48px] items-center justify-between">
<div>Time24</div>
<div className="flex flex-row gap-5">
<ChangeTimeView />
<ChangeTheme />
</div>
</header>
)
}

const Main: FC = () => {
const [tzs, setTzs] = useAtom(TzListState)
const places = tzs.map((tz) => getPlaceByTzName(tz))

return (
<div className="flex flex-col gap-2.5 py-2.5">
<div className="flex flex-row gap-2.5">
<SelectPlace
values={places}
onChange={(place) => setTzs((old) => [...old, place.tzName])}
/>
<ChangeTheme />
<main className="flex flex-col rounded-lg border bg-card text-card-content shadow">
<div className="flex items-center gap-2.5 bg-body/30 px-4 py-2.5">
<div className="w-full max-w-[228px]">
<SelectPlace
values={places}
onChange={(place) => setTzs((old) => [...old, place.tzName])}
/>
</div>
</div>

<div className="flex flex-col gap-2">
<div className="flex flex-col py-2.5">
{places.map((x, idx) => (
<Timeline key={`${idx}-${x.tzAbbr}`} place={x} />
))}
</div>
</div>
</main>
)
}

export const App: FC = () => {
return (
<Provider>
<div className="mx-auto w-[1020px]">
<Index />
<div className="mx-auto w-[1040px]">
<Head />
<Main />
</div>
</Provider>
)
Expand Down
86 changes: 63 additions & 23 deletions src/components/SelectPlace.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TimeZone, getTimeZones } from "@vvo/tzdb"
import clsx from "clsx"
import Fuse from "fuse.js"
import { FC, useState } from "react"
import { FC, useEffect, useState } from "react"
import { Place, tzToPlace } from "../utils/places"

type SelectPlaceProps = {
Expand All @@ -23,29 +24,68 @@ export const SelectPlace: FC<SelectPlaceProps> = ({ values, onChange }) => {
setValue("")
}

const [cursorIndex, setCursorIndex] = useState<number>(0)

useEffect(() => {
setCursorIndex(0)
}, [value])

useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.code === "Enter" || e.code === "Space") {
e.preventDefault()
const item = options[cursorIndex]
if (item) handleSelect(item)
return
}

if (e.code === "ArrowDown" || e.code === "ArrowUp") {
e.preventDefault()
const next = cursorIndex + (e.code === "ArrowDown" ? 1 : -1)
setCursorIndex(next < 0 ? options.length - 1 : next % options.length)
return
}
}

document.addEventListener("keydown", handler)
return () => document.removeEventListener("keydown", handler)
}, [options, cursorIndex])

return (
<div className="relative">
<div className="relative">
<input
type="text"
placeholder="Place or timezone"
value={value}
onChange={(e) => setValue(e.target?.value ?? "")}
className="h-[32px] rounded border px-1.5"
/>
{/* <button className="absolute right-[20px] text-[20px] font-bold">&times;</button> */}
</div>
<div className="absolute mt-0.5 rounded-md border bg-white leading-none">
{options.map((x, idx) => (
<button
key={`${idx}-${x.abbreviation}`}
onClick={() => handleSelect(x)}
className="flex h-[24px] w-full flex-row items-center justify-between gap-2.5 px-1.5 py-1 hover:bg-gray-100"
>
{x.countryName}, {x.mainCities[0]} <span className="text-xs">({x.abbreviation})</span>
</button>
))}
</div>
<div className="relative w-full">
<input
type="text"
placeholder="Enter place or timezone"
value={value}
onChange={(e) => setValue(e.target?.value ?? "")}
className={clsx(
"h-[32px] w-full rounded border bg-card px-1.5 text-card-content",
"focus:border-blue-500 focus:outline-none focus:ring-1",
"border-card-content/30 placeholder:text-sm",
)}
/>
{options.length > 0 && (
<div className="absolute z-[100] mt-0.5 w-full rounded-md border bg-card">
{options.map((x, idx) => (
<button
key={`${idx}-${x.abbreviation}`}
onClick={() => handleSelect(x)}
onMouseOver={() => setCursorIndex(idx)}
className={clsx(
"flex w-full items-center justify-between gap-2.5",
"h-[32px] px-1.5 py-1",
"text-sm leading-none",
idx === cursorIndex && "bg-card-content/30",
)}
>
<div className="line-clamp-1 grow text-left">
{x.mainCities[0].trim()}, {x.countryName}
</div>
<div className="shrink-0">{x.abbreviation}</div>
</button>
))}
</div>
)}
</div>
)
}
Loading

0 comments on commit 1a699b4

Please sign in to comment.