added first searchmodal draft
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { apiFetch } from "./client";
|
||||
import type { AuthResponse } from "../types/connection";
|
||||
import type { SearchRequest } from "../types/search";
|
||||
|
||||
export const search = (req: SearchRequest) =>
|
||||
apiFetch<AuthResponse>("/search", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
.action-btn {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 7px 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.2s, opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&--connect,
|
||||
&--search {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&--disconnect {
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border-color: var(--border);
|
||||
|
||||
&:hover {
|
||||
border-color: #ef4444;
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import './ActionButton.scss'
|
||||
|
||||
type Variant = 'connect' | 'disconnect' | 'search'
|
||||
|
||||
interface Props {
|
||||
variant: Variant
|
||||
onClick: () => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function ActionButton({ variant, onClick, children }: Readonly<Props>) {
|
||||
return (
|
||||
<button className={`action-btn action-btn--${variant}`} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import type { ConnectionStatus } from "../types/connection"
|
||||
import { ServiceCard } from "./ServiceCard"
|
||||
import { ConnectModal } from "./ConnectModal"
|
||||
import "./Dashboard.scss"
|
||||
import { SearchModal } from "./SearchModal"
|
||||
|
||||
const SERVICES = [
|
||||
{ serviceType: 'HOMEBOX', label: 'Homebox', icon: '📦' },
|
||||
@@ -11,7 +12,8 @@ const SERVICES = [
|
||||
|
||||
export function Dashboard() {
|
||||
const [statuses, setStatuses] = useState<ConnectionStatus[]>([])
|
||||
const [openModal, setOpenModal] = useState<string | null>(null)
|
||||
const [openConnectionModal, setOpenConnectionModal] = useState<string | null>(null)
|
||||
const [openSearchModal, setOpenSearchModal] = useState<string | null>(null)
|
||||
|
||||
const refresh = () => {
|
||||
getStatuses().then(setStatuses).catch(() => { })
|
||||
@@ -23,7 +25,9 @@ export function Dashboard() {
|
||||
logout(serviceType).then(refresh).catch(() => { })
|
||||
}
|
||||
|
||||
const activeModal = SERVICES.find(s => s.serviceType === openModal)
|
||||
const activeConnectionModal = SERVICES.find(s => s.serviceType === openConnectionModal)
|
||||
const activeSearchModal = SERVICES.find(s => s.serviceType === openSearchModal)
|
||||
const activeSearchStatus = statuses.find(s => s.serviceType === openSearchModal)
|
||||
|
||||
return (
|
||||
<div className="dashboard">
|
||||
@@ -40,18 +44,30 @@ export function Dashboard() {
|
||||
label={label}
|
||||
icon={icon}
|
||||
status={statuses.find(s => s.serviceType === serviceType) ?? null}
|
||||
onConnect={() => setOpenModal(serviceType)}
|
||||
onConnect={() => setOpenConnectionModal(serviceType)}
|
||||
onDisconnect={() => handleDisconnect(serviceType)}
|
||||
onSearch={() => setOpenSearchModal(serviceType)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{activeModal && (
|
||||
{activeConnectionModal && (
|
||||
<ConnectModal
|
||||
serviceType={activeModal.serviceType}
|
||||
label={activeModal.label}
|
||||
onClose={() => setOpenModal(null)}
|
||||
onSuccess={() => { setOpenModal(null); refresh() }}
|
||||
serviceType={activeConnectionModal.serviceType}
|
||||
label={activeConnectionModal.label}
|
||||
onClose={() => setOpenSearchModal(null)}
|
||||
onSuccess={() => { setOpenSearchModal(null); refresh() }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeSearchModal && (
|
||||
<SearchModal
|
||||
serviceType={activeSearchModal.serviceType}
|
||||
label={activeSearchModal.label}
|
||||
appUrl={activeSearchStatus?.appUrl ?? ''}
|
||||
username={activeSearchStatus?.username ?? ''}
|
||||
onClose={() => setOpenSearchModal(null) }
|
||||
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useEffect, useRef, useState, type SyntheticEvent } from 'react'
|
||||
import './ConnectModal.scss'
|
||||
import type { SearchRequest } from '../types/search'
|
||||
import { search } from '../api/searches'
|
||||
|
||||
interface Props {
|
||||
serviceType: string
|
||||
label: string
|
||||
appUrl: string
|
||||
username: string
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function SearchModal({ serviceType, label, appUrl, username, onClose }: Readonly<Props>) {
|
||||
const [query, setQuery] = useState('');
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const firstInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const dialogRef = useRef<HTMLDialogElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const dialog = dialogRef.current
|
||||
if (!dialog) return
|
||||
|
||||
dialog.showModal()
|
||||
firstInputRef.current?.focus()
|
||||
|
||||
const handleCancel = (e: Event) => {
|
||||
e.preventDefault()
|
||||
onClose()
|
||||
}
|
||||
dialog.addEventListener('cancel', handleCancel)
|
||||
return () => dialog.removeEventListener('cancel', handleCancel)
|
||||
}, [onClose])
|
||||
|
||||
const handleSubmit = async (e: SyntheticEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
try {
|
||||
const req: SearchRequest = { appUrl, serviceType, username, query}
|
||||
await search(req)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Search failed')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<dialog className="modal" ref={dialogRef}>
|
||||
<div className="modal__header">
|
||||
<h2 className="modal__title" id="modal-title">Search in {label}</h2>
|
||||
<button className="modal__close" onClick={onClose} aria-label="Close">×</button>
|
||||
</div>
|
||||
<form className="modal__form" onSubmit={handleSubmit}>
|
||||
<div className="modal__field">
|
||||
<input id="search" className="modal__input"
|
||||
value={query} onChange={e => setQuery(e.target.value)} />
|
||||
</div>
|
||||
{error && <p className="modal__error">{error}</p>}
|
||||
<button className="modal__submit" type="submit" disabled={loading}>
|
||||
'Search'
|
||||
</button>
|
||||
</form>
|
||||
</dialog>
|
||||
)
|
||||
}
|
||||
@@ -81,39 +81,4 @@
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 7px 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.2s, opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&--connect {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&--disconnect {
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border-color: var(--border);
|
||||
|
||||
&:hover {
|
||||
border-color: #ef4444;
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ConnectionStatus } from '../types/connection'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import './ServiceCard.scss'
|
||||
|
||||
interface Props {
|
||||
@@ -8,9 +9,10 @@ interface Props {
|
||||
status: ConnectionStatus | null
|
||||
onConnect: () => void
|
||||
onDisconnect: () => void
|
||||
onSearch: () => void
|
||||
}
|
||||
|
||||
export function ServiceCard({ serviceType: _serviceType, label, icon, status, onConnect, onDisconnect }: Readonly<Props>) {
|
||||
export function ServiceCard({ serviceType: _serviceType, label, icon, status, onConnect, onDisconnect, onSearch }: Readonly<Props>) {
|
||||
const connected = status?.connected ?? false
|
||||
|
||||
const formatExpiry = (iso: string | null) => {
|
||||
@@ -37,15 +39,10 @@ export function ServiceCard({ serviceType: _serviceType, label, icon, status, on
|
||||
</div>
|
||||
</div>
|
||||
<div className="service-card__actions">
|
||||
{connected ? (
|
||||
<button className="service-card__btn service-card__btn--disconnect" onClick={onDisconnect}>
|
||||
Disconnect
|
||||
</button>
|
||||
) : (
|
||||
<button className="service-card__btn service-card__btn--connect" onClick={onConnect}>
|
||||
Connect
|
||||
</button>
|
||||
)}
|
||||
<ActionButton variant={connected ? 'disconnect' : 'connect'} onClick={connected ? onDisconnect : onConnect}>
|
||||
{connected ? 'Disconnect' : 'Connect'}
|
||||
</ActionButton>
|
||||
{connected ? (<ActionButton variant='search' onClick={onSearch}>Search</ActionButton>) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface SearchRequest {
|
||||
appUrl: string;
|
||||
serviceType: string;
|
||||
username: string;
|
||||
query: string;
|
||||
}
|
||||
Reference in New Issue
Block a user