diff --git a/frontend/package.json b/frontend/package.json index 234f1c85..2bf1ea7e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,6 +37,7 @@ "devDependencies": { "@eslint/js": "^9.19.0", "@hey-api/openapi-ts": "^0.82.4", + "@types/luxon": "^3.7.1", "@types/node": "^24.3.1", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", diff --git a/frontend/src/app.jsx b/frontend/src/app.jsx index cb778303..8038b7a8 100644 --- a/frontend/src/app.jsx +++ b/frontend/src/app.jsx @@ -1,11 +1,7 @@ import axios from 'axios'; - import nebula from '/src/nebula'; - import { useState, useEffect, Suspense } from 'react'; - import { useLocalStorage } from '/src/hooks'; - import { Routes, Route, Navigate, BrowserRouter } from 'react-router-dom'; import { MediaUploadMonitor } from './containers/MediaUpload/MediaUploadMonitor'; diff --git a/frontend/src/components/Button.styled.tsx b/frontend/src/components/Button.styled.tsx new file mode 100644 index 00000000..d887351c --- /dev/null +++ b/frontend/src/components/Button.styled.tsx @@ -0,0 +1,72 @@ +import styled from 'styled-components'; + +import { getTheme } from './theme'; + +export const BaseButton = styled.button` + border: 0; + border-radius: ${getTheme().inputBorderRadius}; + background: ${getTheme().inputBackground}; + color: ${getTheme().colors.text}; + font-size: ${getTheme().fontSize}; + padding-left: 12px; + padding-right: 12px; + min-height: ${getTheme().inputHeight}; + max-height: ${getTheme().inputHeight}; + min-width: ${getTheme().inputHeight} !important; + + &.icon-only { + padding: 0; + min-width: ${getTheme().inputHeight}; + max-width: ${getTheme().inputHeight}; + min-height: ${getTheme().inputHeight}; + max-height: ${getTheme().inputHeight}; + display: flex; + align-items: center; + justify-content: center; + } + + user-select: none; + user-drag: none; + + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + cursor: pointer; + white-space: nowrap; + + .icon { + font-size: 20px; + padding-top: 2px; + } + + &:focus { + background: ${getTheme().colors.surface06}; + outline: 0; + } + + &:hover { + background: ${getTheme().colors.surface06}; + color: ${getTheme().colors.text}; + text-decoration: none; // when rendered as a + } + + &:invalid, + &.error { + outline: 1px solid ${getTheme().colors.red} !important; + } + + &.active { + background: ${getTheme().colors.surface06}; + text-shadow: 0 0 4px ${getTheme().colors.highlight}; + } + + &:disabled { + cursor: not-allowed; + background: ${getTheme().colors.surface03}; + color: ${getTheme().colors.surface08}; + transition: + background 0.2s, + color 0.2s; + } +`; diff --git a/frontend/src/components/Button.tsx b/frontend/src/components/Button.tsx index 8bee54d6..078393fd 100644 --- a/frontend/src/components/Button.tsx +++ b/frontend/src/components/Button.tsx @@ -1,77 +1,7 @@ import clsx from 'clsx'; import { forwardRef } from 'react'; -import styled from 'styled-components'; -import { getTheme } from './theme'; - -const BaseButton = styled.button` - border: 0; - border-radius: ${getTheme().inputBorderRadius}; - background: ${getTheme().inputBackground}; - color: ${getTheme().colors.text}; - font-size: ${getTheme().fontSize}; - padding-left: 12px; - padding-right: 12px; - min-height: ${getTheme().inputHeight}; - max-height: ${getTheme().inputHeight}; - min-width: ${getTheme().inputHeight} !important; - - &.icon-only { - padding: 0; - min-width: ${getTheme().inputHeight}; - max-width: ${getTheme().inputHeight}; - min-height: ${getTheme().inputHeight}; - max-height: ${getTheme().inputHeight}; - display: flex; - align-items: center; - justify-content: center; - } - - user-select: none; - user-drag: none; - - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - cursor: pointer; - white-space: nowrap; - - .icon { - font-size: 20px; - padding-top: 2px; - } - - &:focus { - background: ${getTheme().colors.surface06}; - outline: 0; - } - - &:hover { - background: ${getTheme().colors.surface06}; - color: ${getTheme().colors.text}; - text-decoration: none; // when rendered as a - } - - &:invalid, - &.error { - outline: 1px solid ${getTheme().colors.red} !important; - } - - &.active { - background: ${getTheme().colors.surface06}; - text-shadow: 0 0 4px ${getTheme().colors.highlight}; - } - - &:disabled { - cursor: not-allowed; - background: ${getTheme().colors.surface03}; - color: ${getTheme().colors.surface08}; - transition: - background 0.2s, - color 0.2s; - } -`; +import { BaseButton } from './Button.styled'; interface ButtonProps extends React.ButtonHTMLAttributes { icon?: string; diff --git a/frontend/src/containers/Dialogs/DatePickerDialog.jsx b/frontend/src/components/DatePickerDialog.tsx similarity index 65% rename from frontend/src/containers/Dialogs/DatePickerDialog.jsx rename to frontend/src/components/DatePickerDialog.tsx index fd21f787..786660fb 100644 --- a/frontend/src/containers/Dialogs/DatePickerDialog.jsx +++ b/frontend/src/components/DatePickerDialog.tsx @@ -3,7 +3,8 @@ import { useState, useEffect } from 'react'; import DatePicker from 'react-datepicker'; import styled from 'styled-components'; -import { Dialog, Button } from '/src/components'; +import Button from './Button'; +import Dialog from './Dialog'; const DatePickerWrapper = styled.div` display: flex; @@ -14,10 +15,25 @@ const DatePickerWrapper = styled.div` min-height: 250px; `; -const DatePickerDialog = (props) => { - const [value, setValue] = useState(); +interface DatePickerDialogProps { + title: string; + value: string; + handleCancel: () => void; + handleConfirm: (value: string) => void; + cancelLabel?: string; + confirmLabel?: string; +} + +const DatePickerDialog = (props: DatePickerDialogProps) => { + /* + * A dialog component that allows the user to pick a date. + * Date is in the format 'yyyy-MM-dd', which is the + * one and the only sane date format. + */ + const [value, setValue] = useState(); const onCancel = () => props.handleCancel(); const onConfirm = () => { + if (!value) return; const t = value.toFormat('yyyy-MM-dd'); props.handleConfirm(t); }; @@ -44,6 +60,11 @@ const DatePickerDialog = (props) => { ); + const handleChange = (date: Date | null) => { + if (!date) return; + setValue(DateTime.fromJSDate(date)); + }; + return ( @@ -51,7 +72,7 @@ const DatePickerDialog = (props) => { setValue(DateTime.fromJSDate(date))} + onChange={handleChange} inline /> )} diff --git a/frontend/src/components/Dropdown.jsx b/frontend/src/components/Dropdown.jsx deleted file mode 100644 index 54dcd5c3..00000000 --- a/frontend/src/components/Dropdown.jsx +++ /dev/null @@ -1,126 +0,0 @@ -import clsx from 'clsx'; -import styled from 'styled-components'; - -import Button from './Button'; - -const DropdownContainer = styled.div` - position: relative; - display: inline-block; - - .dropdown-content { - display: none; - position: absolute; - background-color: var(--color-surface-02); - min-width: 100px; - box-shadow: 4px 4px 10px 4px rgba(0, 0, 0, 0.7); - z-index: 5; - - hr { - margin: 0; - border: none; - border-top: 2px solid var(--color-surface-03); - } - - button { - background: none; - width: 100%; - justify-content: flex-start; - border-radius: 0; - padding: 25px 8px; - - &:hover { - background-color: var(--color-surface-04); - } - - &.active, - &:focus { - outline: none !important; - background-color: var(--color-surface-04); - text-shadow: none !important; - } - - &:disabled { - color: var(--color-text-dim); - } - } - } - - > button { - // background: none; - // border: none; - // border-radius: 0; - padding: 0 8px; - justify-content: space-between; - - &:active, - &:focus { - outline: none !important; - } - } - - &:not(.disabled):hover .dropdown-content { - display: block; - } -`; - -const DropdownOption = ({ - currentValue, - separator, - disabled, - hlColor, - style, - label, - icon, - onClick, - value, -}) => { - return ( - - {separator &&
} - - ) - + ); }, [file, accept]); - return ( = ({ file, setFile, contentTy ); }; - diff --git a/frontend/src/containers/MediaUpload/MediaUpload.tsx b/frontend/src/containers/MediaUpload/MediaUpload.tsx index 499c0e8b..ff08b6aa 100644 --- a/frontend/src/containers/MediaUpload/MediaUpload.tsx +++ b/frontend/src/containers/MediaUpload/MediaUpload.tsx @@ -1,9 +1,10 @@ -import React, { useState } from 'react'; -import { useMediaUpload } from '@hooks/useMediaUpload'; import { Button } from '@components'; +import { useMediaUpload } from '@hooks/useMediaUpload'; +import React, { useState } from 'react'; + +import { MediaUploadDialog } from './MediaUploadDialog'; import type { ContentType } from '@/client'; -import {MediaUploadDialog} from './MediaUploadDialog'; interface UploadButtonProps { id: string; // Asset ID (unique identifier of the task as well) diff --git a/frontend/src/containers/MediaUpload/MediaUploadDialog.tsx b/frontend/src/containers/MediaUpload/MediaUploadDialog.tsx index 0018e098..afb9d65a 100644 --- a/frontend/src/containers/MediaUpload/MediaUploadDialog.tsx +++ b/frontend/src/containers/MediaUpload/MediaUploadDialog.tsx @@ -2,10 +2,10 @@ import { Dialog, Button } from '@components'; import { useMediaUpload } from '@hooks/useMediaUpload'; import React, { useState } from 'react'; -import type { ContentType } from '@/client'; - import { FileSelect } from './FileSelect'; +import type { ContentType } from '@/client'; + interface UploadDialogProps { onHide: () => void; id: string; // Asset ID @@ -32,11 +32,7 @@ export const MediaUploadDialog: React.FC = ({ const footer = ( <> -
); }; diff --git a/frontend/src/containers/MetadataEditor.jsx b/frontend/src/containers/MetadataEditor.jsx index 20090121..2f375be0 100644 --- a/frontend/src/containers/MetadataEditor.jsx +++ b/frontend/src/containers/MetadataEditor.jsx @@ -1,3 +1,4 @@ +import { RadioButton } from '@components'; import { useMemo } from 'react'; import nebula from '/src/nebula'; @@ -70,14 +71,22 @@ const EditorField = ({ field, value, originalValue, onFieldChanged, disabled }) editor =