From d6894ffb8b693812dc2ff4bb14bc0add0c78408d Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Mon, 28 Nov 2022 18:33:06 -0800 Subject: [PATCH] remove more of geist-ui: add spinner, button dropdown, toasts. bump deps --- .../file-dropdown/dropdown.module.css | 2 +- .../app/(posts)/components/preview/index.tsx | 7 +- client/app/(posts)/components/tabs/index.tsx | 2 + .../drag-and-drop/drag-and-drop.module.css | 14 +- .../new/components/drag-and-drop/index.tsx | 21 +-- .../edit-document/document.module.css | 2 +- .../formatting-icons.module.css | 2 +- client/app/(posts)/new/components/new.tsx | 38 ++-- .../(posts)/new/components/title/index.tsx | 4 +- .../post/[id]/components/post-page/index.tsx | 4 +- .../post-page/password-modal-wrapper.tsx | 6 +- .../components/post-page/post-page.module.css | 6 +- client/app/admin/components/post-table.tsx | 2 +- client/app/admin/components/user-table.tsx | 7 +- .../badges/visibility-control/index.tsx | 9 +- .../button-dropdown/dropdown.module.css | 39 ++-- .../app/components/button-dropdown/index.tsx | 109 +++-------- .../button-group/button-group.module.css | 42 +++-- client/app/components/button-group/index.tsx | 6 +- client/app/components/header/controls.tsx | 36 ---- client/app/components/header/index.tsx | 6 +- client/app/components/home.tsx | 5 - client/app/components/note/note.module.css | 5 +- client/app/components/post-list/index.tsx | 27 +-- .../post-list/list-item-skeleton.tsx | 37 ++-- client/app/components/spinner/index.tsx | 3 + .../app/components/spinner/spinner.module.css | 17 ++ client/app/components/toasts/index.tsx | 69 +++++++ client/app/layout.tsx | 1 - client/app/root-layout-wrapper.tsx | 3 + .../settings/components/sections/password.tsx | 134 -------------- .../settings/components/sections/profile.tsx | 6 +- client/app/styles/globals.css | 10 +- client/package.json | 5 +- client/pnpm-lock.yaml | 170 +++++++++++------- 35 files changed, 397 insertions(+), 459 deletions(-) delete mode 100644 client/app/components/header/controls.tsx create mode 100644 client/app/components/spinner/index.tsx create mode 100644 client/app/components/spinner/spinner.module.css create mode 100644 client/app/components/toasts/index.tsx delete mode 100644 client/app/settings/components/sections/password.tsx diff --git a/client/app/(posts)/components/file-dropdown/dropdown.module.css b/client/app/(posts)/components/file-dropdown/dropdown.module.css index 9d2f48ae..116596e1 100644 --- a/client/app/(posts)/components/file-dropdown/dropdown.module.css +++ b/client/app/(posts)/components/file-dropdown/dropdown.module.css @@ -60,7 +60,7 @@ } .chevron { - transition: transform 0.2s ease-in-out; + transition: transform 0.1s ease-in-out; } [data-state="open"] .chevron { diff --git a/client/app/(posts)/components/preview/index.tsx b/client/app/(posts)/components/preview/index.tsx index 384d9477..4b27b6c4 100644 --- a/client/app/(posts)/components/preview/index.tsx +++ b/client/app/(posts)/components/preview/index.tsx @@ -2,7 +2,8 @@ import { memo, useEffect, useState } from "react" import styles from "./preview.module.css" import "@styles/markdown.css" import "@styles/syntax.css" -import { Spinner } from "@geist-ui/core/dist" +import Skeleton from "@components/skeleton" +import { Spinner } from "@components/spinner" type Props = { height?: number | string @@ -52,9 +53,7 @@ const MarkdownPreview = ({ return ( <> {isLoading ? ( - <> - - + ) : ( )} diff --git a/client/app/(posts)/components/tabs/index.tsx b/client/app/(posts)/components/tabs/index.tsx index e61c82f8..53cea7db 100644 --- a/client/app/(posts)/components/tabs/index.tsx +++ b/client/app/(posts)/components/tabs/index.tsx @@ -1,3 +1,5 @@ +"use client" + import * as RadixTabs from "@radix-ui/react-tabs" import FormattingIcons from "app/(posts)/new/components/edit-document-list/edit-document/formatting-icons" import { ChangeEvent, useRef } from "react" diff --git a/client/app/(posts)/new/components/drag-and-drop/drag-and-drop.module.css b/client/app/(posts)/new/components/drag-and-drop/drag-and-drop.module.css index 08b02448..403d0068 100644 --- a/client/app/(posts)/new/components/drag-and-drop/drag-and-drop.module.css +++ b/client/app/(posts)/new/components/drag-and-drop/drag-and-drop.module.css @@ -17,7 +17,7 @@ border-radius: 2px; border: 2px dashed var(--border) !important; outline: none; - transition: all 0.24s ease-in-out; + transition: all 0.14s ease-in-out; cursor: pointer; } @@ -32,7 +32,7 @@ .error { color: red; font-size: 0.8rem; - transition: border 0.24s ease-in-out; + transition: border 0.14s ease-in-out; border: 2px solid red; border-radius: 2px; padding: 20px; @@ -42,3 +42,13 @@ margin: 0; padding-left: var(--gap-double); } + +.verb:after { + content: "click"; +} + +@media (hover: none) { + .verb:after { + content: "tap"; + } +} diff --git a/client/app/(posts)/new/components/drag-and-drop/index.tsx b/client/app/(posts)/new/components/drag-and-drop/index.tsx index 8e31e082..80c80984 100644 --- a/client/app/(posts)/new/components/drag-and-drop/index.tsx +++ b/client/app/(posts)/new/components/drag-and-drop/index.tsx @@ -1,4 +1,3 @@ -import { useMediaQuery, useTheme, useToasts } from "@geist-ui/core/dist" import { useDropzone } from "react-dropzone" import styles from "./drag-and-drop.module.css" import generateUUID from "@lib/generate-uuid" @@ -9,13 +8,10 @@ import { } from "@lib/constants" import byteToMB from "@lib/byte-to-mb" import type { Document } from "../new" +import { useToasts } from "@components/toasts" function FileDropzone({ setDocs }: { setDocs: (docs: Document[]) => void }) { - const { palette } = useTheme() const { setToast } = useToasts() - const isMobile = useMediaQuery("xs", { - match: "down" - }) const onDrop = async (acceptedFiles: File[]) => { const newDocs = await Promise.all( acceptedFiles.map((file) => { @@ -23,9 +19,9 @@ function FileDropzone({ setDocs }: { setDocs: (docs: Document[]) => void }) { const reader = new FileReader() reader.onabort = () => - setToast({ text: "File reading was aborted", type: "error" }) + setToast({ message: "File reading was aborted", type: "error" }) reader.onerror = () => - setToast({ text: "File reading failed", type: "error" }) + setToast({ message: "File reading failed", type: "error" }) reader.onload = () => { const content = reader.result as string resolve({ @@ -84,20 +80,13 @@ function FileDropzone({ setDocs }: { setDocs: (docs: Document[]) => void }) { )) - const verb = isMobile ? "tap" : "click" return (
-
+
{!isDragActive && (

- Drag some files here, or {verb} to select files + Drag some files here, or to select files

)} {isDragActive &&

Release to drop the files here

} diff --git a/client/app/(posts)/new/components/edit-document-list/edit-document/document.module.css b/client/app/(posts)/new/components/edit-document-list/edit-document/document.module.css index a343ffac..99c75125 100644 --- a/client/app/(posts)/new/components/edit-document-list/edit-document/document.module.css +++ b/client/app/(posts)/new/components/edit-document-list/edit-document/document.module.css @@ -1,7 +1,7 @@ .card { margin: var(--gap) auto; padding: var(--gap); - border: 1px solid var(--light-gray); + border: 1px solid var(--lighter-gray); border-radius: var(--radius); } diff --git a/client/app/(posts)/new/components/edit-document-list/edit-document/formatting-icons/formatting-icons.module.css b/client/app/(posts)/new/components/edit-document-list/edit-document/formatting-icons/formatting-icons.module.css index 5a602add..e7b98c22 100644 --- a/client/app/(posts)/new/components/edit-document-list/edit-document/formatting-icons/formatting-icons.module.css +++ b/client/app/(posts)/new/components/edit-document-list/edit-document/formatting-icons/formatting-icons.module.css @@ -6,7 +6,7 @@ .actionWrapper .actions { position: absolute; right: 0; - top: -34px; + top: -40px; } /* small screens, top: 0 */ diff --git a/client/app/(posts)/new/components/new.tsx b/client/app/(posts)/new/components/new.tsx index 91a89939..d0564813 100644 --- a/client/app/(posts)/new/components/new.tsx +++ b/client/app/(posts)/new/components/new.tsx @@ -1,6 +1,5 @@ "use client" -import { useToasts, ButtonDropdown } from "@geist-ui/core/dist" import { useRouter } from "next/navigation" import { useCallback, useState } from "react" import generateUUID from "@lib/generate-uuid" @@ -16,6 +15,8 @@ import Title from "./title" import FileDropzone from "./drag-and-drop" import Button from "@components/button" import Input from "@components/input" +import ButtonDropdown from "@components/button-dropdown" +import { useToasts } from "@components/toasts" const emptyDoc = { title: "", content: "", @@ -87,7 +88,8 @@ const Post = ({ const json = await res.json() console.error(json) setToast({ - text: "Please fill out all fields", + id: "error", + message: "Please fill out all fields", type: "error" }) setPasswordModalVisible(false) @@ -114,7 +116,7 @@ const Post = ({ if (!title) { setToast({ - text: "Please fill out the post title", + message: "Please fill out the post title", type: "error" }) hasErrored = true @@ -122,7 +124,7 @@ const Post = ({ if (!docs.length) { setToast({ - text: "Please add at least one document", + message: "Please add at least one document", type: "error" }) hasErrored = true @@ -131,7 +133,7 @@ const Post = ({ for (const doc of docs) { if (!doc.title) { setToast({ - text: "Please fill out all the document titles", + message: "Please fill out all the document titles", type: "error" }) hasErrored = true @@ -308,19 +310,27 @@ const Post = ({ enableTabLoop={false} minDate={new Date()} /> - - onSubmit("unlisted")}> + + + + +
diff --git a/client/app/(posts)/new/components/title/index.tsx b/client/app/(posts)/new/components/title/index.tsx index df839532..3e32b8d7 100644 --- a/client/app/(posts)/new/components/title/index.tsx +++ b/client/app/(posts)/new/components/title/index.tsx @@ -13,14 +13,14 @@ const titlePlaceholders = [ "I'm thinking about ..." ] +const placeholder = titlePlaceholders[3] + type props = { onChange: (e: ChangeEvent) => void title?: string } const Title = ({ onChange, title }: props) => { - const placeholder = - titlePlaceholders[Math.floor(Math.random() * titlePlaceholders.length)] return (

Drift

diff --git a/client/app/(posts)/post/[id]/components/post-page/index.tsx b/client/app/(posts)/post/[id]/components/post-page/index.tsx index b2248d92..e5a1333a 100644 --- a/client/app/(posts)/post/[id]/components/post-page/index.tsx +++ b/client/app/(posts)/post/[id]/components/post-page/index.tsx @@ -4,7 +4,6 @@ import VisibilityBadge from "@components/badges/visibility-badge" import DocumentComponent from "./view-document" import styles from "./post-page.module.css" -import { useMediaQuery } from "@geist-ui/core/dist" import { useEffect, useState } from "react" import Archive from "@geist-ui/icons/archive" import Edit from "@geist-ui/icons/edit" @@ -32,7 +31,6 @@ const PostPage = ({ post: initialPost, isProtected, isAuthor }: Props) => { ) const [visibility, setVisibility] = useState(post.visibility) const router = useRouter() - const isMobile = useMediaQuery("mobile") useEffect(() => { if (post.expiresAt) { @@ -101,7 +99,7 @@ const PostPage = ({ post: initialPost, isProtected, isAuthor }: Props) => { {!isAvailable && }
- + +
- {visible && ( -
-
- {props.children.slice(1)} -
-
- )} -
+ ) } diff --git a/client/app/components/button-group/button-group.module.css b/client/app/components/button-group/button-group.module.css index 89983fdd..0b865fd6 100644 --- a/client/app/components/button-group/button-group.module.css +++ b/client/app/components/button-group/button-group.module.css @@ -1,29 +1,45 @@ .button-group { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - justify-content: flex-start; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; } .button-group > * { - flex: 1 1 auto; - margin: 0; + flex: 1 1 auto; + margin: 0; } .button-group > * { - border-radius: 0 !important; + border-radius: 0 !important; } .button-group > button:first-of-type { - border-top-left-radius: var(--radius) !important; - border-bottom-left-radius: var(--radius) !important; + border-top-left-radius: var(--radius) !important; + border-bottom-left-radius: var(--radius) !important; } .button-group > button:last-of-type { - border-top-right-radius: var(--radius) !important; - border-bottom-right-radius: var(--radius) !important; + border-top-right-radius: var(--radius) !important; + border-bottom-right-radius: var(--radius) !important; } -.vertical { - flex-direction: column; +@media screen and (max-width: 768px) { + .verticalIfMobile { + flex-direction: column; + } + + .verticalIfMobile.button-group > button:first-of-type { + border-top-left-radius: var(--radius) !important; + border-top-right-radius: var(--radius) !important; + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; + } + + .verticalIfMobile.button-group > button:last-of-type { + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; + border-bottom-left-radius: var(--radius) !important; + border-bottom-right-radius: var(--radius) !important; + } } diff --git a/client/app/components/button-group/index.tsx b/client/app/components/button-group/index.tsx index 01e5fb9d..a84ce6fe 100644 --- a/client/app/components/button-group/index.tsx +++ b/client/app/components/button-group/index.tsx @@ -2,18 +2,18 @@ import styles from "./button-group.module.css" import clsx from "clsx" export default function ButtonGroup({ children, - vertical, + verticalIfMobile, ...props }: { children: React.ReactNode | React.ReactNode[] - vertical?: boolean + verticalIfMobile?: boolean } & React.HTMLAttributes) { return (
diff --git a/client/app/components/header/controls.tsx b/client/app/components/header/controls.tsx deleted file mode 100644 index e7cc9636..00000000 --- a/client/app/components/header/controls.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { useEffect, useState } from "react" -import MoonIcon from "@geist-ui/icons/moon" -import SunIcon from "@geist-ui/icons/sun" -import styles from "./header.module.css" -import { Select } from "@geist-ui/core/dist" -import { useTheme } from "@components/theme/ThemeClientContextProvider" - -const Controls = () => { - const { theme, setTheme } = useTheme() - const switchThemes = () => { - if (theme === "dark") { - setTheme("light") - } else { - setTheme("dark") - } - } - - return ( -
- -
- ) -} - -export default React.memo(Controls) diff --git a/client/app/components/header/index.tsx b/client/app/components/header/index.tsx index ffb0bab7..486a2058 100644 --- a/client/app/components/header/index.tsx +++ b/client/app/components/header/index.tsx @@ -1,6 +1,6 @@ "use client" -import { Page, useBodyScroll, useMediaQuery } from "@geist-ui/core/dist" +import { useBodyScroll, useMediaQuery } from "@geist-ui/core/dist" import { useEffect, useState } from "react" import styles from "./header.module.css" @@ -174,7 +174,7 @@ const Header = ({ signedIn = false, isAdmin = false }) => { const buttons = pages.map(getButton) return ( - +
{buttons}
@@ -189,7 +189,7 @@ const Header = ({ signedIn = false, isAdmin = false }) => { {buttons}
)} - + ) } diff --git a/client/app/components/home.tsx b/client/app/components/home.tsx index 8a8569db..9ecdb5a9 100644 --- a/client/app/components/home.tsx +++ b/client/app/components/home.tsx @@ -1,9 +1,4 @@ -"use client" -import { Tabs, Textarea } from "@geist-ui/core/dist" import Image from "next/image" -import styles from "./home.module.css" -// TODO:components/new-post/ move these styles -import markdownStyles from "app/(posts)/components/preview/preview.module.css" import Card from "./card" import DocumentTabs from "app/(posts)/components/tabs" const Home = ({ diff --git a/client/app/components/note/note.module.css b/client/app/components/note/note.module.css index 7cec86b2..739b3cd4 100644 --- a/client/app/components/note/note.module.css +++ b/client/app/components/note/note.module.css @@ -12,7 +12,8 @@ } .warning { - background: #f33; + background: var(--warning); + color: var(--bg); } .error { @@ -21,7 +22,7 @@ [data-theme="light"] .warning, [data-theme="light"] .error { - color: var(--bg); + color: var(--fg); } .type { diff --git a/client/app/components/post-list/index.tsx b/client/app/components/post-list/index.tsx index bf8a9e99..f519c5c3 100644 --- a/client/app/components/post-list/index.tsx +++ b/client/app/components/post-list/index.tsx @@ -1,7 +1,7 @@ "use client" import styles from "./post-list.module.css" -import ListItemSkeleton from "./list-item-skeleton" +import { ListItemSkeleton } from "./list-item-skeleton" import ListItem from "./list-item" import { ChangeEvent, useCallback, useEffect, useState } from "react" import useDebounce from "@lib/hooks/use-debounce" @@ -9,6 +9,7 @@ import Link from "@components/link" import type { PostWithFiles } from "@lib/server/prisma" import Input from "@components/input" import Button from "@components/button" +import { useToasts } from "@components/toasts" type Props = { initialPosts: string | PostWithFiles[] @@ -29,6 +30,7 @@ const PostList = ({ const [posts, setPosts] = useState(initialPosts) const [searching, setSearching] = useState(false) const [hasMorePosts, setHasMorePosts] = useState(morePosts) + const { setToast } = useToasts() const debouncedSearchValue = useDebounce(search, 200) @@ -71,7 +73,7 @@ const PostList = ({ } ) const json = await res.json() - setPosts(json.posts) + setPosts(json) setSearching(false) } fetchPosts() @@ -97,9 +99,13 @@ const PostList = ({ return } else { setPosts((posts) => posts.filter((post) => post.id !== postId)) + setToast({ + message: "Post deleted", + type: "success" + }) } }, - [] + [setToast] ) return ( @@ -108,21 +114,18 @@ const PostList = ({
{!posts &&

Failed to load.

} - {!posts?.length && searching && ( + {/* {!posts?.length && (
    -
  • - -
  • -
  • - -
  • + +
- )} + )} */} {posts?.length === 0 && posts && (

No posts found. Create one{" "} diff --git a/client/app/components/post-list/list-item-skeleton.tsx b/client/app/components/post-list/list-item-skeleton.tsx index d7e08282..9f276287 100644 --- a/client/app/components/post-list/list-item-skeleton.tsx +++ b/client/app/components/post-list/list-item-skeleton.tsx @@ -1,25 +1,22 @@ +import styles from "./list-item.module.css" import Card from "@components/card" import Skeleton from "@components/skeleton" -import { Divider, Grid, Spacer } from "@geist-ui/core/dist" -const ListItemSkeleton = () => ( - - - - - - - - - - - - - +export const ListItemSkeleton = () => ( +

  • + + <> +
    + {/* title */} + +
    - - -
    +
    + +
    + +
    + + +
  • ) - -export default ListItemSkeleton diff --git a/client/app/components/spinner/index.tsx b/client/app/components/spinner/index.tsx new file mode 100644 index 00000000..ce7b3e0d --- /dev/null +++ b/client/app/components/spinner/index.tsx @@ -0,0 +1,3 @@ +import styles from './spinner.module.css' + +export const Spinner = () =>
    diff --git a/client/app/components/spinner/spinner.module.css b/client/app/components/spinner/spinner.module.css new file mode 100644 index 00000000..97bfd9bd --- /dev/null +++ b/client/app/components/spinner/spinner.module.css @@ -0,0 +1,17 @@ +.spinner { + border: 4px solid var(--light-gray); + border-top: 4px solid var(--gray); + border-radius: 50%; + width: 24px; + height: 24px; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/client/app/components/toasts/index.tsx b/client/app/components/toasts/index.tsx new file mode 100644 index 00000000..b340bd25 --- /dev/null +++ b/client/app/components/toasts/index.tsx @@ -0,0 +1,69 @@ +"use client" + +import Toast, { Toaster } from "react-hot-toast" + +export type ToastType = "success" | "error" | "loading" | "default" + +export type ToastProps = { + id?: string + type: ToastType + message: string + duration?: number + icon?: string + style?: React.CSSProperties + className?: string + loading?: boolean + loadingProgress?: number +} + +export const useToasts = () => { + const setToast = (toast: ToastProps) => { + const { type, message, ...rest } = toast + if (toast.id) { + Toast.dismiss(toast.id) + } + + switch (type) { + case "success": + Toast.success(message, rest) + break + case "error": + Toast.error(message, rest) + break + case "loading": + Toast.loading(message, rest) + break + default: + Toast(message, rest) + break + } + } + + return { setToast } +} + +export const Toasts = () => { + return ( + + ) +} diff --git a/client/app/layout.tsx b/client/app/layout.tsx index 9e6ba77e..8f6f1593 100644 --- a/client/app/layout.tsx +++ b/client/app/layout.tsx @@ -4,7 +4,6 @@ import styles from "@styles/Home.module.css" import { getSession } from "@lib/server/session" import ThemeProvider from "@components/theme/ThemeProvider" import { THEME_COOKIE_NAME } from "@components/theme/theme" -import { useServerTheme } from "@components/theme/ThemeServerContextProvider" interface RootLayoutProps { children: React.ReactNode diff --git a/client/app/root-layout-wrapper.tsx b/client/app/root-layout-wrapper.tsx index dc62247d..972d3341 100644 --- a/client/app/root-layout-wrapper.tsx +++ b/client/app/root-layout-wrapper.tsx @@ -2,7 +2,9 @@ import Header from "@components/header" import Page from "@components/page" +import { Toasts } from "@components/toasts" import * as RadixTooltip from "@radix-ui/react-tooltip" +import { Toaster } from "react-hot-toast" export function LayoutWrapper({ children, @@ -15,6 +17,7 @@ export function LayoutWrapper({ }) { return ( +
    {children} diff --git a/client/app/settings/components/sections/password.tsx b/client/app/settings/components/sections/password.tsx deleted file mode 100644 index bf4229d3..00000000 --- a/client/app/settings/components/sections/password.tsx +++ /dev/null @@ -1,134 +0,0 @@ -"use client" - -import { Input, Button, useToasts } from "@geist-ui/core/dist" -import { useState } from "react" - -const Password = () => { - const [password, setPassword] = useState("") - const [newPassword, setNewPassword] = useState("") - const [confirmPassword, setConfirmPassword] = useState("") - - const { setToast } = useToasts() - - const handlePasswordChange = (e: React.ChangeEvent) => { - setPassword(e.target.value) - } - - const handleNewPasswordChange = (e: React.ChangeEvent) => { - setNewPassword(e.target.value) - } - - const handleConfirmPasswordChange = ( - e: React.ChangeEvent - ) => { - setConfirmPassword(e.target.value) - } - - const onSubmit = async (e: React.FormEvent) => { - e.preventDefault() - if (!password || !newPassword || !confirmPassword) { - setToast({ - text: "Please fill out all fields", - type: "error" - }) - } - - if (newPassword !== confirmPassword) { - setToast({ - text: "New password and confirm password do not match", - type: "error" - }) - } - - const res = await fetch("/server-api/auth/change-password", { - method: "PUT", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - oldPassword: password, - newPassword - }) - }) - - if (res.status === 200) { - setToast({ - text: "Password updated successfully", - type: "success" - }) - setPassword("") - setNewPassword("") - setConfirmPassword("") - } else { - const data = await res.json() - - setToast({ - text: data.error ?? "Failed to update password", - type: "error" - }) - } - } - - return ( -
    -
    - - -
    -
    - - -
    -
    - - -
    - -
    - ) -} - -export default Password diff --git a/client/app/settings/components/sections/profile.tsx b/client/app/settings/components/sections/profile.tsx index 1ef38191..91dc2508 100644 --- a/client/app/settings/components/sections/profile.tsx +++ b/client/app/settings/components/sections/profile.tsx @@ -93,7 +93,7 @@ const Profile = ({ user }: { user: User }) => { disabled />
    -
    + {/*