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:
parent
7c9708906e
commit
b49c1e01a9
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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) }} />
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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'
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
@ -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>
|
||||||
|
@ -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 }} />
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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: []
|
||||||
|
})
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user