1
0
mirror of https://github.com/h3poteto/whalebird-desktop synced 2025-01-03 12:30:00 +01:00

Change component library to material-tailwind

This commit is contained in:
AkiraFukushima 2024-01-09 21:30:05 +09:00
parent 7c9708906e
commit b49c1e01a9
No known key found for this signature in database
GPG Key ID: B6E51BAC4DE1A957
27 changed files with 864 additions and 4371 deletions

View File

@ -91,7 +91,7 @@
}, },
"nsfw": "Sensitive", "nsfw": "Sensitive",
"poll": { "poll": {
"add": "Add a choice", "add": "Add",
"5min": "5 minutes", "5min": "5 minutes",
"30min": "30 minutes", "30min": "30 minutes",
"1h": "1 hour", "1h": "1 hour",

View File

@ -16,14 +16,13 @@
}, },
"dependencies": { "dependencies": {
"@emoji-mart/react": "^1.1.1", "@emoji-mart/react": "^1.1.1",
"@material-tailwind/react": "^2.1.8",
"blurhash": "^2.0.5", "blurhash": "^2.0.5",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"dexie": "^3.2.4", "dexie": "^3.2.4",
"electron-serve": "^1.1.0", "electron-serve": "^1.1.0",
"electron-store": "^8.1.0", "electron-store": "^8.1.0",
"emoji-mart": "^5.5.2", "emoji-mart": "^5.5.2",
"flowbite": "^2.0.0",
"flowbite-react": "^0.7.0",
"megalodon": "^9.1.1", "megalodon": "^9.1.1",
"react-blurhash": "^0.3.0", "react-blurhash": "^0.3.0",
"react-hotkeys-hook": "^4.4.1", "react-hotkeys-hook": "^4.4.1",
@ -36,7 +35,7 @@
"@babel/runtime-corejs3": "^7.23.2", "@babel/runtime-corejs3": "^7.23.2",
"@electron/notarize": "^2.1.0", "@electron/notarize": "^2.1.0",
"@types/node": "^20.0.0", "@types/node": "^20.0.0",
"@types/react": "^18.0.26", "@types/react": "18.2.19",
"@types/sanitize-html": "^2.9.4", "@types/sanitize-html": "^2.9.4",
"@typescript-eslint/eslint-plugin": "^6.13.1", "@typescript-eslint/eslint-plugin": "^6.13.1",
"@typescript-eslint/parser": "^6.13.1", "@typescript-eslint/parser": "^6.13.1",

View File

@ -1,4 +1,4 @@
import { Modal } from 'flowbite-react' import { Dialog, DialogBody } from '@material-tailwind/react'
import { Entity } from 'megalodon' import { Entity } from 'megalodon'
type Props = { type Props = {
@ -9,9 +9,8 @@ type Props = {
export default function Media(props: Props) { export default function Media(props: Props) {
return ( return (
<Modal show={props.open} onClose={props.close} size="6xl"> <Dialog open={props.open} handler={props.close} size="lg">
<Modal.Header /> <DialogBody className="max-h-full max-w-full">
<Modal.Body className="max-h-full max-w-full">
{props.attachment && ( {props.attachment && (
<img <img
src={props.attachment.url} src={props.attachment.url}
@ -20,7 +19,8 @@ export default function Media(props: Props) {
className="object-contain max-h-full max-w-full m-auto" className="object-contain max-h-full max-w-full m-auto"
/> />
)} )}
</Modal.Body> <></>
</Modal> </DialogBody>
</Dialog>
) )
} }

View File

@ -1,5 +1,5 @@
import { localeType } from '@/utils/i18n' import { localeType } from '@/utils/i18n'
import { Label, Modal, Select, TextInput } from 'flowbite-react' import { Dialog, DialogBody, DialogHeader, Input, Option, Select, Typography } from '@material-tailwind/react'
import { ChangeEvent, useEffect, useState } from 'react' import { ChangeEvent, useEffect, useState } from 'react'
import { FormattedMessage } from 'react-intl' import { FormattedMessage } from 'react-intl'
@ -31,10 +31,9 @@ export default function Settings(props: Props) {
} }
}, []) }, [])
const languageChanged = (e: ChangeEvent<HTMLSelectElement>) => { const languageChanged = (e: string) => {
setLanguage(e.target.value as localeType)
if (typeof localStorage !== 'undefined') { if (typeof localStorage !== 'undefined') {
localStorage.setItem('language', e.target.value) localStorage.setItem('language', e)
} }
props.reloadSettings() props.reloadSettings()
} }
@ -48,40 +47,40 @@ export default function Settings(props: Props) {
} }
return ( return (
<Modal show={props.opened} onClose={props.close}> <Dialog open={props.opened} handler={props.close} size="sm">
<Modal.Header> <DialogHeader>
<FormattedMessage id="settings.title" /> <FormattedMessage id="settings.title" />
</Modal.Header> </DialogHeader>
<Modal.Body> <DialogBody>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div> <div>
<div className="mb-2"> <div className="mb-2">
<Label htmlFor="fontsize"> <Typography>
<FormattedMessage id="settings.font_size" /> <FormattedMessage id="settings.font_size" />
</Label> </Typography>
</div> </div>
<div> <div>
<TextInput type="number" value={fontSize} onChange={fontSizeChanged} /> <Input type="number" value={fontSize} onChange={fontSizeChanged} />
</div> </div>
</div> </div>
<div> <div>
<div className="mb-2"> <div className="mb-2">
<Label htmlFor="language"> <Typography>
<FormattedMessage id="settings.language" /> <FormattedMessage id="settings.language" />
</Label> </Typography>
</div> </div>
<div> <div>
<Select id="language" onChange={languageChanged} defaultValue={language}> <Select id="language" onChange={languageChanged} value={language}>
{languages.map(lang => ( {languages.map(lang => (
<option key={lang.value} value={lang.value}> <Option key={lang.value} value={lang.value}>
{lang.label} {lang.label}
</option> </Option>
))} ))}
</Select> </Select>
</div> </div>
</div> </div>
</div> </div>
</Modal.Body> </DialogBody>
</Modal> </Dialog>
) )
} }

View File

@ -1,8 +1,8 @@
import { Label, Modal, TextInput, Button, Alert, Spinner } from 'flowbite-react'
import generator, { MegalodonInterface, OAuth, detector } from 'megalodon' import generator, { MegalodonInterface, OAuth, detector } from 'megalodon'
import { useState } from 'react' import { useState } from 'react'
import { db } from '@/db' import { db } from '@/db'
import { FormattedMessage, useIntl } from 'react-intl' import { FormattedMessage, useIntl } from 'react-intl'
import { Alert, Button, Dialog, DialogBody, DialogHeader, Input, Spinner, Typography } from '@material-tailwind/react'
type NewProps = { type NewProps = {
opened: boolean opened: boolean
@ -107,13 +107,13 @@ export default function New(props: NewProps) {
return ( return (
<> <>
<Modal dismissible={false} show={props.opened} onClose={close} size="lg"> <Dialog open={props.opened} handler={close} size="xs">
<Modal.Header> <DialogHeader>
<FormattedMessage id="accounts.new.title" /> <FormattedMessage id="accounts.new.title" />
</Modal.Header> </DialogHeader>
<Modal.Body> <DialogBody>
{error && ( {error && (
<Alert color="failure"> <Alert color="red">
<span>{error}</span> <span>{error}</span>
</Alert> </Alert>
)} )}
@ -121,12 +121,12 @@ export default function New(props: NewProps) {
{sns === null ? ( {sns === null ? (
<> <>
<div className="block"> <div className="block">
<Label htmlFor="domain"> <Typography>
<FormattedMessage id="accounts.new.domain" /> <FormattedMessage id="accounts.new.domain" />
</Label> </Typography>
</div> </div>
<TextInput id="domain" placeholder="mastodon.social" required type="text" /> <Input type="text" id="domain" placeholder="mastodon.social" />
<Button color="blue" onClick={checkDomain} disabled={loading}> <Button onClick={checkDomain} loading={loading} color="blue">
<FormattedMessage id="accounts.new.sign_in" /> <FormattedMessage id="accounts.new.sign_in" />
</Button> </Button>
</> </>
@ -143,31 +143,31 @@ export default function New(props: NewProps) {
) : ( ) : (
<> <>
<div className="block"> <div className="block">
<Label htmlFor="authorization"> <Typography>
<FormattedMessage id="accounts.new.authorization_code" /> <FormattedMessage id="accounts.new.authorization_code" />
</Label> </Typography>
<p className="text-sm text-gray-600"> <Typography variant="small">
<FormattedMessage id="accounts.new.authorization_helper" /> <FormattedMessage id="accounts.new.authorization_helper" />
</p> </Typography>
</div> </div>
<TextInput id="authorization" required type="text" /> <Input id="authorization" type="text" />
</> </>
)} )}
<Button onClick={authorize} disabled={loading}> <Button onClick={authorize} disabled={loading} color="blue">
<FormattedMessage id="accounts.new.authorize" /> <FormattedMessage id="accounts.new.authorize" />
</Button> </Button>
</> </>
) : ( ) : (
<div className="text-center"> <div className="text-center">
<Spinner aria-label="Loading" /> <Spinner />
</div> </div>
)} )}
</> </>
)} )}
</form> </form>
</Modal.Body> </DialogBody>
</Modal> </Dialog>
</> </>
) )
} }

View File

@ -1,17 +1,3 @@
import {
Button,
Checkbox,
CustomFlowbiteTheme,
Dropdown,
Flowbite,
Label,
Radio,
Select,
Spinner,
TextInput,
Textarea,
ToggleSwitch
} from 'flowbite-react'
import { ChangeEvent, Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react' import { ChangeEvent, Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react'
import { FormattedMessage, useIntl } from 'react-intl' import { FormattedMessage, useIntl } from 'react-intl'
import { import {
@ -31,6 +17,22 @@ import { useToast } from '@/utils/toast'
import Picker from '@emoji-mart/react' import Picker from '@emoji-mart/react'
import { data } from '@/utils/emojiData' import { data } from '@/utils/emojiData'
import EditMedia from './EditMedia' import EditMedia from './EditMedia'
import {
Button,
Checkbox,
IconButton,
Input,
List,
ListItem,
Option,
Popover,
PopoverContent,
PopoverHandler,
Radio,
Select,
Switch,
Textarea
} from '@material-tailwind/react'
type Props = { type Props = {
client: MegalodonInterface client: MegalodonInterface
@ -43,17 +45,6 @@ type Poll = {
multiple: boolean multiple: boolean
} }
const customTheme: CustomFlowbiteTheme = {
dropdown: {
content: 'focus:outline-none',
floating: {
item: {
base: 'hidden'
}
}
}
}
export default function Compose(props: Props) { export default function Compose(props: Props) {
const [body, setBody] = useState('') const [body, setBody] = useState('')
const [visibility, setVisibility] = useState<'public' | 'unlisted' | 'private' | 'direct'>('public') const [visibility, setVisibility] = useState<'public' | 'unlisted' | 'private' | 'direct'>('public')
@ -65,11 +56,13 @@ export default function Compose(props: Props) {
const [editMedia, setEditMedia] = useState<Entity.Attachment>() const [editMedia, setEditMedia] = useState<Entity.Attachment>()
const [maxCharacters, setMaxCharacters] = useState<number | null>(null) const [maxCharacters, setMaxCharacters] = useState<number | null>(null)
const [remaining, setRemaining] = useState<number | null>(null) const [remaining, setRemaining] = useState<number | null>(null)
const [popoverVisibility, setPopoverVisibility] = useState(false)
const [popoverEmoji, setPopoverEmoji] = useState(false)
const { formatMessage } = useIntl() const { formatMessage } = useIntl()
const uploaderRef = useRef(null) const uploaderRef = useRef(null)
const showToast = useToast() const showToast = useToast()
const textareaRef = useRef<HTMLTextAreaElement>(null) const textareaRef = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
if (!props.client) return if (!props.client) return
@ -228,19 +221,18 @@ export default function Compose(props: Props) {
} else if (emoji.shortcodes) { } else if (emoji.shortcodes) {
setBody(current => `${current.slice(0, cursor)}${emoji.shortcodes} ${current.slice(cursor)}`) setBody(current => `${current.slice(0, cursor)}${emoji.shortcodes} ${current.slice(cursor)}`)
} }
const dummy = document.getElementById('dummy-emoji-picker') setPopoverEmoji(false)
dummy.click()
} }
return ( return (
<div className="px-4 pb-4"> <div className="px-4 pb-4">
<form id="form"> <form id="form">
{cw && ( {cw && (
<TextInput <Input
id="spoiler" id="spoiler"
type="text" type="text"
sizing="sm" color="blue"
className="mb-2" containerProps={{ className: 'mb-2' }}
value={spoiler} value={spoiler}
onChange={ev => setSpoiler(ev.target.value)} onChange={ev => setSpoiler(ev.target.value)}
placeholder={formatMessage({ id: 'compose.spoiler.placeholder' })} placeholder={formatMessage({ id: 'compose.spoiler.placeholder' })}
@ -249,6 +241,7 @@ export default function Compose(props: Props) {
<div className="relative"> <div className="relative">
<Textarea <Textarea
id="body" id="body"
color="blue"
className="resize-none focus:ring-0" className="resize-none focus:ring-0"
placeholder={formatMessage({ id: 'compose.placeholder' })} placeholder={formatMessage({ id: 'compose.placeholder' })}
rows={3} rows={3}
@ -256,22 +249,16 @@ export default function Compose(props: Props) {
onChange={ev => setBody(ev.target.value)} onChange={ev => setBody(ev.target.value)}
ref={textareaRef} ref={textareaRef}
/> />
<Flowbite theme={{ theme: customTheme }}> <Popover open={popoverEmoji} handler={setPopoverEmoji}>
<Dropdown <PopoverHandler>
label="" <span className="absolute top-1 right-1 text-gray-600 cursor-pointer">
dismissOnClick <FaFaceLaughBeam />
renderTrigger={() => ( </span>
<span className="absolute top-1 right-1 text-gray-600 cursor-pointer"> </PopoverHandler>
<FaFaceLaughBeam /> <PopoverContent>
</span>
)}
>
<Picker data={data} onEmojiSelect={onEmojiSelect} previewPosition="none" set="native" perLine="7" theme="light" /> <Picker data={data} onEmojiSelect={onEmojiSelect} previewPosition="none" set="native" perLine="7" theme="light" />
<Dropdown.Item> </PopoverContent>
<span id="dummy-emoji-picker" /> </Popover>
</Dropdown.Item>
</Dropdown>
</Flowbite>
</div> </div>
</form> </form>
{poll && <PollForm poll={poll} setPoll={setPoll} />} {poll && <PollForm poll={poll} setPoll={setPoll} />}
@ -291,48 +278,84 @@ export default function Compose(props: Props) {
{attachments.length > 0 && ( {attachments.length > 0 && (
<div> <div>
<Checkbox id="sensitive" className="focus:ring-0" /> <Checkbox id="sensitive" label={formatMessage({ id: 'compose.nsfw' })} />
<Label htmlFor="sensitive" className="pl-2 text-gray-600">
<FormattedMessage id="compose.nsfw" />
</Label>
</div> </div>
)} )}
<div className="w-full flex justify-between mt-1 items-center h-5"> <div className="w-full flex justify-between mt-1 items-center h-5">
<div className="ml-1 flex gap-3"> <div className="ml-1 flex gap-3">
<input type="file" id="file" className="hidden" ref={uploaderRef} onChange={fileChanged} /> <input type="file" id="file" className="hidden" ref={uploaderRef} onChange={fileChanged} />
<FaPaperclip className="text-gray-400 hover:text-gray-600 cursor-pointer" onClick={selectFile} /> <IconButton variant="text" size="sm" onClick={selectFile} className="text-gray-400 hover:text-gray-600 text-base">
<FaListCheck className="text-gray-400 hover:text-gray-600 cursor-pointer" onClick={togglePoll} /> <FaPaperclip />
<Dropdown label="" dismissOnClick={true} placement="top" renderTrigger={() => visibilityIcon(visibility)}> </IconButton>
<Dropdown.Item onClick={() => setVisibility('public')}> <IconButton variant="text" size="sm" onClick={togglePoll} className="text-gray-400 hover:text-gray-600 text-base">
<FormattedMessage id="compose.visibility.public" /> <FaListCheck />
</Dropdown.Item> </IconButton>
<Dropdown.Item onClick={() => setVisibility('unlisted')}> <Popover open={popoverVisibility} handler={setPopoverVisibility}>
<FormattedMessage id="compose.visibility.unlisted" /> <PopoverHandler>{visibilityIcon(visibility)}</PopoverHandler>
</Dropdown.Item> <PopoverContent>
<Dropdown.Item onClick={() => setVisibility('private')}> <List>
<FormattedMessage id="compose.visibility.private" /> <ListItem
</Dropdown.Item> onClick={() => {
<Dropdown.Item onClick={() => setVisibility('direct')}> setVisibility('public')
<FormattedMessage id="compose.visibility.direct" /> setPopoverVisibility(false)
</Dropdown.Item> }}
</Dropdown> >
<FormattedMessage id="compose.visibility.public" />
</ListItem>
<ListItem
onClick={() => {
setVisibility('unlisted')
setPopoverVisibility(false)
}}
>
<FormattedMessage id="compose.visibility.unlisted" />
</ListItem>
<ListItem
onClick={() => {
setVisibility('private')
setPopoverVisibility(false)
}}
>
<FormattedMessage id="compose.visibility.private" />
</ListItem>
<ListItem
onClick={() => {
setVisibility('direct')
setPopoverVisibility(false)
}}
>
<FormattedMessage id="compose.visibility.direct" />
</ListItem>
</List>
</PopoverContent>
</Popover>
{cw ? ( {cw ? (
<span className="text-blue-400 hover:text-blue-600 leading-4 cursor-pointer" onClick={() => setCW(false)}> <IconButton
variant="text"
size="sm"
className="text-blue-400 hover:text-blue-600 leading-4 text-base"
onClick={() => setCW(false)}
>
CW CW
</span> </IconButton>
) : ( ) : (
<span className="text-gray-400 hover:text-gray-600 leading-4 cursor-pointer" onClick={() => setCW(true)}> <IconButton
variant="text"
size="sm"
className="text-gray-400 hover:text-gray-600 leading-4 text-base"
onClick={() => setCW(true)}
>
CW CW
</span> </IconButton>
)} )}
</div> </div>
<div className="mr-1"> <div className="mr-1 flex items-center gap-2">
<span className="text-gray-400">{remaining}</span> <span className="text-gray-400">{remaining}</span>
<button className="ml-2 text-gray-400 hover:text-gray-600" disabled={loading} onClick={post}> <IconButton disabled={loading} onClick={post} variant="text" size="sm">
{loading ? <Spinner size="sm" /> : <FaPaperPlane />} <FaPaperPlane className="text-base text-gray-600" />
</button> </IconButton>
</div> </div>
</div> </div>
<EditMedia media={editMedia} close={closeDescription} client={props.client} /> <EditMedia media={editMedia} close={closeDescription} client={props.client} />
@ -344,27 +367,27 @@ const visibilityIcon = (visibility: 'public' | 'unlisted' | 'private' | 'direct'
switch (visibility) { switch (visibility) {
case 'public': case 'public':
return ( return (
<span> <IconButton variant="text" size="sm" className="text-gray-400 hover:text-gray-600 text-base">
<FaGlobe className="text-gray-400 hover:text-gray-600 cursor-pointer" /> <FaGlobe />
</span> </IconButton>
) )
case 'unlisted': case 'unlisted':
return ( return (
<span> <IconButton variant="text" size="sm" className="text-gray-400 hover:text-gray-600 text-base">
<FaLockOpen className="text-gray-400 hover:text-gray-600 cursor-pointer" /> <FaLockOpen />
</span> </IconButton>
) )
case 'private': case 'private':
return ( return (
<span> <IconButton variant="text" size="sm" className="text-gray-400 hover:text-gray-600 text-base">
<FaLock className="text-gray-400 hover:text-gray-600 cursor-pointer" /> <FaLock />
</span> </IconButton>
) )
case 'direct': case 'direct':
return ( return (
<span> <IconButton variant="text" size="sm" className="text-gray-400 hover:text-gray-600 text-base">
<FaEnvelope className="text-gray-400 hover:text-gray-600 cursor-pointer" /> <FaEnvelope />
</span> </IconButton>
) )
} }
} }
@ -417,10 +440,11 @@ const PollForm = (props: PollProps) => {
) )
} }
const changeMultiple = (value: boolean) => { const changeMultiple = (ev: ChangeEvent<HTMLInputElement>) => {
console.log(ev)
props.setPoll(current => props.setPoll(current =>
Object.assign({}, current, { Object.assign({}, current, {
multiple: value multiple: ev.target.checked
}) })
) )
} }
@ -437,29 +461,42 @@ const PollForm = (props: PollProps) => {
<div className="pt-1"> <div className="pt-1">
{props.poll.options.map((option, index) => ( {props.poll.options.map((option, index) => (
<div className="flex items-center gap-3 py-1" key={index}> <div className="flex items-center gap-3 py-1" key={index}>
{props.poll.multiple ? <Checkbox disabled /> : <Radio disabled />} {props.poll.multiple ? (
<TextInput sizing="sm" value={option} onChange={ev => updateOption(index, ev.target.value)} /> <Checkbox disabled containerProps={{ className: 'p-1' }} />
) : (
<Radio disabled containerProps={{ className: 'p-1' }} />
)}
<Input
type="text"
color="blue"
value={option}
onChange={ev => updateOption(index, ev.target.value)}
containerProps={{ className: 'h-8' }}
/>
<FaXmark className="text-gray-400 cursor-pointer" onClick={() => removeOption(index)} /> <FaXmark className="text-gray-400 cursor-pointer" onClick={() => removeOption(index)} />
</div> </div>
))} ))}
<div className="flex gap-3 pt-2"> <div className="flex gap-3 pt-2">
<Button onClick={addOption} color="light"> <Button onClick={addOption} size="sm" color="indigo" variant="outlined">
<FormattedMessage id="compose.poll.add" /> <FormattedMessage id="compose.poll.add" />
</Button> </Button>
<Select id="expires" onChange={e => changeExpire(parseInt(e.target.value))}> <Select
id="expires"
color="blue"
value={`${props.poll.expires_in}`}
onChange={e => changeExpire(parseInt(e))}
containerProps={{ className: 'h-8' }}
>
{expiresList.map((expire, index) => ( {expiresList.map((expire, index) => (
<option value={expire.value} key={index}> <Option value={`${expire.value}`} key={index}>
{expire.label} {expire.label}
</option> </Option>
))} ))}
</Select> </Select>
</div> </div>
<ToggleSwitch <div className="mt-2">
checked={props.poll.multiple} <Switch checked={props.poll.multiple} onChange={v => changeMultiple(v)} label={formatMessage({ id: 'compose.poll.multiple' })} />
onChange={v => changeMultiple(v)} </div>
className="mt-2"
label={formatMessage({ id: 'compose.poll.multiple' })}
/>
</div> </div>
) )
} }

View File

@ -1,4 +1,4 @@
import { Label, Modal, Textarea, Button } from 'flowbite-react' import { Button, Dialog, DialogBody, DialogHeader, Textarea, Typography } from '@material-tailwind/react'
import { Entity, MegalodonInterface } from 'megalodon' import { Entity, MegalodonInterface } from 'megalodon'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { FormattedMessage } from 'react-intl' import { FormattedMessage } from 'react-intl'
@ -37,20 +37,20 @@ export default function EditMedia(props: Props) {
} }
return ( return (
<Modal show={props.media !== undefined} onClose={onClose} size="2xl"> <Dialog open={props.media !== undefined} handler={onClose} size="xl">
{props.media && ( {props.media && (
<> <>
<Modal.Header> <DialogHeader>
<FormattedMessage id="compose.edit_media.title" /> <FormattedMessage id="compose.edit_media.title" />
</Modal.Header> </DialogHeader>
<Modal.Body className="max-h-full max-w-full"> <DialogBody className="max-h-full max-w-full">
<div className="flex gap-2"> <div className="flex gap-2">
<div className="w-1/4"> <div className="w-1/4">
<Label htmlFor="description" className="mb-2 block"> <Typography>
<FormattedMessage id="compose.edit_media.label" /> <FormattedMessage id="compose.edit_media.label" />
</Label> </Typography>
<Textarea id="description" rows={4} value={description} onChange={ev => setDescription(ev.target.value)} /> <Textarea id="description" rows={4} value={description} onChange={ev => setDescription(ev.target.value)} />
<Button color="blue" className="mt-2" onClick={submit}> <Button className="mt-2" onClick={submit}>
<FormattedMessage id="compose.edit_media.submit" /> <FormattedMessage id="compose.edit_media.submit" />
</Button> </Button>
</div> </div>
@ -58,9 +58,10 @@ export default function EditMedia(props: Props) {
<img src={props.media.preview_url} className="object-cover m-auto" /> <img src={props.media.preview_url} className="object-cover m-auto" />
</div> </div>
</div> </div>
</Modal.Body> </DialogBody>
</> </>
)} )}
</Modal> <></>
</Dialog>
) )
} }

View File

@ -1,14 +1,28 @@
import emojify from '@/utils/emojify' import emojify from '@/utils/emojify'
import { Avatar, Button, CustomFlowbiteTheme, Dropdown, Flowbite, Tabs } from 'flowbite-react'
import { Entity, MegalodonInterface } from 'megalodon' import { Entity, MegalodonInterface } from 'megalodon'
import { MouseEventHandler, useEffect, useState } from 'react' import { MouseEventHandler, useEffect, useState } from 'react'
import { FaEllipsisVertical } from 'react-icons/fa6' import { FaEllipsisVertical } from 'react-icons/fa6'
import { FormattedMessage, useIntl } from 'react-intl' import { FormattedMessage } from 'react-intl'
import Timeline from './profile/Timeline' import Timeline from './profile/Timeline'
import Followings from './profile/Followings' import Followings from './profile/Followings'
import Followers from './profile/Followers' import Followers from './profile/Followers'
import { findLink } from '@/utils/statusParser' import { findLink } from '@/utils/statusParser'
import { Account } from '@/db' import { Account } from '@/db'
import {
Avatar,
Button,
IconButton,
List,
ListItem,
Popover,
PopoverContent,
PopoverHandler,
Tab,
TabPanel,
Tabs,
TabsBody,
TabsHeader
} from '@material-tailwind/react'
type Props = { type Props = {
client: MegalodonInterface client: MegalodonInterface
@ -17,20 +31,10 @@ type Props = {
openMedia: (media: Entity.Attachment) => void openMedia: (media: Entity.Attachment) => void
} }
const customTheme: CustomFlowbiteTheme = {
tabs: {
tablist: {
tabitem: {
base: 'flex items-center justify-center p-4 rounded-t-lg text-sm font-medium first:ml-0 disabled:cursor-not-allowed disabled:text-gray-400 disabled:dark:text-gray-500'
}
}
}
}
export default function Profile(props: Props) { export default function Profile(props: Props) {
const [user, setUser] = useState<Entity.Account | null>(null) const [user, setUser] = useState<Entity.Account | null>(null)
const [relationship, setRelationship] = useState<Entity.Relationship | null>(null) const [relationship, setRelationship] = useState<Entity.Relationship | null>(null)
const { formatMessage } = useIntl() const [popoverDetail, setPopoverDetail] = useState(false)
useEffect(() => { useEffect(() => {
const f = async () => { const f = async () => {
@ -69,74 +73,92 @@ export default function Profile(props: Props) {
return ( return (
<div style={{ height: 'calc(100% - 50px)' }} className="overflow-y-auto timeline-scrollable"> <div style={{ height: 'calc(100% - 50px)' }} className="overflow-y-auto timeline-scrollable">
<Flowbite theme={{ theme: customTheme }}> {user && relationship && (
{user && relationship && ( <>
<> <div className="header-image w-full bg-gray-100">
<div className="header-image w-full bg-gray-100"> <img src={user.header} alt="header image" className="w-full object-cover h-40" />
<img src={user.header} alt="header image" className="w-full object-cover h-40" /> </div>
</div> <div className="p-5">
<div className="p-5"> <div className="flex items-end justify-between" style={{ marginTop: '-50px' }}>
<div className="flex items-end justify-between" style={{ marginTop: '-50px' }}> <Avatar src={user.avatar} size="xl" variant="rounded" />
<Avatar img={user.avatar} size="lg" stacked /> <div className="flex gap-2">
<div className="flex gap-2"> {relationship.following ? (
{relationship.following ? ( <Button color="red" onClick={() => unfollow(user.id)}>
<Button color="failure" onClick={() => unfollow(user.id)}> <FormattedMessage id="profile.unfollow" />
<FormattedMessage id="profile.unfollow" /> </Button>
</Button> ) : (
) : ( <Button color="blue" onClick={() => follow(user.id)}>
<Button color="blue" onClick={() => follow(user.id)}> <FormattedMessage id="profile.follow" />
<FormattedMessage id="profile.follow" /> </Button>
</Button> )}
)} <Popover open={popoverDetail} handler={setPopoverDetail}>
<Dropdown <PopoverHandler>
label="" <IconButton variant="outlined">
renderTrigger={() => ( <FaEllipsisVertical />
<Button color="gray"> </IconButton>
<FaEllipsisVertical /> </PopoverHandler>
</Button> <PopoverContent>
)} <List>
> <ListItem
<Dropdown.Item onClick={() => openOriginal(user.url)}> onClick={() => {
<FormattedMessage id="profile.open_original" /> openOriginal(user.url)
</Dropdown.Item> setPopoverDetail(false)
</Dropdown> }}
</div> >
</div> <FormattedMessage id="profile.open_original" />
<div className="pt-4"> </ListItem>
<div className="font-bold" dangerouslySetInnerHTML={{ __html: emojify(user.display_name, user.emojis) }} /> </List>
<div className="text-gray-500">@{user.acct}</div> </PopoverContent>
<div className="mt-4 raw-html profile" onClick={profileClicked}> </Popover>
<span
dangerouslySetInnerHTML={{ __html: emojify(user.note, user.emojis) }}
className="overflow-hidden break-all text-gray-800"
/>
</div>
<div className="bg-gray-100 overflow-hidden break-all raw-html mt-2 profile" onClick={profileClicked}>
{user.fields.map((data, index) => (
<dl key={index} className="px-4 py-2 border-gray-200 border-b">
<dt className="text-gray-500">{data.name}</dt>
<dd className="text-gray-700" dangerouslySetInnerHTML={{ __html: emojify(data.value, user.emojis) }} />
</dl>
))}
</div>
</div> </div>
</div> </div>
<div> <div className="pt-4">
<Tabs aria-label="Tabs with icons" style="underline"> <div className="font-bold" dangerouslySetInnerHTML={{ __html: emojify(user.display_name, user.emojis) }} />
<Tabs.Item active title={formatMessage({ id: 'profile.timeline' })}> <div className="text-gray-500">@{user.acct}</div>
<div className="mt-4 raw-html profile" onClick={profileClicked}>
<span
dangerouslySetInnerHTML={{ __html: emojify(user.note, user.emojis) }}
className="overflow-hidden break-all text-gray-800"
/>
</div>
<div className="bg-gray-100 overflow-hidden break-all raw-html mt-2 profile" onClick={profileClicked}>
{user.fields.map((data, index) => (
<dl key={index} className="px-4 py-2 border-gray-200 border-b">
<dt className="text-gray-500">{data.name}</dt>
<dd className="text-gray-700" dangerouslySetInnerHTML={{ __html: emojify(data.value, user.emojis) }} />
</dl>
))}
</div>
</div>
</div>
<div>
<Tabs value="timeline">
<TabsHeader>
<Tab value="timeline">
<FormattedMessage id="profile.timeline" />
</Tab>
<Tab value="followings">
<FormattedMessage id="profile.followings" />
</Tab>
<Tab value="followers">
<FormattedMessage id="profile.followers" />
</Tab>
</TabsHeader>
<TabsBody>
<TabPanel value="timeline">
<Timeline client={props.client} account={props.account} user_id={props.user_id} openMedia={props.openMedia} /> <Timeline client={props.client} account={props.account} user_id={props.user_id} openMedia={props.openMedia} />
</Tabs.Item> </TabPanel>
<Tabs.Item title={formatMessage({ id: 'profile.followings' })}> <TabPanel value="followings">
<Followings client={props.client} user_id={props.user_id} /> <Followings client={props.client} user_id={props.user_id} />
</Tabs.Item> </TabPanel>
<Tabs.Item title={formatMessage({ id: 'profile.followers' })} className="focus:ring-0"> <TabPanel value="followers">
<Followers client={props.client} user_id={props.user_id} /> <Followers client={props.client} user_id={props.user_id} />
</Tabs.Item> </TabPanel>
</Tabs> </TabsBody>
</div> </Tabs>
</> </div>
)} </>
</Flowbite> )}
</div> </div>
) )
} }

View File

@ -1,5 +1,5 @@
import emojify from '@/utils/emojify' import emojify from '@/utils/emojify'
import { Avatar } from 'flowbite-react' import { Avatar } from '@material-tailwind/react'
import { Entity } from 'megalodon' import { Entity } from 'megalodon'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
@ -18,7 +18,7 @@ export default function User(props: Props) {
<div className="border-b mr-2 py-1"> <div className="border-b mr-2 py-1">
<div className="flex" onClick={() => openUser(props.user.id)}> <div className="flex" onClick={() => openUser(props.user.id)}>
<div className="p2 cursor-pointer" style={{ width: '56px' }}> <div className="p2 cursor-pointer" style={{ width: '56px' }}>
<Avatar img={props.user.avatar} /> <Avatar src={props.user.avatar} />
</div> </div>
<div> <div>
<p className="text-gray-800" dangerouslySetInnerHTML={{ __html: emojify(props.user.display_name, props.user.emojis) }} /> <p className="text-gray-800" dangerouslySetInnerHTML={{ __html: emojify(props.user.display_name, props.user.emojis) }} />

View File

@ -1,15 +1,15 @@
import { CSSProperties, useContext, useEffect, useRef, useState } from 'react' import { CSSProperties, useContext, useEffect, useRef, useState } from 'react'
import { FaGear, FaPlus } from 'react-icons/fa6' import { FaGear, FaPlus, FaTrash } from 'react-icons/fa6'
import { Account, db } from '@/db' import { Account, db } from '@/db'
import NewAccount from '@/components/accounts/New' import NewAccount from '@/components/accounts/New'
import Settings from '@/components/Settings' import Settings from '@/components/Settings'
import { Avatar, Dropdown } from 'flowbite-react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { FormattedMessage, useIntl } from 'react-intl' import { FormattedMessage, useIntl } from 'react-intl'
import generateNotification from '@/utils/notification' import generateNotification from '@/utils/notification'
import generator, { Entity, WebSocketInterface } from 'megalodon' import generator, { Entity, WebSocketInterface } from 'megalodon'
import { Context } from '@/utils/i18n' import { Context } from '@/utils/i18n'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { Avatar, IconButton, List, ListItem, ListItemPrefix, Popover, PopoverContent, PopoverHandler } from '@material-tailwind/react'
type LayoutProps = { type LayoutProps = {
children: React.ReactNode children: React.ReactNode
@ -20,6 +20,8 @@ export default function Layout({ children }: LayoutProps) {
const [openNewModal, setOpenNewModal] = useState(false) const [openNewModal, setOpenNewModal] = useState(false)
const [openSettings, setOpenSettings] = useState(false) const [openSettings, setOpenSettings] = useState(false)
const [style, setStyle] = useState<CSSProperties>({}) const [style, setStyle] = useState<CSSProperties>({})
const [openPopover, setOpenPopover] = useState(false)
const { switchLang } = useContext(Context) const { switchLang } = useContext(Context)
const router = useRouter() const router = useRouter()
const { formatMessage } = useIntl() const { formatMessage } = useIntl()
@ -91,8 +93,6 @@ export default function Layout({ children }: LayoutProps) {
document.getElementById(`${id}`).click() document.getElementById(`${id}`).click()
} }
const dropdownTrigger = (accountId: number) => <span id={`${accountId}`} className="" />
const removeAccount = async (id: number) => { const removeAccount = async (id: number) => {
await db.accounts.delete(id) await db.accounts.delete(id)
const acct = await db.accounts.toArray() const acct = await db.accounts.toArray()
@ -105,9 +105,9 @@ export default function Layout({ children }: LayoutProps) {
const selectedClassName = (id: number) => { const selectedClassName = (id: number) => {
if (id === parseInt(router.query.id as string)) { if (id === parseInt(router.query.id as string)) {
return 'bg-blue-950 cursor-pointer' return 'bg-blue-950 cursor-pointer text-center'
} else { } else {
return 'cursor-pointer' return 'cursor-pointer text-center'
} }
} }
@ -131,44 +131,61 @@ export default function Layout({ children }: LayoutProps) {
<div> <div>
{accounts.map(account => ( {accounts.map(account => (
<div key={account.id} className={selectedClassName(account.id)}> <div key={account.id} className={selectedClassName(account.id)}>
<Popover>
<PopoverHandler>
<span id={`${account.id}`} />
</PopoverHandler>
<PopoverContent>
<List className="py-2 px-0">
<ListItem onClick={() => removeAccount(account.id)} className="py-2 px-4 rounded-none">
<ListItemPrefix>
<FaTrash />
</ListItemPrefix>
<FormattedMessage id="accounts.remove" />
</ListItem>
</List>
</PopoverContent>
</Popover>
<Avatar <Avatar
alt={account.domain} alt={account.domain}
img={account.avatar} src={account.avatar}
rounded className="p-1"
key={account.id}
className="py-2"
onClick={() => openAccount(account.id)} onClick={() => openAccount(account.id)}
onContextMenu={() => openContextMenu(account.id)} onContextMenu={() => openContextMenu(account.id)}
/> />
<Dropdown label="" dismissOnClick={true} renderTrigger={() => dropdownTrigger(account.id)}>
<Dropdown.Item onClick={() => removeAccount(account.id)}>
<FormattedMessage id="accounts.remove" />
</Dropdown.Item>
</Dropdown>
</div> </div>
))} ))}
<button className="py-4 px-6 items-center" onClick={() => setOpenNewModal(true)}> <div className="flex flex-col items-center">
<FaPlus className="text-gray-400" /> <IconButton variant="text" size="lg" onClick={() => setOpenNewModal(true)}>
</button> <FaPlus className="text-gray-400 text-xl" />
</IconButton>
</div>
<NewAccount opened={openNewModal} close={closeNewModal} /> <NewAccount opened={openNewModal} close={closeNewModal} />
</div> </div>
<div className="settings text-gray-400 py-4 px-6 items-center"> <div className="settings text-gray-400 flex flex-col items-center mb-2">
<div className="relative cursor-pointer"> <Popover open={openPopover} handler={setOpenPopover}>
<Dropdown <PopoverHandler>
label="" <IconButton variant="text" size="lg">
dismissOnClick <FaGear className="text-gray-400 text-xl" />
renderTrigger={() => ( </IconButton>
<span> </PopoverHandler>
<FaGear /> <PopoverContent>
</span> <List className="py-2 px-0">
)} <ListItem
placement="right-start" onClick={() => {
> setOpenSettings(true)
<Dropdown.Item onClick={() => setOpenSettings(true)}> setOpenPopover(false)
<FormattedMessage id="settings.title" />{' '} }}
</Dropdown.Item> className="py-2 px-4 rounded-none"
</Dropdown> >
</div> <ListItemPrefix>
<FaGear />
</ListItemPrefix>
<FormattedMessage id="settings.title" />
</ListItem>
</List>
</PopoverContent>
</Popover>
</div> </div>
</aside> </aside>
{children} {children}

View File

@ -1,5 +1,5 @@
import { Account, db } from '@/db' import { Account, db } from '@/db'
import { CustomFlowbiteTheme, Flowbite, Sidebar } from 'flowbite-react' import { Card, List, ListItem, ListItemPrefix } from '@material-tailwind/react'
import generator, { Entity } from 'megalodon' import generator, { Entity } from 'megalodon'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -10,22 +10,6 @@ type LayoutProps = {
children: React.ReactNode children: React.ReactNode
} }
const customTheme: CustomFlowbiteTheme = {
sidebar: {
root: {
inner: 'h-full overflow-y-auto overflow-x-hidden bg-blue-950 py-4 px-3 dark:bg-blue-950'
},
item: {
base: 'flex items-center justify-center rounded-lg p-2 text-base font-normal text-blue-200 hover:bg-blue-900 dark:text-blue-200 dark:hover:bg-blue-900 cursor-pointer',
active: 'bg-blue-400 text-gray-800 hover:bg-blue-300',
icon: {
base: 'h-5 w-5 flex-shrink-0 text-gray-500 transition duration-75 text-blue-200 group-hover:text-blue-900 dark:text-blue-200 dark:group-hover:text-blue-900',
active: 'text-gray-800'
}
}
}
}
export default function Layout({ children }: LayoutProps) { export default function Layout({ children }: LayoutProps) {
const router = useRouter() const router = useRouter()
const { formatMessage } = useIntl() const { formatMessage } = useIntl()
@ -58,65 +42,63 @@ export default function Layout({ children }: LayoutProps) {
{ {
id: 'home', id: 'home',
title: formatMessage({ id: 'timeline.home' }), title: formatMessage({ id: 'timeline.home' }),
icon: FaHouse, icon: <FaHouse />,
path: `/accounts/${router.query.id}/home` path: `/accounts/${router.query.id}/home`
}, },
{ {
id: 'notifications', id: 'notifications',
title: formatMessage({ id: 'timeline.notifications' }), title: formatMessage({ id: 'timeline.notifications' }),
icon: FaBell, icon: <FaBell />,
path: `/accounts/${router.query.id}/notifications` path: `/accounts/${router.query.id}/notifications`
}, },
{ {
id: 'local', id: 'local',
title: formatMessage({ id: 'timeline.local' }), title: formatMessage({ id: 'timeline.local' }),
icon: FaUsers, icon: <FaUsers />,
path: `/accounts/${router.query.id}/local` path: `/accounts/${router.query.id}/local`
}, },
{ {
id: 'public', id: 'public',
title: formatMessage({ id: 'timeline.public' }), title: formatMessage({ id: 'timeline.public' }),
icon: FaGlobe, icon: <FaGlobe />,
path: `/accounts/${router.query.id}/public` path: `/accounts/${router.query.id}/public`
} }
] ]
return ( return (
<section className="flex h-screen w-full overflow-hidden"> <section className="flex h-screen w-full overflow-hidden">
<Flowbite theme={{ theme: customTheme }}> <Card className="text-blue-100 sidebar w-64 bg-blue-950 rounded-none">
<Sidebar className="text-blue-200 sidebar"> <div className="max-w-full pl-4 mt-2 mb-4 my-profile">
<div className="max-w-full pl-4 mt-2 mb-4 my-profile"> <p>{account?.username}</p>
<p>{account?.username}</p> <p>@{account?.domain}</p>
<p>@{account?.domain}</p> </div>
</div> <List className="min-w-[64px]">
<Sidebar.Items> {pages.map(page => (
<Sidebar.ItemGroup> <ListItem
{pages.map(page => ( key={page.id}
<Sidebar.Item selected={router.asPath.includes(page.path)}
key={page.id} onClick={() => router.push(page.path)}
active={router.asPath.includes(page.path)} className="sidebar-menu-item text-blue-100"
onClick={() => router.push(page.path)} >
icon={page.icon} <ListItemPrefix>{page.icon}</ListItemPrefix>
className="sidebar-menu-item" <span className="sidebar-menu">{page.title}</span>
> </ListItem>
<span className="sidebar-menu">{page.title}</span> ))}
</Sidebar.Item> {lists.map(list => (
))} <ListItem
{lists.map(list => ( key={list.id}
<Sidebar.Item selected={router.asPath.includes(`list_${list.id}`)}
key={list.id} onClick={() => router.push({ pathname: `/accounts/${router.query.id}/list_${list.id}` })}
active={router.asPath.includes(`list_${list.id}`)} className="sidebar-menu-item text-blue-100"
onClick={() => router.push({ pathname: `/accounts/${router.query.id}/list_${list.id}` })} >
icon={FaList} <ListItemPrefix>
className="sidebar-menu-item" <FaList />
> </ListItemPrefix>
<span className="sidebar-menu">{list.title}</span> <span className="sidebar-menu">{list.title}</span>
</Sidebar.Item> </ListItem>
))} ))}
</Sidebar.ItemGroup> </List>
</Sidebar.Items> </Card>
</Sidebar>
</Flowbite>
{children} {children}
</section> </section>
) )

View File

@ -1,4 +1,4 @@
import { Button, Label, Modal, Textarea } from 'flowbite-react' import { Button, Dialog, DialogBody, DialogHeader, Textarea, Typography } from '@material-tailwind/react'
import { Entity, MegalodonInterface } from 'megalodon' import { Entity, MegalodonInterface } from 'megalodon'
import { FormattedMessage } from 'react-intl' import { FormattedMessage } from 'react-intl'
@ -21,23 +21,24 @@ export default function Report(props: Props) {
} }
return ( return (
<Modal show={props.open} onClose={props.close} size="xl"> <Dialog open={props.open} handler={props.close} size="md">
<Modal.Header> <DialogHeader>
<FormattedMessage id="report.title" values={{ user: `@${props.status.account.username}` }} /> <FormattedMessage id="report.title" values={{ user: `@${props.status.account.username}` }} />
</Modal.Header> </DialogHeader>
<Modal.Body className="max-h-full max-w-full"> <DialogBody className="max-h-full max-w-full">
<form> <form>
<div className="block"> <div className="block">
<Label htmlFor="comment"> <Typography>
<FormattedMessage id="report.detail" /> <FormattedMessage id="report.detail" />
</Label> </Typography>
<Textarea id="comment" />
<Textarea id="comment" rows={4} /> <Textarea id="comment" rows={4} />
</div> </div>
<Button color="blue" className="mt-2" onClick={submit}> <Button className="mt-2" onClick={submit}>
<FormattedMessage id="report.submit" /> <FormattedMessage id="report.submit" />
</Button> </Button>
</form> </form>
</Modal.Body> </DialogBody>
</Modal> </Dialog>
) )
} }

View File

@ -1,10 +1,10 @@
import { Account } from '@/db' import { Account } from '@/db'
import { TextInput } from 'flowbite-react'
import generator, { Entity, MegalodonInterface, WebSocketInterface } from 'megalodon' import generator, { Entity, MegalodonInterface, WebSocketInterface } from 'megalodon'
import { useEffect, useState, useCallback, useRef, Dispatch, SetStateAction } from 'react' import { useEffect, useState, useCallback, useRef, Dispatch, SetStateAction } from 'react'
import { FormattedMessage, useIntl } from 'react-intl' import { FormattedMessage, useIntl } from 'react-intl'
import { Virtuoso } from 'react-virtuoso' import { Virtuoso } from 'react-virtuoso'
import Notification from './notification/Notification' import Notification from './notification/Notification'
import { Input, Spinner } from '@material-tailwind/react'
const TIMELINE_STATUSES_COUNT = 30 const TIMELINE_STATUSES_COUNT = 30
const TIMELINE_MAX_STATUSES = 2147483647 const TIMELINE_MAX_STATUSES = 2147483647
@ -107,32 +107,38 @@ export default function Notifications(props: Props) {
</div> </div>
<div className="w-64 text-xs"> <div className="w-64 text-xs">
<form> <form>
<TextInput type="text" placeholder={formatMessage({ id: 'timeline.search' })} disabled sizing="sm" /> <Input type="text" placeholder={formatMessage({ id: 'timeline.search' })} disabled />
</form> </form>
</div> </div>
</div> </div>
<div className="timeline overflow-y-auto w-full overflow-x-hidden" style={{ height: 'calc(100% - 50px)' }}> <div className="timeline overflow-y-auto w-full overflow-x-hidden" style={{ height: 'calc(100% - 50px)' }}>
<Virtuoso {notifications.length > 0 ? (
style={{ height: '100%' }} <Virtuoso
scrollerRef={ref => { style={{ height: '100%' }}
scrollerRef.current = ref as HTMLElement scrollerRef={ref => {
}} scrollerRef.current = ref as HTMLElement
firstItemIndex={firstItemIndex} }}
atTopStateChange={prependUnreads} firstItemIndex={firstItemIndex}
className="timeline-scrollable" atTopStateChange={prependUnreads}
data={notifications} className="timeline-scrollable"
endReached={loadMore} data={notifications}
itemContent={(_, notification) => ( endReached={loadMore}
<Notification itemContent={(_, notification) => (
client={props.client} <Notification
account={props.account} client={props.client}
notification={notification} account={props.account}
onRefresh={updateStatus} notification={notification}
key={notification.id} onRefresh={updateStatus}
openMedia={media => props.setAttachment(media)} key={notification.id}
/> openMedia={media => props.setAttachment(media)}
)} />
/> )}
/>
) : (
<div className="w-full" style={{ height: '100%' }}>
<Spinner className="m-auto mt-6" />
</div>
)}
</div> </div>
</section> </section>
) )

View File

@ -1,5 +1,4 @@
import { Account } from '@/db' import { Account } from '@/db'
import { TextInput } from 'flowbite-react'
import generator, { Entity, MegalodonInterface, WebSocketInterface } from 'megalodon' import generator, { Entity, MegalodonInterface, WebSocketInterface } from 'megalodon'
import { useEffect, useState, useCallback, useRef, Dispatch, SetStateAction } from 'react' import { useEffect, useState, useCallback, useRef, Dispatch, SetStateAction } from 'react'
import { Virtuoso } from 'react-virtuoso' import { Virtuoso } from 'react-virtuoso'
@ -9,6 +8,7 @@ import Detail from '../detail/Detail'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import Compose from '../compose/Compose' import Compose from '../compose/Compose'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { Input, Spinner } from '@material-tailwind/react'
const TIMELINE_STATUSES_COUNT = 30 const TIMELINE_STATUSES_COUNT = 30
const TIMELINE_MAX_STATUSES = 2147483647 const TIMELINE_MAX_STATUSES = 2147483647
@ -189,32 +189,46 @@ export default function Timeline(props: Props) {
</div> </div>
<div className="w-64 text-xs"> <div className="w-64 text-xs">
<form> <form>
<TextInput type="text" placeholder={formatMessage({ id: 'timeline.search' })} disabled sizing="sm" /> <Input
type="text"
label={formatMessage({ id: 'timeline.search' })}
disabled
containerProps={{ className: 'h-7' }}
className="!py-1 !px-2 !text-xs"
labelProps={{ className: '!leading-10' }}
/>
</form> </form>
</div> </div>
</div> </div>
<div className="overflow-x-hidden" style={{ height: 'calc(100% - 50px)' }}> <div className="overflow-x-hidden" style={{ height: 'calc(100% - 50px)' }}>
<Virtuoso {statuses.length > 0 ? (
style={{ height: `calc(100% - ${composeHeight}px)` }} <Virtuoso
scrollerRef={ref => { style={{ height: `calc(100% - ${composeHeight}px)` }}
scrollerRef.current = ref as HTMLElement scrollerRef={ref => {
}} scrollerRef.current = ref as HTMLElement
className="timeline-scrollable" }}
firstItemIndex={firstItemIndex} className="timeline-scrollable"
atTopStateChange={prependUnreads} firstItemIndex={firstItemIndex}
data={statuses} atTopStateChange={prependUnreads}
endReached={loadMore} data={statuses}
itemContent={(_, status) => ( endReached={loadMore}
<Status itemContent={(_, status) => (
client={props.client} <Status
account={props.account} client={props.client}
status={status} account={props.account}
key={status.id} status={status}
onRefresh={status => setStatuses(current => updateStatus(current, status))} key={status.id}
openMedia={media => props.setAttachment(media)} onRefresh={status => setStatuses(current => updateStatus(current, status))}
/> openMedia={media => props.setAttachment(media)}
)} />
/> )}
/>
) : (
<div className="w-full" style={{ height: `calc(100% - ${composeHeight}px)` }}>
<Spinner className="m-auto mt-6" />
</div>
)}
<div ref={composeRef}> <div ref={composeRef}>
<Compose client={props.client} /> <Compose client={props.client} />
</div> </div>

View File

@ -1,8 +1,8 @@
import { Avatar } from 'flowbite-react'
import { Entity } from 'megalodon' import { Entity } from 'megalodon'
import { FaUserPlus } from 'react-icons/fa6' import { FaUserPlus } from 'react-icons/fa6'
import { FormattedMessage, useIntl } from 'react-intl' import { FormattedMessage, useIntl } from 'react-intl'
import emojify from '@/utils/emojify' import emojify from '@/utils/emojify'
import { Avatar } from '@material-tailwind/react'
type Props = { type Props = {
notification: Entity.Notification notification: Entity.Notification
@ -35,7 +35,7 @@ export default function Follow(props: Props) {
</div> </div>
<div className="flex"> <div className="flex">
<div className="p-2" style={{ width: '56px' }}> <div className="p-2" style={{ width: '56px' }}>
<Avatar img={props.notification.account.avatar} /> <Avatar src={props.notification.account.avatar} />
</div> </div>
<div style={{ width: 'calc(100% - 56px)' }}> <div style={{ width: 'calc(100% - 56px)' }}>
<div className="flex"> <div className="flex">

View File

@ -1,4 +1,3 @@
import { Avatar } from 'flowbite-react'
import { Entity, MegalodonInterface } from 'megalodon' import { Entity, MegalodonInterface } from 'megalodon'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import emojify from '@/utils/emojify' import emojify from '@/utils/emojify'
@ -9,6 +8,7 @@ import Media from '../status/Media'
import { FaBarsProgress, FaHouse, FaPenToSquare, FaRetweet, FaStar } from 'react-icons/fa6' import { FaBarsProgress, FaHouse, FaPenToSquare, FaRetweet, FaStar } from 'react-icons/fa6'
import { useIntl } from 'react-intl' import { useIntl } from 'react-intl'
import { useState } from 'react' import { useState } from 'react'
import { Avatar } from '@material-tailwind/react'
type Props = { type Props = {
notification: Entity.Notification notification: Entity.Notification
@ -49,7 +49,7 @@ export default function Reaction(props: Props) {
</div> </div>
<div className="flex"> <div className="flex">
<div className="p-2" style={{ width: '56px' }}> <div className="p-2" style={{ width: '56px' }}>
<Avatar img={status.account.avatar} /> <Avatar src={status.account.avatar} />
</div> </div>
<div className="text-gray-600 break-all overflow-hidden" style={{ width: 'calc(100% - 56px)' }}> <div className="text-gray-600 break-all overflow-hidden" style={{ width: 'calc(100% - 56px)' }}>
<div className="flex justify-between"> <div className="flex justify-between">

View File

@ -1,4 +1,3 @@
import { CustomFlowbiteTheme, Dropdown, Flowbite } from 'flowbite-react'
import { Entity, MegalodonInterface } from 'megalodon' import { Entity, MegalodonInterface } from 'megalodon'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { FaBookmark, FaEllipsis, FaFaceLaughBeam, FaReply, FaRetweet, FaStar } from 'react-icons/fa6' import { FaBookmark, FaEllipsis, FaFaceLaughBeam, FaReply, FaRetweet, FaStar } from 'react-icons/fa6'
@ -6,6 +5,8 @@ import Picker from '@emoji-mart/react'
import { data } from '@/utils/emojiData' import { data } from '@/utils/emojiData'
import { Account } from '@/db' import { Account } from '@/db'
import { FormattedMessage } from 'react-intl' import { FormattedMessage } from 'react-intl'
import { IconButton, List, ListItem, Popover, PopoverContent, PopoverHandler } from '@material-tailwind/react'
import { useState } from 'react'
type Props = { type Props = {
status: Entity.Status status: Entity.Status
@ -14,18 +15,9 @@ type Props = {
onRefresh: () => void onRefresh: () => void
} }
const customTheme: CustomFlowbiteTheme = {
dropdown: {
content: 'focus:outline-none',
floating: {
item: {
base: 'hidden'
}
}
}
}
export default function Actions(props: Props) { export default function Actions(props: Props) {
const [popoverDetail, setPopoverDetail] = useState(false)
const [popoverEmoji, setPopoverEmoji] = useState(false)
const router = useRouter() const router = useRouter()
const reply = async () => { const reply = async () => {
@ -61,53 +53,56 @@ export default function Actions(props: Props) {
const onEmojiSelect = async emoji => { const onEmojiSelect = async emoji => {
await props.client.createEmojiReaction(props.status.id, emoji.native) await props.client.createEmojiReaction(props.status.id, emoji.native)
const dummy = document.getElementById('dummy-emoji-picker') setPopoverDetail(false)
dummy.click()
props.onRefresh() props.onRefresh()
} }
const report = () => { const report = () => {
setPopoverDetail(false)
router.push({ query: { id: router.query.id, timeline: router.query.timeline, report_target_id: props.status.id, modal: true } }) router.push({ query: { id: router.query.id, timeline: router.query.timeline, report_target_id: props.status.id, modal: true } })
} }
return ( return (
<div className="flex gap-6"> <div className="flex gap-2">
<FaReply className={`w-4 text-gray-400 cursor-pointer hover:text-gray-600`} onClick={reply} /> <IconButton variant="text" size="sm" onClick={reply} className="text-gray-400 text-base hover:text-gray-600">
<FaRetweet className={`${retweetColor(props.status)} w-4 cursor-pointer hover:text-gray-600`} onClick={reblog} /> <FaReply className="w-4" />
<FaStar className={`${favouriteColor(props.status)} w-4 cursor-pointer hover:text-gray-600`} onClick={favourite} /> </IconButton>
<FaBookmark className={`${bookmarkColor(props.status)} w-4 cursor-pointer hover:text-gray-600`} onClick={bookmark} /> <IconButton variant="text" size="sm" onClick={reblog} className={`${retweetColor(props.status)} text-base hover:text-gray-600`}>
<FaRetweet className="w-4" />
</IconButton>
<IconButton variant="text" size="sm" onClick={favourite} className={`${favouriteColor(props.status)} text-base hover:text-gray-600`}>
<FaStar className="w-4" />
</IconButton>
<IconButton variant="text" size="sm" onClick={bookmark} className={`${bookmarkColor(props.status)} text-base hover:text-gray-600`}>
<FaBookmark className="w-4" />
</IconButton>
{props.account.sns !== 'mastodon' && ( {props.account.sns !== 'mastodon' && (
<Flowbite theme={{ theme: customTheme }}> <Popover open={popoverEmoji} handler={setPopoverEmoji}>
<Dropdown <PopoverHandler>
label="" <IconButton variant="text" size="sm" className="text-gray-400 hover:text-gray-600 text-base">
dismissOnClick <FaFaceLaughBeam />
renderTrigger={() => ( </IconButton>
<span className="text-gray-400 hover:text-gray-600 cursor-pointer"> </PopoverHandler>
<FaFaceLaughBeam /> <PopoverContent className="z-10">
</span>
)}
>
<Picker data={data} onEmojiSelect={onEmojiSelect} previewPosition="none" set="native" perLine="7" theme="light" /> <Picker data={data} onEmojiSelect={onEmojiSelect} previewPosition="none" set="native" perLine="7" theme="light" />
<Dropdown.Item> </PopoverContent>
<span id="dummy-emoji-picker" /> </Popover>
</Dropdown.Item>
</Dropdown>
</Flowbite>
)} )}
<Dropdown <Popover open={popoverDetail} handler={setPopoverDetail}>
label="" <PopoverHandler>
dismissOnClick <IconButton variant="text" size="sm" className="text-gray-400 hover:text-gray-600 text-base">
renderTrigger={() => (
<span className="text-gray-400 hover:text-gray-600 cursor-pointer">
<FaEllipsis className="w-4" /> <FaEllipsis className="w-4" />
</span> </IconButton>
)} </PopoverHandler>
> <PopoverContent className="z-10">
<Dropdown.Item onClick={report}> <List className="py-2 px-0">
<FormattedMessage id="timeline.status.report" values={{ user: `@${props.status.account.acct}` }} /> <ListItem onClick={report} className="rounded-none">
</Dropdown.Item> <FormattedMessage id="timeline.status.report" values={{ user: `@${props.status.account.acct}` }} />
</Dropdown> </ListItem>
</List>
</PopoverContent>
</Popover>
</div> </div>
) )
} }
@ -130,7 +125,7 @@ const favouriteColor = (status: Entity.Status) => {
const bookmarkColor = (status: Entity.Status) => { const bookmarkColor = (status: Entity.Status) => {
if (status.bookmarked) { if (status.bookmarked) {
return 'text-rose-500' return 'text-red-500'
} else { } else {
return 'text-gray-400' return 'text-gray-400'
} }

View File

@ -1,8 +1,8 @@
import { Entity } from 'megalodon' import { Entity } from 'megalodon'
import { Dispatch, HTMLAttributes, SetStateAction } from 'react' import { Dispatch, HTMLAttributes, SetStateAction } from 'react'
import emojify from '@/utils/emojify' import emojify from '@/utils/emojify'
import { Button } from 'flowbite-react'
import { FormattedMessage } from 'react-intl' import { FormattedMessage } from 'react-intl'
import { Button } from '@material-tailwind/react'
type Props = { type Props = {
status: Entity.Status status: Entity.Status
@ -24,7 +24,7 @@ export default function Body(props: Props) {
dangerouslySetInnerHTML={{ __html: emojify(props.status.spoiler_text, props.status.emojis) }} dangerouslySetInnerHTML={{ __html: emojify(props.status.spoiler_text, props.status.emojis) }}
onClick={props.onClick} onClick={props.onClick}
/> />
<Button size="xs" color="gray" className="focus:ring-0 my-1" onClick={() => setSpoilered(current => !current)}> <Button size="sm" onClick={() => setSpoilered(current => !current)} variant="outlined" color="blue-gray">
{spoilered ? <FormattedMessage id="timeline.status.show_more" /> : <FormattedMessage id="timeline.status.show_less" />} {spoilered ? <FormattedMessage id="timeline.status.show_more" /> : <FormattedMessage id="timeline.status.show_less" />}
</Button> </Button>
</div> </div>

View File

@ -1,4 +1,4 @@
import { Button } from 'flowbite-react' import { Button } from '@material-tailwind/react'
import { Entity } from 'megalodon' import { Entity } from 'megalodon'
import { useState } from 'react' import { useState } from 'react'
import { FaEyeSlash } from 'react-icons/fa6' import { FaEyeSlash } from 'react-icons/fa6'
@ -16,7 +16,7 @@ export default function Media(props: Props) {
return ( return (
<div className="relative"> <div className="relative">
{sensitive ? ( {sensitive ? (
<Button size="xs" color="gray" className="focus:ring-0 my-1" onClick={() => setSensitive(false)}> <Button size="sm" onClick={() => setSensitive(false)} variant="outlined" className="my-1" color="blue-gray">
<FormattedMessage id="timeline.status.cw" /> <FormattedMessage id="timeline.status.cw" />
</Button> </Button>
) : ( ) : (

View File

@ -1,5 +1,5 @@
import { Button, Checkbox, Progress, Radio } from '@material-tailwind/react'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Progress, Button, Radio, Label, Checkbox } from 'flowbite-react'
import { Entity, MegalodonInterface } from 'megalodon' import { Entity, MegalodonInterface } from 'megalodon'
import { HTMLAttributes } from 'react' import { HTMLAttributes } from 'react'
import { FormattedMessage } from 'react-intl' import { FormattedMessage } from 'react-intl'
@ -38,12 +38,11 @@ function SimplePoll(props: Props) {
<div className={props.className + ' my-2'}> <div className={props.className + ' my-2'}>
{props.poll.options.map((option, index) => ( {props.poll.options.map((option, index) => (
<div key={index} className="flex items-center gap-2 my-2 pl-1"> <div key={index} className="flex items-center gap-2 my-2 pl-1">
<Radio id={option.title} name={props.poll.id} value={option.title} /> <Radio id={option.title} name={props.poll.id} value={option.title} label={option.title} />
<Label htmlFor={option.title}>{option.title}</Label>
</div> </div>
))} ))}
<div className="flex gap-2 items-center mt-2"> <div className="flex gap-2 items-center mt-2">
<Button color="blue" outline={true} size="xs" onClick={vote}> <Button size="sm" onClick={vote} variant="outlined">
<FormattedMessage id="timeline.status.poll.vote" /> <FormattedMessage id="timeline.status.poll.vote" />
</Button> </Button>
<div> <div>
@ -76,12 +75,11 @@ function MultiplePoll(props: Props) {
<div className={props.className + ' my-2'}> <div className={props.className + ' my-2'}>
{props.poll.options.map((option, index) => ( {props.poll.options.map((option, index) => (
<div key={index} className="flex items-center gap-2 my-2 pl-1"> <div key={index} className="flex items-center gap-2 my-2 pl-1">
<Checkbox id={option.title} /> <Checkbox id={option.title} label={option.title} />
<Label htmlFor={option.title}>{option.title}</Label>
</div> </div>
))} ))}
<div className="flex gap-2 items-center mt-2"> <div className="flex gap-2 items-center mt-2">
<Button color="blue" outline={true} size="xs" onClick={vote}> <Button size="sm" onClick={vote} variant="outlined">
<FormattedMessage id="timeline.status.poll.vote" /> <FormattedMessage id="timeline.status.poll.vote" />
</Button> </Button>
<div> <div>
@ -102,11 +100,11 @@ function PollResult(props: Props) {
<div key={index}> <div key={index}>
<span className="pr-2">{percent(option.votes_count ?? 0, props.poll.votes_count)}%</span> <span className="pr-2">{percent(option.votes_count ?? 0, props.poll.votes_count)}%</span>
<span>{option.title}</span> <span>{option.title}</span>
<Progress progress={percent(option.votes_count ?? 0, props.poll.votes_count)} /> <Progress value={percent(option.votes_count ?? 0, props.poll.votes_count)} color="blue" />
</div> </div>
))} ))}
<div className="flex gap-2 items-center mt-2"> <div className="flex gap-2 items-center mt-2">
<Button color="gray" outline={true} size="xs" onClick={props.onRefresh}> <Button size="sm" onClick={props.onRefresh} variant="outlined">
<FormattedMessage id="timeline.status.poll.refresh" /> <FormattedMessage id="timeline.status.poll.refresh" />
</Button> </Button>
<div> <div>

View File

@ -1,4 +1,3 @@
import { Avatar } from 'flowbite-react'
import { Entity, MegalodonInterface } from 'megalodon' import { Entity, MegalodonInterface } from 'megalodon'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import Body from './Body' import Body from './Body'
@ -12,6 +11,7 @@ import { useRouter } from 'next/router'
import { MouseEventHandler, useState } from 'react' import { MouseEventHandler, useState } from 'react'
import { findAccount, findLink, ParsedAccount, accountMatch, findTag } from '@/utils/statusParser' import { findAccount, findLink, ParsedAccount, accountMatch, findTag } from '@/utils/statusParser'
import { Account } from '@/db' import { Account } from '@/db'
import { Avatar } from '@material-tailwind/react'
type Props = { type Props = {
status: Entity.Status status: Entity.Status
@ -75,7 +75,12 @@ export default function Status(props: Props) {
{rebloggedHeader(props.status)} {rebloggedHeader(props.status)}
<div className="flex"> <div className="flex">
<div className="p-2 cursor-pointer" style={{ width: '56px' }}> <div className="p-2 cursor-pointer" style={{ width: '56px' }}>
<Avatar img={status.account.avatar} onClick={() => openUser(status.account.id)} /> <Avatar
src={status.account.avatar}
onClick={() => openUser(status.account.id)}
variant="rounded"
style={{ width: '40px', height: '40px' }}
/>
</div> </div>
<div className="text-gray-950 break-all overflow-hidden" style={{ width: 'calc(100% - 56px)' }}> <div className="text-gray-950 break-all overflow-hidden" style={{ width: 'calc(100% - 56px)' }}>
<div className="flex justify-between"> <div className="flex justify-between">
@ -129,7 +134,7 @@ const rebloggedHeader = (status: Entity.Status) => {
return ( return (
<div className="flex text-gray-600"> <div className="flex text-gray-600">
<div className="grid justify-items-end pr-2" style={{ width: '56px' }}> <div className="grid justify-items-end pr-2" style={{ width: '56px' }}>
<Avatar img={status.account.avatar} size="xs" /> <Avatar src={status.account.avatar} size="xs" variant="rounded" />
</div> </div>
<div style={{ width: 'calc(100% - 56px)' }}> <div style={{ width: 'calc(100% - 56px)' }}>
<FormattedMessage id="timeline.status.boosted" values={{ user: status.account.username }} /> <FormattedMessage id="timeline.status.boosted" values={{ user: status.account.username }} />

View File

@ -3,18 +3,111 @@ import '../app.css'
import AccountLayout from '@/components/layouts/account' import AccountLayout from '@/components/layouts/account'
import TimelineLayout from '@/components/layouts/timelines' import TimelineLayout from '@/components/layouts/timelines'
import { IntlProviderWrapper } from '@/utils/i18n' import { IntlProviderWrapper } from '@/utils/i18n'
import { ToastProvider } from '@/utils/toast' import { ThemeProvider } from '@material-tailwind/react'
export default function MyApp({ Component, pageProps }: AppProps) { export default function MyApp({ Component, pageProps }: AppProps) {
const customTheme = {
popover: {
styles: {
base: {
p: 'p-0'
}
}
},
input: {
styles: {
variants: {
outlined: {
base: {
input: {
floated: {
borderWidth: 'border focus:border'
}
},
label: {
before: {
floated: {
bt: 'before:border-t peer-focus:before:border-t-1',
bl: 'before:border-l peer-focus:before:border-l-1'
}
},
after: {
floated: {
bt: 'after:border-t peer-focus:after:border-t-1',
br: 'after:border-r peer-focus:after:border-r-1'
}
}
}
}
}
}
}
},
textarea: {
styles: {
variants: {
outlined: {
base: {
textarea: {
floated: {
borderWidth: 'border focus:border'
}
},
label: {
before: {
floated: {
bt: 'before:border-t peer-focus:before:border-t-1',
bl: 'before:border-l peer-focus:before:border-l-1'
}
},
after: {
floated: {
bt: 'after:border-t peer-focus:after:border-t-1',
br: 'after:border-r peer-focus:after:border-r-1'
}
}
}
}
}
}
}
},
select: {
styles: {
variants: {
outlined: {
states: {
open: {
select: {
borderWidth: 'border'
},
label: {
before: {
bt: 'before:border-t',
bl: 'before:border-l'
},
after: {
bt: 'after:border-t',
br: 'after:border-r'
}
}
}
}
}
}
}
}
}
return ( return (
<IntlProviderWrapper> <ThemeProvider value={customTheme}>
<ToastProvider> <IntlProviderWrapper>
<AccountLayout> <AccountLayout>
<TimelineLayout> <TimelineLayout>
<Component {...pageProps} /> <Component {...pageProps} />
</TimelineLayout> </TimelineLayout>
</AccountLayout> </AccountLayout>
</ToastProvider> </IntlProviderWrapper>
</IntlProviderWrapper> </ThemeProvider>
) )
} }

View File

@ -1,5 +1,5 @@
import { Spinner } from '@material-tailwind/react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { Spinner } from 'flowbite-react'
export default function Account() { export default function Account() {
const router = useRouter() const router = useRouter()
@ -14,7 +14,7 @@ export default function Account() {
return ( return (
<div className="h-screen w-full flex justify-center items-center"> <div className="h-screen w-full flex justify-center items-center">
<Spinner color="info" /> <Spinner />
</div> </div>
) )
} }

View File

@ -1,7 +1,7 @@
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useEffect } from 'react' import { useEffect } from 'react'
import { db } from '@/db' import { db } from '@/db'
import { Spinner } from 'flowbite-react' import { Spinner } from '@material-tailwind/react'
export default function Index() { export default function Index() {
const router = useRouter() const router = useRouter()
@ -27,7 +27,7 @@ export default function Index() {
return ( return (
<div className="h-screen w-full flex justify-center items-center"> <div className="h-screen w-full flex justify-center items-center">
<Spinner color="info" /> <Spinner />
</div> </div>
) )
} }

View File

@ -1,25 +1,15 @@
module.exports = { const withMT = require('@material-tailwind/react/utils/withMT')
content: ['./node_modules/flowbite-react/**/*.js', './renderer/pages/**/*.{js,ts,jsx,tsx}', './renderer/components/**/*.{js,ts,jsx,tsx}'],
plugins: [require('flowbite/plugin')], module.exports = withMT({
darkMode: 'class', content: ['./renderer/pages/**/*.{js,ts,jsx,tsx}', './renderer/components/**/*.{js,ts,jsx,tsx}'],
theme: { theme: {
extend: { extend: {
colors: { colors: {
// flowbite-svelte blue: {
// Refs: https://github.com/themesberg/flowbite-svelte/blob/main/tailwind.config.cjs 950: '#0B2A68'
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a'
} }
} }
} }
} },
} plugins: []
})

View File

@ -1,4 +1,4 @@
import { Alert } from 'flowbite-react' import { Alert } from '@material-tailwind/react'
import React, { useState, createContext, useContext } from 'react' import React, { useState, createContext, useContext } from 'react'
type ToastTypes = 'info' | 'success' | 'failure' | 'warning' type ToastTypes = 'info' | 'success' | 'failure' | 'warning'
@ -28,11 +28,24 @@ export const ToastProvider: React.FC<Props> = ({ children }) => {
}, 10000) }, 10000)
} }
const color = (toastType: ToastTypes) => {
switch (toastType) {
case 'success':
return 'green'
case 'failure':
return 'red'
case 'warning':
return 'amber'
default:
return 'blue'
}
}
return ( return (
<ToastContext.Provider value={showToast}> <ToastContext.Provider value={showToast}>
{children} {children}
<div className={`${showable ? 'block' : 'hidden'} fixed top-2 -translate-x-1/2`} style={{ left: '50%' }}> <div className={`${showable ? 'block' : 'hidden'} fixed top-2 -translate-x-1/2`} style={{ left: '50%' }}>
<Alert color={toastType} className="w96"> <Alert color={color(toastType)} className="w96">
<span>{toastText}</span> <span>{toastText}</span>
</Alert> </Alert>
</div> </div>

4107
yarn.lock

File diff suppressed because it is too large Load Diff