mirror of
https://github.com/fankes/beszel.git
synced 2025-10-18 17:29:28 +08:00
react + updates
This commit is contained in:
26
main.go
26
main.go
@@ -112,7 +112,7 @@ func main() {
|
||||
|
||||
// start ticker for server updates
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
// go serverUpdateTicker()
|
||||
go serverUpdateTicker()
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -128,7 +128,7 @@ func main() {
|
||||
}
|
||||
|
||||
func serverUpdateTicker() {
|
||||
ticker := time.NewTicker(15 * time.Second)
|
||||
ticker := time.NewTicker(60 * time.Second)
|
||||
for range ticker.C {
|
||||
updateServers()
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func updateServers() {
|
||||
|
||||
records := []*models.Record{}
|
||||
if err := query.All(&records); err != nil {
|
||||
app.Logger().Error("Failed to get servers: ", "err", err)
|
||||
app.Logger().Error("Failed to get servers: ", "err", err.Error())
|
||||
// return nil, err
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ func updateServer(record *models.Record) {
|
||||
}
|
||||
client, err := getServerConnection(&server)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to connect:", "err", err, "server", server.Ip, "port", server.Port)
|
||||
app.Logger().Error("Failed to connect:", "err", err.Error(), "server", server.Ip, "port", server.Port)
|
||||
// todo update record to not connected
|
||||
record.Set("active", false)
|
||||
delete(serverConnections, record.Id)
|
||||
@@ -178,7 +178,7 @@ func updateServer(record *models.Record) {
|
||||
// get server stats from agent
|
||||
systemData, err := requestJson(&server)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to get server stats: ", "err", err)
|
||||
app.Logger().Error("Failed to get server stats: ", "err", err.Error())
|
||||
record.Set("active", false)
|
||||
if server.Client != nil {
|
||||
server.Client.Close()
|
||||
@@ -190,7 +190,7 @@ func updateServer(record *models.Record) {
|
||||
record.Set("active", true)
|
||||
record.Set("stats", systemData.System)
|
||||
if err := app.Dao().SaveRecord(record); err != nil {
|
||||
app.Logger().Error("Failed to update record: ", "err", err)
|
||||
app.Logger().Error("Failed to update record: ", "err", err.Error())
|
||||
}
|
||||
// add new system_stats record
|
||||
system_stats, _ := app.Dao().FindCollectionByNameOrId("system_stats")
|
||||
@@ -198,7 +198,7 @@ func updateServer(record *models.Record) {
|
||||
system_stats_record.Set("system", record.Id)
|
||||
system_stats_record.Set("stats", systemData.System)
|
||||
if err := app.Dao().SaveRecord(system_stats_record); err != nil {
|
||||
app.Logger().Error("Failed to save record: ", "err", err)
|
||||
app.Logger().Error("Failed to save record: ", "err", err.Error())
|
||||
}
|
||||
// add new container_stats record
|
||||
if len(systemData.Containers) > 0 {
|
||||
@@ -207,7 +207,7 @@ func updateServer(record *models.Record) {
|
||||
container_stats_record.Set("system", record.Id)
|
||||
container_stats_record.Set("stats", systemData.Containers)
|
||||
if err := app.Dao().SaveRecord(container_stats_record); err != nil {
|
||||
app.Logger().Error("Failed to save record: ", "err", err)
|
||||
app.Logger().Error("Failed to save record: ", "err", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,7 +216,7 @@ func getServerConnection(server *Server) (*ssh.Client, error) {
|
||||
// app.Logger().Debug("new ssh connection", "server", server.Ip)
|
||||
key, err := getSSHKey()
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to get SSH key: ", "err", err)
|
||||
app.Logger().Error("Failed to get SSH key: ", "err", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
@@ -284,27 +284,27 @@ func getSSHKey() ([]byte, error) {
|
||||
// Generate the Ed25519 key pair
|
||||
pubKey, privKey, err := ed25519.GenerateKey(nil)
|
||||
if err != nil {
|
||||
// app.Logger().Error("Error generating key pair:", "err", err)
|
||||
// app.Logger().Error("Error generating key pair:", "err", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the private key in OpenSSH format
|
||||
privKeyBytes, err := ssh.MarshalPrivateKey(privKey, "")
|
||||
if err != nil {
|
||||
// app.Logger().Error("Error marshaling private key:", "err", err)
|
||||
// app.Logger().Error("Error marshaling private key:", "err", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save the private key to a file
|
||||
privateFile, err := os.Create("./pb_data/id_ed25519")
|
||||
if err != nil {
|
||||
// app.Logger().Error("Error creating private key file:", "err", err)
|
||||
// app.Logger().Error("Error creating private key file:", "err", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
defer privateFile.Close()
|
||||
|
||||
if err := pem.Encode(privateFile, privKeyBytes); err != nil {
|
||||
// app.Logger().Error("Error writing private key to file:", "err", err)
|
||||
// app.Logger().Error("Error writing private key to file:", "err", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
BIN
site/bun.lockb
BIN
site/bun.lockb
Binary file not shown.
@@ -9,29 +9,33 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nanostores/preact": "^0.5.1",
|
||||
"@nanostores/react": "^0.7.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@tanstack/react-table": "^8.19.2",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"lucide-react": "^0.401.0",
|
||||
"nanostores": "^0.10.3",
|
||||
"pocketbase": "^0.21.3",
|
||||
"preact": "^10.22.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"valibot": "^0.36.0",
|
||||
"wouter-preact": "^3.3.1"
|
||||
"wouter": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "^2.8.3",
|
||||
"@types/bun": "^1.1.6",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.39",
|
||||
"tailwindcss": "^3.4.4",
|
||||
|
@@ -14,29 +14,43 @@ import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { $publicKey, pb } from '@/lib/stores'
|
||||
import { ClipboardIcon, Plus } from 'lucide-react'
|
||||
import { MutableRef, useRef, useState } from 'preact/hooks'
|
||||
|
||||
function copyDockerCompose(port: string) {
|
||||
console.log('copying docker compose')
|
||||
navigator.clipboard.writeText(`services:
|
||||
agent:
|
||||
image: 'henrygd/monitor-agent'
|
||||
container_name: 'monitor-agent'
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '${port}:45876'
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock`)
|
||||
}
|
||||
import { useState, useRef, MutableRefObject, useEffect } from 'react'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { copyToClipboard } from '@/lib/utils'
|
||||
|
||||
export function AddServerButton() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const port = useRef() as MutableRef<HTMLInputElement>
|
||||
const port = useRef() as MutableRefObject<HTMLInputElement>
|
||||
const publicKey = useStore($publicKey)
|
||||
|
||||
function copyDockerCompose(port: string) {
|
||||
copyToClipboard(`services:
|
||||
agent:
|
||||
image: 'henrygd/monitor-agent'
|
||||
container_name: 'monitor-agent'
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '${port}:45876'
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock`)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (publicKey || !open) {
|
||||
return
|
||||
}
|
||||
// get public key
|
||||
pb.send('/getkey', {}).then(({ key }) => {
|
||||
console.log('key', key)
|
||||
$publicKey.set(key)
|
||||
})
|
||||
}, [open])
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
const formData = new FormData(e.target as HTMLFormElement)
|
||||
const stats = {
|
||||
const data = Object.fromEntries(formData) as Record<string, any>
|
||||
data.stats = {
|
||||
cpu: 0,
|
||||
mem: 0,
|
||||
memUsed: 0,
|
||||
@@ -45,16 +59,10 @@ export function AddServerButton() {
|
||||
diskUsed: 0,
|
||||
diskPct: 0,
|
||||
}
|
||||
const data = { stats } as Record<string, any>
|
||||
for (const [key, value] of formData) {
|
||||
data[key.slice(2)] = value
|
||||
}
|
||||
console.log(data)
|
||||
|
||||
try {
|
||||
const record = await pb.collection('systems').create(data)
|
||||
console.log(record)
|
||||
setOpen(false)
|
||||
await pb.collection('systems').create(data)
|
||||
// console.log(record)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
@@ -73,57 +81,56 @@ export function AddServerButton() {
|
||||
<DialogTitle>Add New Server</DialogTitle>
|
||||
<DialogDescription>
|
||||
The agent must be running on the server to connect. Copy the{' '}
|
||||
<code class="bg-muted px-1 rounded-sm">docker-compose.yml</code> for the agent below.
|
||||
<code className="bg-muted px-1 rounded-sm">docker-compose.yml</code> for the agent
|
||||
below.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form name="testing" action="/" onSubmit={handleSubmit}>
|
||||
<form name="testing" action="/" onSubmit={handleSubmit as any}>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="s-name" className="text-right">
|
||||
<Label htmlFor="name" className="text-right">
|
||||
Name
|
||||
</Label>
|
||||
<Input id="s-name" name="s-name" className="col-span-3" required />
|
||||
<Input id="name" name="name" className="col-span-3" required />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="s-ip" className="text-right">
|
||||
<Label htmlFor="ip" className="text-right">
|
||||
IP Address
|
||||
</Label>
|
||||
<Input id="s-ip" name="s-ip" className="col-span-3" required />
|
||||
<Input id="ip" name="ip" className="col-span-3" required />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="s-port" className="text-right">
|
||||
<Label htmlFor="port" className="text-right">
|
||||
Port
|
||||
</Label>
|
||||
<Input
|
||||
ref={port}
|
||||
name="s-port"
|
||||
id="s-port"
|
||||
name="port"
|
||||
id="port"
|
||||
defaultValue="45876"
|
||||
className="col-span-3"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4 relative">
|
||||
<Label for="s-port" className="text-right">
|
||||
<Label htmlFor="pkey" className="text-right">
|
||||
Public Key
|
||||
</Label>
|
||||
<Input
|
||||
readonly
|
||||
name="s-port"
|
||||
id="s-port"
|
||||
value={$publicKey.get()}
|
||||
className="col-span-3"
|
||||
required
|
||||
></Input>
|
||||
<Input readOnly id="pkey" value={publicKey} className="col-span-3" required></Input>
|
||||
<div
|
||||
className={
|
||||
'h-6 w-28 bg-gradient-to-r from-transparent to-background to-70% absolute right-1 pointer-events-none'
|
||||
}
|
||||
></div>
|
||||
<TooltipProvider className="z-10">
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button type="button" variant={'link'} className="absolute right-0 z-50">
|
||||
<Button
|
||||
type="button"
|
||||
variant={'link'}
|
||||
className="absolute right-0"
|
||||
onClick={() => copyToClipboard(publicKey)}
|
||||
>
|
||||
<ClipboardIcon className="h-4 w-4 " />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
@@ -12,9 +12,9 @@ import {
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
} from '@/components/ui/command'
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { navigate } from 'wouter-preact/use-browser-location'
|
||||
import { useStore } from '@nanostores/preact'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { navigate } from 'wouter/use-browser-location'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { $servers } from '@/lib/stores'
|
||||
|
||||
export function CommandPalette() {
|
||||
@@ -24,7 +24,6 @@ export function CommandPalette() {
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
||||
console.log('open')
|
||||
e.preventDefault()
|
||||
setOpen((open) => !open)
|
||||
}
|
||||
@@ -39,7 +38,7 @@ export function CommandPalette() {
|
||||
<CommandInput placeholder="Type a command or search..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup heading="Suggestions">
|
||||
<CommandGroup heading="Suggestions0">
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
navigate('/')
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Link } from 'wouter-preact'
|
||||
import { Link } from 'wouter'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
@@ -29,14 +29,14 @@ export default function LoginPage() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative hidden h-full flex-col bg-muted p-10 text-white dark:border-r lg:flex">
|
||||
<div className="relative hidden h-full flex-col bg-muted p-10 text-white dark:border-r lg:flex">
|
||||
<div
|
||||
class="absolute inset-0 bg-slate-900 bg-cover opacity-80"
|
||||
className="absolute inset-0 bg-slate-900 bg-cover opacity-80"
|
||||
style={{
|
||||
backgroundImage: `url(https://directus.cloud/assets/waves.2b156907.svg)`,
|
||||
}}
|
||||
></div>
|
||||
<div class="relative z-20 flex gap-2 items-center text-lg font-medium ml-auto">
|
||||
<div className="relative z-20 flex gap-2 items-center text-lg font-medium ml-auto">
|
||||
placeholder
|
||||
<img
|
||||
className={'w-6 h-6'}
|
||||
@@ -44,13 +44,13 @@ export default function LoginPage() {
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
{/* <div class="relative z-20 mt-auto">
|
||||
<blockquote class="space-y-2">
|
||||
<p class="text-lg">
|
||||
{/* <div className="relative z-20 mt-auto">
|
||||
<blockquote className="space-y-2">
|
||||
<p className="text-lg">
|
||||
“This library has saved me countless hours of work and helped me deliver stunning
|
||||
designs to my clients faster than ever before.”
|
||||
</p>
|
||||
<footer class="text-sm">Sofia Davis</footer>
|
||||
<footer className="text-sm">Sofia Davis</footer>
|
||||
</blockquote>
|
||||
</div> */}
|
||||
</div>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { useEffect } from 'preact/hooks'
|
||||
import { useEffect } from 'react'
|
||||
import { $servers, pb } from '@/lib/stores'
|
||||
import { DataTable } from '../server-table/data-table'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
||||
import { useStore } from '@nanostores/preact'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { SystemRecord } from '@/types'
|
||||
|
||||
export function Home() {
|
||||
@@ -43,7 +43,9 @@ export function Home() {
|
||||
}
|
||||
$servers.set(newServers)
|
||||
})
|
||||
return () => pb.collection('systems').unsubscribe('*')
|
||||
return () => {
|
||||
pb.collection('systems').unsubscribe('*')
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { pb } from '@/lib/stores'
|
||||
import { SystemRecord } from '@/types'
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { useRoute } from 'wouter-preact'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRoute } from 'wouter'
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
|
||||
|
||||
export function ServerDetail() {
|
||||
@@ -18,7 +18,7 @@ export function ServerDetail() {
|
||||
.then((record) => {
|
||||
setServer(record)
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@@ -45,13 +45,13 @@ import {
|
||||
|
||||
import { SystemRecord } from '@/types'
|
||||
import { MoreHorizontal, ArrowUpDown, Copy, RefreshCcw } from 'lucide-react'
|
||||
import { useMemo, useState } from 'preact/hooks'
|
||||
import { navigate } from 'wouter-preact/use-browser-location'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { navigate } from 'wouter/use-browser-location'
|
||||
import { $servers, pb } from '@/lib/stores'
|
||||
import { useStore } from '@nanostores/preact'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { AddServerButton } from '../add-server'
|
||||
import clsx from 'clsx'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn, copyToClipboard } from '@/lib/utils'
|
||||
|
||||
function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
||||
const val = info.getValue() as number
|
||||
@@ -62,14 +62,14 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
||||
color = 'yellow'
|
||||
}
|
||||
return (
|
||||
<div class="flex gap-2 items-center">
|
||||
<span class="grow block bg-muted h-4 relative rounded-sm overflow-hidden">
|
||||
<div className="flex gap-2 items-center">
|
||||
<span className="grow block bg-muted h-4 relative rounded-sm overflow-hidden">
|
||||
<span
|
||||
className={clsx('absolute inset-0 w-full h-full origin-left', `bg-${color}-500`)}
|
||||
style={{ transform: `scalex(${val}%)` }}
|
||||
></span>
|
||||
</span>
|
||||
<span class="w-16">{val.toFixed(2)}%</span>
|
||||
<span className="w-16">{val.toFixed(2)}%</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -100,7 +100,7 @@ export function DataTable() {
|
||||
// size: 70,
|
||||
accessorKey: 'name',
|
||||
cell: (info) => (
|
||||
<span className="flex gap-1.5 items-center text-base">
|
||||
<span className="flex gap-1 items-center text-base">
|
||||
<span
|
||||
className={clsx(
|
||||
'w-2.5 h-2.5 block left-0 rounded-full',
|
||||
@@ -108,14 +108,14 @@ export function DataTable() {
|
||||
)}
|
||||
style={{ marginBottom: '-1px' }}
|
||||
></span>
|
||||
{info.getValue() as string}
|
||||
<button
|
||||
title={`Copy "${info.getValue() as string}" to clipboard`}
|
||||
class="opacity-50 hover:opacity-70 active:opacity-100 duration-75"
|
||||
onClick={() => navigator.clipboard.writeText(info.getValue() as string)}
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
className="text-foreground/80 h-7 px-2 gap-1.5"
|
||||
onClick={() => copyToClipboard(info.getValue() as string)}
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5 " />
|
||||
</button>
|
||||
{info.getValue() as string}
|
||||
<Copy className="h-3.5 w-3.5 opacity-70" />
|
||||
</Button>
|
||||
</span>
|
||||
),
|
||||
header: ({ column }) => sortableHeader(column, 'Server'),
|
||||
@@ -143,7 +143,7 @@ export function DataTable() {
|
||||
const system = row.original
|
||||
|
||||
return (
|
||||
<div class={'flex justify-end'}>
|
||||
<div className={'flex justify-end'}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
@@ -288,7 +288,8 @@ export function DataTable() {
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete all current records for{' '}
|
||||
<code class={'bg-muted rounded-sm px-1'}>{deleteServer.name}</code> from the database.
|
||||
<code className={'bg-muted rounded-sm px-1'}>{deleteServer.name}</code> from the
|
||||
database.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
@@ -1,23 +0,0 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
|
||||
export function TooltipDemo() {
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
onMouseEnter={() => console.log('hovered')}
|
||||
onMouseLeave={() => console.log('unhovered')}
|
||||
variant="outline"
|
||||
>
|
||||
Hover
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent hideWhenDetached={true} className="pointer-events-none">
|
||||
<p>Add to library</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
import * as React from 'react'
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
@@ -13,108 +13,90 @@ const DialogPortal = DialogPrimitive.Portal
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
DialogHeader.displayName = 'DialogHeader'
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
DialogFooter.displayName = 'DialogFooter'
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
|
127
site/src/components/ui/toast.tsx
Normal file
127
site/src/components/ui/toast.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
33
site/src/components/ui/toaster.tsx
Normal file
33
site/src/components/ui/toaster.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
192
site/src/components/ui/use-toast.ts
Normal file
192
site/src/components/ui/use-toast.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
@@ -3,22 +3,22 @@
|
||||
@tailwind utilities;
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 7.69% 97.45%;
|
||||
--foreground: 0 0% 0%;
|
||||
--card: 0 0% 100%;
|
||||
--background: 30 8% 97.45%;
|
||||
--foreground: 30 0% 0%;
|
||||
--card: 30 0% 100%;
|
||||
--card-foreground: 240 6.67% 2.94%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.92%;
|
||||
--popover: 30 0% 100%;
|
||||
--popover-foreground: 240 10% 6.2%;
|
||||
--primary: 240 5.88% 10%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--primary-foreground: 30 0% 100%;
|
||||
--secondary: 240 4.76% 95.88%;
|
||||
--secondary-foreground: 240 5.88% 10%;
|
||||
--muted: 26 6% 90%;
|
||||
--muted: 26 6% 94%;
|
||||
--muted-foreground: 24 2.79% 35.1%;
|
||||
--accent: 20 23.08% 93%;
|
||||
--accent: 20 23.08% 94%;
|
||||
--accent-foreground: 240 5.88% 10%;
|
||||
--destructive: 0 65.33% 44.12%;
|
||||
--destructive-foreground: 0 0% 98.04%;
|
||||
--destructive: 30 67% 46%;
|
||||
--destructive-foreground: 30 0% 98.04%;
|
||||
--border: 30 8.11% 85.49%;
|
||||
--input: 30 4.29% 72.55%;
|
||||
--ring: 30 3.97% 49.41%;
|
||||
@@ -26,11 +26,11 @@
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 3.92%;
|
||||
--background: 240 10% 6.2%;
|
||||
--foreground: 0 0% 98.04%;
|
||||
--card: 240 8.57% 6.86%;
|
||||
--card: 240 8.57% 8%;
|
||||
--card-foreground: 0 0% 98.04%;
|
||||
--popover: 240 10% 3.92%;
|
||||
--popover: 240 10% 6.2%;
|
||||
--popover-foreground: 0 0% 98.04%;
|
||||
--primary: 0 0% 98.04%;
|
||||
--primary-foreground: 240 5.88% 10%;
|
||||
@@ -42,9 +42,9 @@
|
||||
--accent-foreground: 0 0% 98.04%;
|
||||
--destructive: 0 56.48% 42.35%;
|
||||
--destructive-foreground: 0 0% 98.04%;
|
||||
--border: 240 2.86% 13.73%;
|
||||
--border: 240 2.86% 14%;
|
||||
--input: 240 3.7% 15.88%;
|
||||
--ring: 240 4.88% 83.92%;
|
||||
--ring: 240 4.88% 86%;
|
||||
--radius: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,23 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export async function copyToClipboard(content: string) {
|
||||
const duration = 1500
|
||||
try {
|
||||
await navigator.clipboard.writeText(content)
|
||||
toast({
|
||||
duration,
|
||||
description: 'Copied to clipboard',
|
||||
})
|
||||
} catch (e: any) {
|
||||
toast({
|
||||
duration,
|
||||
description: 'Failed to copy',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import './index.css'
|
||||
import { render } from 'preact'
|
||||
import { Route, Switch } from 'wouter-preact'
|
||||
import React, { useEffect } from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { Route, Switch } from 'wouter'
|
||||
import { Home } from './components/routes/home.tsx'
|
||||
import { ThemeProvider } from './components/theme-provider.tsx'
|
||||
import LoginPage from './components/login.tsx'
|
||||
@@ -11,9 +12,9 @@ import { CommandPalette } from './components/command-dialog.tsx'
|
||||
import { cn } from './lib/utils.ts'
|
||||
import { buttonVariants } from './components/ui/button.tsx'
|
||||
import { Github } from 'lucide-react'
|
||||
import { useStore } from '@nanostores/preact'
|
||||
import { useEffect } from 'preact/hooks'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { SystemRecord } from './types'
|
||||
import { Toaster } from './components/ui/toaster.tsx'
|
||||
|
||||
const App = () => {
|
||||
const authenticated = useStore($authenticated)
|
||||
@@ -22,25 +23,18 @@ const App = () => {
|
||||
}
|
||||
|
||||
const Main = () => {
|
||||
// const servers = useStore($servers)
|
||||
|
||||
// get servers
|
||||
useEffect(() => {
|
||||
// get servers
|
||||
pb.collection<SystemRecord>('systems')
|
||||
.getFullList({ sort: '+name' })
|
||||
.then((records) => {
|
||||
$servers.set(records)
|
||||
})
|
||||
|
||||
// get public key
|
||||
pb.send('/getkey', {}).then(({ key }) => {
|
||||
console.log('key', key)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="container mt-7 mb-14">
|
||||
<div class="flex mb-4">
|
||||
<div className="flex mb-4">
|
||||
{/* <Link
|
||||
className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))}
|
||||
href="/"
|
||||
@@ -73,7 +67,13 @@ const Main = () => {
|
||||
<Route>404: No such page!</Route>
|
||||
</Switch>
|
||||
<CommandPalette />
|
||||
<Toaster />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
render(<App />, document.getElementById('app')!)
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('app')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
@@ -9,8 +9,6 @@
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"react": ["./node_modules/preact/compat/"],
|
||||
"react-dom": ["./node_modules/preact/compat/"],
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
@@ -22,7 +20,6 @@
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import preact from '@preact/preset-vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [preact()],
|
||||
plugins: [react()],
|
||||
esbuild: {
|
||||
legalComments: 'external',
|
||||
},
|
||||
|
Reference in New Issue
Block a user