Compare commits
16 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 600f8dcfa6 | |||
| f6d0a0b795 | |||
| 62e17e429d | |||
| 03568357ba | |||
| 2bc8415c51 | |||
| 161c256f06 | |||
| fc0a4b1165 | |||
| 516b34752f | |||
| 5a3cebf9ff | |||
| ce57014677 | |||
| 9c1208dd38 | |||
| 68cc6d7167 | |||
| 0e5960f6d5 | |||
| 9adcd50519 | |||
| 955cd14762 | |||
| 15a32d56bb |
36 changed files with 2321 additions and 125 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -49,7 +49,7 @@ dist/
|
|||
# Instructions will be provided to download these from the game
|
||||
public/images/weapon*
|
||||
public/images/summon*
|
||||
public/images/chara*
|
||||
public/images/character*
|
||||
public/images/job*
|
||||
public/images/awakening*
|
||||
public/images/ax*
|
||||
|
|
|
|||
|
|
@ -54,9 +54,9 @@ root
|
|||
├─ accessory-square/
|
||||
├─ awakening/
|
||||
├─ ax/
|
||||
├─ chara-main/
|
||||
├─ chara-grid/
|
||||
├─ chara-square/
|
||||
├─ character-main/
|
||||
├─ character-grid/
|
||||
├─ character-square/
|
||||
├─ guidebooks/
|
||||
├─ jobs/
|
||||
├─ job-icons/
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ const HovercardHeader = ({ gridObject, object, type, ...props }: Props) => {
|
|||
else if (gridCharacter.uncap_level == 5) suffix = '03'
|
||||
else if (gridCharacter.uncap_level > 2) suffix = '02'
|
||||
|
||||
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_${suffix}.jpg`
|
||||
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/character-grid/${character.granblue_id}_${suffix}.jpg`
|
||||
}
|
||||
|
||||
const summonImage = () => {
|
||||
|
|
|
|||
56
components/MentionList/index.module.scss
Normal file
56
components/MentionList/index.module.scss
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
.items {
|
||||
background: #fff;
|
||||
border-radius: $item-corner;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0px 10px 20px rgba(0, 0, 0, 0.1);
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
overflow: hidden;
|
||||
padding: $unit-half;
|
||||
pointer-events: all;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item {
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: $item-corner-small;
|
||||
color: var(--text-tertiary);
|
||||
font-size: $font-small;
|
||||
font-weight: $medium;
|
||||
display: flex;
|
||||
gap: $unit;
|
||||
margin: 0;
|
||||
padding: $unit-half $unit;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
|
||||
&:hover,
|
||||
&.selected {
|
||||
background: var(--menu-bg-item-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.job {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: $unit-4x;
|
||||
height: $unit-4x;
|
||||
|
||||
img {
|
||||
width: $unit-3x;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: $item-corner-small;
|
||||
width: $unit-4x;
|
||||
height: $unit-4x;
|
||||
}
|
||||
}
|
||||
|
||||
.noResult {
|
||||
padding: $unit;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
124
components/MentionList/index.tsx
Normal file
124
components/MentionList/index.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import React, {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { SuggestionProps } from '@tiptap/suggestion'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import styles from './index.module.scss'
|
||||
|
||||
type Props = Pick<SuggestionProps, 'items' | 'command'>
|
||||
|
||||
export type MentionRef = {
|
||||
onKeyDown: (props: { event: KeyboardEvent }) => boolean
|
||||
}
|
||||
|
||||
export type MentionSuggestion = {
|
||||
granblue_id: string
|
||||
name: {
|
||||
[key: string]: string
|
||||
en: string
|
||||
ja: string
|
||||
}
|
||||
type: string
|
||||
element: number
|
||||
}
|
||||
|
||||
interface MentionProps extends SuggestionProps {
|
||||
items: MentionSuggestion[]
|
||||
}
|
||||
|
||||
export const MentionList = forwardRef<MentionRef, Props>(
|
||||
({ items, ...props }: Props, forwardedRef) => {
|
||||
const router = useRouter()
|
||||
const locale = router.locale || 'en'
|
||||
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
|
||||
const selectItem = (index: number) => {
|
||||
const item = items[index]
|
||||
|
||||
if (item) {
|
||||
props.command({ id: item })
|
||||
}
|
||||
}
|
||||
|
||||
const upHandler = () => {
|
||||
setSelectedIndex((selectedIndex + items.length - 1) % items.length)
|
||||
}
|
||||
|
||||
const downHandler = () => {
|
||||
setSelectedIndex((selectedIndex + 1) % items.length)
|
||||
}
|
||||
|
||||
const enterHandler = () => {
|
||||
selectItem(selectedIndex)
|
||||
}
|
||||
|
||||
useEffect(() => setSelectedIndex(0), [items])
|
||||
|
||||
useImperativeHandle(forwardedRef, () => ({
|
||||
onKeyDown: ({ event }) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
upHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
downHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
enterHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className={styles.items}>
|
||||
{items.length ? (
|
||||
items.map((item, index) => (
|
||||
<button
|
||||
className={classNames({
|
||||
[styles.item]: true,
|
||||
[styles.selected]: index === selectedIndex,
|
||||
})}
|
||||
key={index}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
<div className={styles[item.type]}>
|
||||
<img
|
||||
alt={item.name[locale]}
|
||||
src={
|
||||
item.type === 'character'
|
||||
? `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/${item.type}-square/${item.granblue_id}_01.jpg`
|
||||
: item.type === 'job'
|
||||
? `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/job-icons/${item.granblue_id}.png`
|
||||
: `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/${item.type}-square/${item.granblue_id}.jpg`
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span>{item.name[locale]}</span>
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className={styles.noResult}>
|
||||
{t('search.errors.no_results_generic')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
MentionList.displayName = 'MentionList'
|
||||
|
|
@ -68,7 +68,7 @@ const ChangelogUnit = ({ id, type, image }: Props) => {
|
|||
|
||||
switch (type) {
|
||||
case 'character':
|
||||
src = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${id}_${image}.jpg`
|
||||
src = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/character-grid/${id}_${image}.jpg`
|
||||
break
|
||||
case 'weapon':
|
||||
src = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${id}.jpg`
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ const CharacterConflictModal = (props: Props) => {
|
|||
suffix = `${suffix}_0${element}`
|
||||
}
|
||||
|
||||
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-square/${character?.granblue_id}_${suffix}.jpg`
|
||||
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/character-square/${character?.granblue_id}_${suffix}.jpg`
|
||||
}
|
||||
|
||||
function openChange(open: boolean) {
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ const CharacterModal = ({
|
|||
title={gridCharacter.object.name[locale]}
|
||||
subtitle={t('modals.characters.title')}
|
||||
image={{
|
||||
src: `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-square/${gridCharacter.object.granblue_id}_01.jpg`,
|
||||
src: `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/character-square/${gridCharacter.object.granblue_id}_01.jpg`,
|
||||
alt: gridCharacter.object.name[locale],
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ const CharacterResult = (props: Props) => {
|
|||
const character = props.data
|
||||
|
||||
const characterUrl = () => {
|
||||
let url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_01.jpg`
|
||||
let url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/character-grid/${character.granblue_id}_01.jpg`
|
||||
|
||||
if (character.granblue_id === '3030182000') {
|
||||
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_01_01.jpg`
|
||||
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/character-grid/${character.granblue_id}_01_01.jpg`
|
||||
}
|
||||
|
||||
return url
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ const CharacterUnit = ({
|
|||
suffix = `${suffix}_0${element}`
|
||||
}
|
||||
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-main/${character.granblue_id}_${suffix}.jpg`
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/character-main/${character.granblue_id}_${suffix}.jpg`
|
||||
}
|
||||
|
||||
setImageUrl(imgSrc)
|
||||
|
|
|
|||
252
components/common/Editor/index.module.scss
Normal file
252
components/common/Editor/index.module.scss
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
.wrapper {
|
||||
border-radius: $input-corner;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
|
||||
&.bound {
|
||||
background-color: var(--input-bg);
|
||||
height: 350px; // Temporary
|
||||
}
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
box-sizing: border-box;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
font-size: $font-regular;
|
||||
line-height: 1.4;
|
||||
overflow: scroll;
|
||||
padding: $unit * 1.5 $unit-2x;
|
||||
white-space: pre-wrap;
|
||||
width: 100%;
|
||||
|
||||
&:focus {
|
||||
// border: 2px solid $blue;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&.bound {
|
||||
background-color: var(--input-bound-bg);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--input-bound-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&.editParty {
|
||||
border-bottom-left-radius: $input-corner;
|
||||
border-bottom-right-radius: $input-corner;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0 $unit-2x;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
ol {
|
||||
padding: 0 $unit-2x;
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: $font-xlarge;
|
||||
font-weight: $medium;
|
||||
margin: $unit 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $font-large;
|
||||
font-weight: $medium;
|
||||
margin: $unit 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: $font-regular;
|
||||
font-weight: $medium;
|
||||
margin: $unit 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
mark {
|
||||
border-radius: $item-corner-small;
|
||||
background: var(--highlight-bg);
|
||||
color: var(--highlight-text);
|
||||
padding: 1px $unit-fourth;
|
||||
}
|
||||
|
||||
iframe {
|
||||
background: var(--input-bound-bg);
|
||||
border-radius: $card-corner;
|
||||
min-width: 200px;
|
||||
min-height: 200px;
|
||||
display: block;
|
||||
outline: 0px solid transparent;
|
||||
margin: $unit 0;
|
||||
|
||||
&:hover {
|
||||
background: var(--input-bound-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.mention {
|
||||
border-radius: $item-corner-small;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
||||
0 1px 0px rgba(0, 0, 0, 0.25);
|
||||
background: var(--card-bg);
|
||||
color: var(--text-primary);
|
||||
font-weight: $medium;
|
||||
font-size: 15px;
|
||||
padding: 1px $unit-half;
|
||||
|
||||
&:hover {
|
||||
background: var(--card-bg-hover);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&[data-element='fire'] {
|
||||
background: var(--fire-bg);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
||||
0 1px 0px var(--fire-shadow);
|
||||
color: var(--fire-text);
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
||||
0 1px 0px var(--fire-shadow-hover);
|
||||
color: var(--fire-text-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-element='water'] {
|
||||
background: var(--water-bg);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
||||
0 1px 0px var(--water-shadow);
|
||||
color: var(--water-text);
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
||||
0 1px 0px var(--water-shadow-hover);
|
||||
color: var(--water-text-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-element='earth'] {
|
||||
background: var(--earth-bg);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
||||
0 1px 0px var(--earth-shadow);
|
||||
color: var(--earth-text);
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
||||
0 1px 0px var(--earth-shadow-hover);
|
||||
color: var(--earth-text-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-element='wind'] {
|
||||
background: var(--wind-bg);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
||||
0 1px 0px var(--wind-shadow);
|
||||
color: var(--wind-text);
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
||||
0 1px 0px var(--wind-shadow-hover);
|
||||
color: var(--wind-text-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-element='dark'] {
|
||||
background: var(--dark-bg);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
||||
0 1px 0px var(--dark-shadow);
|
||||
color: var(--dark-text);
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
||||
0 1px 0px var(--dark-shadow-hover);
|
||||
color: var(--dark-text-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-element='light'] {
|
||||
background: var(--light-bg);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
||||
0 1px 0px var(--light-shadow);
|
||||
color: var(--light-text);
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
||||
0 1px 0px var(--light-shadow-hover);
|
||||
color: var(--light-text-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
background: var(--toolbar-bg);
|
||||
position: sticky;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: $unit-half;
|
||||
padding: $unit;
|
||||
z-index: 10;
|
||||
|
||||
.divider {
|
||||
background: var(--toolbar-divider-bg);
|
||||
border-radius: $full-corner;
|
||||
height: calc($unit-2x + $unit-half);
|
||||
width: $unit-fourth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
background: var(--formatting-menu-bg);
|
||||
border-radius: $bubble-menu-corner;
|
||||
padding: $unit-half;
|
||||
|
||||
button {
|
||||
background: var(--formatting-menu-item-bg);
|
||||
border-radius: $bubble-menu-item-corner;
|
||||
color: var(--formatting-menu-item-text);
|
||||
font-weight: $medium;
|
||||
font-size: $font-small;
|
||||
|
||||
&:hover {
|
||||
background: var(--formatting-menu-item-bg-hover);
|
||||
color: var(--formatting-menu-item-text-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--formatting-menu-item-bg-active);
|
||||
color: var(--formatting-menu-item-text-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
261
components/common/Editor/index.tsx
Normal file
261
components/common/Editor/index.tsx
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
import { ComponentProps, useCallback, useEffect } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEditor, EditorContent } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Link from '@tiptap/extension-link'
|
||||
import Highlight from '@tiptap/extension-highlight'
|
||||
import Typography from '@tiptap/extension-typography'
|
||||
import Youtube from '@tiptap/extension-youtube'
|
||||
import CustomMention from '~extensions/CustomMention'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { mentionSuggestionOptions } from '~utils/mentionSuggestions'
|
||||
import type { JSONContent } from '@tiptap/core'
|
||||
import ToolbarButton from '~components/common/ToolbarButton'
|
||||
|
||||
import BoldIcon from 'remixicon-react/BoldIcon'
|
||||
import ItalicIcon from 'remixicon-react/ItalicIcon'
|
||||
import StrikethroughIcon from 'remixicon-react/StrikethroughIcon'
|
||||
import UnorderedListIcon from 'remixicon-react/ListUnorderedIcon'
|
||||
import OrderedListIcon from '~public/icons/remix/list-ordered-2.svg'
|
||||
import PaintbrushIcon from 'remixicon-react/PaintbrushLineIcon'
|
||||
import H1Icon from 'remixicon-react/H1Icon'
|
||||
import H2Icon from 'remixicon-react/H2Icon'
|
||||
import H3Icon from 'remixicon-react/H3Icon'
|
||||
import LinkIcon from 'remixicon-react/LinkIcon'
|
||||
import YoutubeIcon from 'remixicon-react/YoutubeLineIcon'
|
||||
import styles from './index.module.scss'
|
||||
|
||||
interface Props extends ComponentProps<'div'> {
|
||||
bound: boolean
|
||||
editable?: boolean
|
||||
content?: string
|
||||
onUpdate?: (content: JSONContent) => void
|
||||
}
|
||||
|
||||
const Editor = ({
|
||||
bound,
|
||||
className,
|
||||
content,
|
||||
editable,
|
||||
onUpdate,
|
||||
...props
|
||||
}: Props) => {
|
||||
// Hooks: Router
|
||||
const router = useRouter()
|
||||
const locale = router.locale || 'en'
|
||||
|
||||
useEffect(() => {
|
||||
editor?.commands.setContent(formatContent(content))
|
||||
}, [content])
|
||||
|
||||
// Setup: Editor
|
||||
const editor = useEditor({
|
||||
content: formatContent(content),
|
||||
editable: editable,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: classNames(
|
||||
{
|
||||
[styles.editor]: true,
|
||||
[styles.bound]: bound,
|
||||
},
|
||||
className?.split(' ').map((c) => styles[c])
|
||||
),
|
||||
},
|
||||
},
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
heading: {
|
||||
levels: [1, 2, 3],
|
||||
},
|
||||
}),
|
||||
Link,
|
||||
Highlight,
|
||||
Typography,
|
||||
CustomMention.configure({
|
||||
renderLabel({ options, node }) {
|
||||
return `${node.attrs.id.name[locale] ?? node.attrs.id.granblue_en}`
|
||||
},
|
||||
suggestion: mentionSuggestionOptions,
|
||||
HTMLAttributes: {
|
||||
class: classNames({
|
||||
[styles.mention]: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
Youtube.configure({
|
||||
inline: false,
|
||||
modestBranding: true,
|
||||
interfaceLanguage: locale,
|
||||
}),
|
||||
],
|
||||
onUpdate: ({ editor }) => {
|
||||
const json = editor.getJSON()
|
||||
if (onUpdate) onUpdate(json)
|
||||
},
|
||||
})
|
||||
|
||||
// Methods: Convenience
|
||||
function isJSON(content?: string) {
|
||||
if (!content) return false
|
||||
|
||||
try {
|
||||
JSON.parse(content)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function formatContent(content?: string) {
|
||||
if (!content) return ''
|
||||
if (isJSON(content)) return JSON.parse(content)
|
||||
else {
|
||||
// Otherwise, create a new <p> tag after each double newline.
|
||||
// Add < br /> tags for single newlines.
|
||||
// Add a < br /> after each paragraph.
|
||||
const paragraphs = content.split('\n\n')
|
||||
const formatted = paragraphs
|
||||
.map((p) => {
|
||||
const lines = p.split('\n')
|
||||
return lines.join('<br />')
|
||||
})
|
||||
.join('</p><br /><p>')
|
||||
return formatted
|
||||
}
|
||||
}
|
||||
|
||||
// Methods: Actions
|
||||
const setLink = useCallback(() => {
|
||||
const previousUrl = editor?.getAttributes('link').href
|
||||
const url = window.prompt('URL', previousUrl)
|
||||
|
||||
// cancelled
|
||||
if (url === null) {
|
||||
return
|
||||
}
|
||||
|
||||
// empty
|
||||
if (url === '') {
|
||||
editor?.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// update link
|
||||
editor?.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
||||
}, [editor])
|
||||
|
||||
const addYoutubeVideo = () => {
|
||||
const url = prompt('Enter YouTube URL')
|
||||
|
||||
if (editor && url) {
|
||||
editor.commands.setYoutubeVideo({
|
||||
src: url,
|
||||
width: 320,
|
||||
height: 180,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Methods: Rendering
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={styles.wrapper}>
|
||||
{editor && editable === true && (
|
||||
<nav className={styles.toolbar}>
|
||||
<ToolbarButton
|
||||
editor={editor}
|
||||
action="bold"
|
||||
icon={<BoldIcon />}
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
/>
|
||||
<ToolbarButton
|
||||
editor={editor}
|
||||
action="italic"
|
||||
icon={<ItalicIcon />}
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
/>
|
||||
<ToolbarButton
|
||||
editor={editor}
|
||||
action="strike"
|
||||
icon={<StrikethroughIcon />}
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
/>
|
||||
<ToolbarButton
|
||||
editor={editor}
|
||||
action="highlight"
|
||||
icon={<PaintbrushIcon />}
|
||||
onClick={() => editor.chain().focus().toggleHighlight().run()}
|
||||
/>
|
||||
<div className={styles.divider} />
|
||||
<ToolbarButton
|
||||
editor={editor}
|
||||
action="heading"
|
||||
level={1}
|
||||
icon={<H1Icon />}
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleHeading({ level: 1 }).run()
|
||||
}
|
||||
/>
|
||||
<ToolbarButton
|
||||
editor={editor}
|
||||
action="heading"
|
||||
level={2}
|
||||
icon={<H2Icon />}
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleHeading({ level: 2 }).run()
|
||||
}
|
||||
/>
|
||||
<ToolbarButton
|
||||
editor={editor}
|
||||
action="heading"
|
||||
level={3}
|
||||
icon={<H3Icon />}
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleHeading({ level: 3 }).run()
|
||||
}
|
||||
/>
|
||||
<div className={styles.divider} />
|
||||
<ToolbarButton
|
||||
editor={editor}
|
||||
action="bulletList"
|
||||
icon={<UnorderedListIcon />}
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
/>
|
||||
<ToolbarButton
|
||||
editor={editor}
|
||||
action="orderedList"
|
||||
icon={<OrderedListIcon />}
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
/>
|
||||
<div className={styles.divider} />
|
||||
<ToolbarButton
|
||||
editor={editor}
|
||||
action="link"
|
||||
icon={<LinkIcon />}
|
||||
onClick={setLink}
|
||||
/>
|
||||
<ToolbarButton
|
||||
editor={editor}
|
||||
action="youtube"
|
||||
icon={<YoutubeIcon />}
|
||||
onClick={addYoutubeVideo}
|
||||
/>
|
||||
</nav>
|
||||
)}
|
||||
<EditorContent editor={editor} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
Editor.defaultProps = {
|
||||
bound: false,
|
||||
editable: false,
|
||||
}
|
||||
|
||||
export default Editor
|
||||
|
|
@ -14,7 +14,7 @@ interface Props
|
|||
imageAlt?: string
|
||||
imageClass?: string
|
||||
imageSrc?: string[]
|
||||
onValueChange: (value?: string) => void
|
||||
onValueChange: (value?: string | number | readonly string[]) => void
|
||||
}
|
||||
|
||||
const InputTableField = ({
|
||||
|
|
@ -25,10 +25,12 @@ const InputTableField = ({
|
|||
imageSrc,
|
||||
...props
|
||||
}: Props) => {
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [inputValue, setInputValue] = useState<
|
||||
string | number | readonly string[]
|
||||
>()
|
||||
|
||||
useEffect(() => {
|
||||
if (props.value) setInputValue(`${props.value}`)
|
||||
if (props.value !== undefined) setInputValue(props.value)
|
||||
}, [props.value])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -54,7 +56,7 @@ const InputTableField = ({
|
|||
<Input
|
||||
className={props.className}
|
||||
placeholder={props.placeholder}
|
||||
value={inputValue ? `${inputValue}` : ''}
|
||||
value={inputValue !== undefined ? inputValue : ''}
|
||||
step={1}
|
||||
tabIndex={props.tabIndex}
|
||||
bound={true}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
font-size: $font-tiny;
|
||||
font-weight: $bold;
|
||||
line-height: 1.4;
|
||||
min-width: 3rem;
|
||||
text-align: center;
|
||||
padding: $unit-three-fourth ($unit * 1.5);
|
||||
user-select: none;
|
||||
|
|
|
|||
36
components/common/ToolbarButton/index.module.scss
Normal file
36
components/common/ToolbarButton/index.module.scss
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
.button {
|
||||
background: var(--toolbar-item-bg);
|
||||
border-radius: $bubble-menu-item-corner;
|
||||
color: var(--toolbar-item-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: $medium;
|
||||
font-size: $font-small;
|
||||
padding: $unit-half;
|
||||
|
||||
&:hover {
|
||||
background: var(--toolbar-item-bg-hover);
|
||||
color: var(--toolbar-item-text-hover);
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
fill: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--toolbar-item-bg-active);
|
||||
color: var(--toolbar-item-text-active);
|
||||
|
||||
svg {
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--text-tertiary);
|
||||
height: $unit-2x;
|
||||
width: $unit-2x;
|
||||
}
|
||||
}
|
||||
35
components/common/ToolbarButton/index.tsx
Normal file
35
components/common/ToolbarButton/index.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { useTranslation } from 'next-i18next'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { Editor } from '@tiptap/react'
|
||||
import Tooltip from '~components/common/Tooltip'
|
||||
|
||||
import styles from './index.module.scss'
|
||||
|
||||
interface Props {
|
||||
editor: Editor
|
||||
action: string
|
||||
level?: number
|
||||
icon: React.ReactNode
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const ToolbarIcon = ({ editor, action, level, icon, onClick }: Props) => {
|
||||
const { t } = useTranslation('common')
|
||||
const classes = classNames({
|
||||
[styles.button]: true,
|
||||
[styles.active]: level
|
||||
? editor.isActive(action, { level: level })
|
||||
: editor.isActive(action),
|
||||
})
|
||||
|
||||
return (
|
||||
<Tooltip content={t(`toolbar.tooltips.${action}`)}>
|
||||
<button onClick={onClick} className={classes}>
|
||||
{icon}
|
||||
</button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default ToolbarIcon
|
||||
|
|
@ -199,14 +199,18 @@ const FilterModal = (props: Props) => {
|
|||
setMinWeaponCount(value)
|
||||
}
|
||||
|
||||
function handleMaxButtonsCountValueChange(value?: string) {
|
||||
function handleMaxButtonsCountValueChange(
|
||||
value?: string | number | readonly string[]
|
||||
) {
|
||||
if (!value) return
|
||||
setMaxButtonsCount(parseInt(value))
|
||||
setMaxButtonsCount(value as number)
|
||||
}
|
||||
|
||||
function handleMaxTurnsCountValueChange(value?: string) {
|
||||
function handleMaxTurnsCountValueChange(
|
||||
value?: string | number | readonly string[]
|
||||
) {
|
||||
if (!value) return
|
||||
setMaxTurnsCount(parseInt(value))
|
||||
setMaxTurnsCount(value as number)
|
||||
}
|
||||
|
||||
function handleNameQualityValueChange(value?: boolean) {
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ import classNames from 'classnames'
|
|||
import debounce from 'lodash.debounce'
|
||||
|
||||
import Alert from '~components/common/Alert'
|
||||
import Button from '~components/common/Button'
|
||||
import { Dialog, DialogTrigger } from '~components/common/Dialog'
|
||||
import DialogHeader from '~components/common/DialogHeader'
|
||||
import DialogFooter from '~components/common/DialogFooter'
|
||||
import DialogContent from '~components/common/DialogContent'
|
||||
import Button from '~components/common/Button'
|
||||
import DurationInput from '~components/common/DurationInput'
|
||||
import Editor from '~components/common/Editor'
|
||||
import Input from '~components/common/Input'
|
||||
import InputTableField from '~components/common/InputTableField'
|
||||
import RaidCombobox from '~components/raids/RaidCombobox'
|
||||
|
|
@ -24,6 +25,7 @@ import Textarea from '~components/common/Textarea'
|
|||
import capitalizeFirstLetter from '~utils/capitalizeFirstLetter'
|
||||
import type { DetailsObject } from 'types'
|
||||
import type { DialogProps } from '@radix-ui/react-dialog'
|
||||
import type { JSONContent } from '@tiptap/core'
|
||||
|
||||
import { appState } from '~utils/appState'
|
||||
|
||||
|
|
@ -129,6 +131,10 @@ const EditPartyModal = ({
|
|||
setErrors(newErrors)
|
||||
}
|
||||
|
||||
function handleEditorUpdate(content: JSONContent) {
|
||||
setDescription(JSON.stringify(content))
|
||||
}
|
||||
|
||||
function handleChargeAttackChanged(checked: boolean) {
|
||||
setChargeAttack(checked)
|
||||
}
|
||||
|
|
@ -153,22 +159,23 @@ const EditPartyModal = ({
|
|||
if (!isNaN(value)) setClearTime(value)
|
||||
}
|
||||
|
||||
function handleTurnCountChanged(value?: string) {
|
||||
if (!value) return
|
||||
const numericalValue = parseInt(value)
|
||||
if (!isNaN(numericalValue)) setTurnCount(numericalValue)
|
||||
function handleTurnCountChanged(value?: string | number | readonly string[]) {
|
||||
if (value === null || value === undefined) return
|
||||
setTurnCount(value as number)
|
||||
}
|
||||
|
||||
function handleButtonCountChanged(value?: string) {
|
||||
if (!value) return
|
||||
const numericalValue = parseInt(value)
|
||||
if (!isNaN(numericalValue)) setButtonCount(numericalValue)
|
||||
function handleButtonCountChanged(
|
||||
value?: string | number | readonly string[]
|
||||
) {
|
||||
if (value === null || value === undefined) return
|
||||
setButtonCount(value as number)
|
||||
}
|
||||
|
||||
function handleChainCountChanged(value?: string) {
|
||||
if (!value) return
|
||||
const numericalValue = parseInt(value)
|
||||
if (!isNaN(numericalValue)) setChainCount(numericalValue)
|
||||
function handleChainCountChanged(
|
||||
value?: string | number | readonly string[]
|
||||
) {
|
||||
if (value === null || value === undefined) return
|
||||
setChainCount(value as number)
|
||||
}
|
||||
|
||||
function handleTextAreaChanged(event: React.ChangeEvent<HTMLDivElement>) {
|
||||
|
|
@ -291,7 +298,6 @@ const EditPartyModal = ({
|
|||
function hasBeenModified() {
|
||||
const nameChanged =
|
||||
name !== party.name && !(name === '' && party.name === undefined)
|
||||
|
||||
const descriptionChanged =
|
||||
description !== party.description &&
|
||||
!(description === '' && party.description === undefined)
|
||||
|
|
@ -306,6 +312,21 @@ const EditPartyModal = ({
|
|||
const buttonCountChanged = buttonCount !== party.buttonCount
|
||||
const chainCountChanged = chainCount !== party.chainCount
|
||||
|
||||
// Debugging for if you need to check if a value is being changed
|
||||
// console.log(`
|
||||
// nameChanged: ${nameChanged}\n
|
||||
// descriptionChanged: ${descriptionChanged}\n
|
||||
// raidChanged: ${raidChanged}\n
|
||||
// chargeAttackChanged: ${chargeAttackChanged}\n
|
||||
// fullAutoChanged: ${fullAutoChanged}\n
|
||||
// autoGuardChanged: ${autoGuardChanged}\n
|
||||
// autoSummonChanged: ${autoSummonChanged}\n
|
||||
// clearTimeChanged: ${clearTimeChanged}\n
|
||||
// turnCountChanged: ${turnCountChanged}\n
|
||||
// buttonCountChanged: ${buttonCountChanged}\n
|
||||
// chainCountChanged: ${chainCountChanged}\n
|
||||
// `)
|
||||
|
||||
return (
|
||||
nameChanged ||
|
||||
descriptionChanged ||
|
||||
|
|
@ -332,13 +353,12 @@ const EditPartyModal = ({
|
|||
setFullAuto(party.fullAuto)
|
||||
setChargeAttack(party.chargeAttack)
|
||||
setClearTime(party.clearTime)
|
||||
if (party.turnCount) setTurnCount(party.turnCount)
|
||||
if (party.buttonCount) setButtonCount(party.buttonCount)
|
||||
if (party.chainCount) setChainCount(party.chainCount)
|
||||
if (party.turnCount !== undefined) setTurnCount(party.turnCount)
|
||||
if (party.buttonCount !== undefined) setButtonCount(party.buttonCount)
|
||||
if (party.chainCount !== undefined) setChainCount(party.chainCount)
|
||||
}
|
||||
|
||||
async function updateDetails(event: React.MouseEvent) {
|
||||
const descriptionValue = descriptionInput.current?.innerHTML
|
||||
const details: DetailsObject = {
|
||||
fullAuto: fullAuto,
|
||||
autoGuard: autoGuard,
|
||||
|
|
@ -349,7 +369,7 @@ const EditPartyModal = ({
|
|||
turnCount: turnCount,
|
||||
chainCount: chainCount,
|
||||
name: name,
|
||||
description: descriptionValue,
|
||||
description: description,
|
||||
raid: raid,
|
||||
extra: extra,
|
||||
}
|
||||
|
|
@ -446,14 +466,13 @@ const EditPartyModal = ({
|
|||
}
|
||||
}
|
||||
|
||||
const descriptionField = (
|
||||
<Textarea
|
||||
className="editParty"
|
||||
const editorField = (
|
||||
<Editor
|
||||
bound={true}
|
||||
placeholder={t('modals.edit_team.placeholders.description')}
|
||||
value={description}
|
||||
onInput={handleTextAreaChanged}
|
||||
ref={descriptionInput}
|
||||
content={props.party?.description}
|
||||
editable={true}
|
||||
key={props.party?.shortcode}
|
||||
onUpdate={handleEditorUpdate}
|
||||
/>
|
||||
)
|
||||
|
||||
|
|
@ -560,7 +579,7 @@ const EditPartyModal = ({
|
|||
{nameField}
|
||||
{raidField}
|
||||
{extraNotice()}
|
||||
{descriptionField}
|
||||
{editorField}
|
||||
</>
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ const Party = (props: Props) => {
|
|||
|
||||
// Set up states
|
||||
const { party } = useSnapshot(appState)
|
||||
const [updatedParty, setUpdatedParty] = useState<Party | undefined>()
|
||||
const [editable, setEditable] = useState(false)
|
||||
const [refresh, setRefresh] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
|
|
@ -57,7 +58,10 @@ const Party = (props: Props) => {
|
|||
useEffect(() => {
|
||||
const resetState = clonedeep(initialAppState)
|
||||
appState.grid = resetState.grid
|
||||
if (props.team) storeParty(props.team)
|
||||
if (props.team) {
|
||||
storeParty(props.team)
|
||||
setUpdatedParty(props.team)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Subscribe to app state to listen for account changes and
|
||||
|
|
@ -108,9 +112,11 @@ const Party = (props: Props) => {
|
|||
let payload = {}
|
||||
if (details) payload = formatDetailsObject(details)
|
||||
|
||||
return await api.endpoints.parties
|
||||
.create(payload)
|
||||
.then((response) => storeParty(response.data.party))
|
||||
return await api.endpoints.parties.create(payload).then((response) => {
|
||||
storeParty(response.data.party)
|
||||
setUpdatedParty(response.data.party)
|
||||
return Promise.resolve(response.data.party)
|
||||
})
|
||||
}
|
||||
|
||||
async function updateParty(details: DetailsObject) {
|
||||
|
|
@ -119,7 +125,11 @@ const Party = (props: Props) => {
|
|||
if (props.team && props.team.id) {
|
||||
return await api.endpoints.parties
|
||||
.update(props.team.id, payload)
|
||||
.then((response) => storeParty(response.data.party))
|
||||
.then((response) => {
|
||||
storeParty(response.data.party)
|
||||
setUpdatedParty(response.data.party)
|
||||
return Promise.resolve(response.data.party)
|
||||
})
|
||||
.catch((error) => {
|
||||
const data = error.response.data
|
||||
if (data.errors && Object.keys(data.errors).includes('guidebooks')) {
|
||||
|
|
@ -438,7 +448,7 @@ const Party = (props: Props) => {
|
|||
{errorAlert()}
|
||||
|
||||
<PartyHeader
|
||||
party={props.team}
|
||||
party={updatedParty}
|
||||
new={props.new || false}
|
||||
editable={props.new ? true : party.editable}
|
||||
raidGroups={props.raidGroups}
|
||||
|
|
@ -452,7 +462,7 @@ const Party = (props: Props) => {
|
|||
<section id="Party">{currentGrid()}</section>
|
||||
|
||||
<PartyFooter
|
||||
party={props.team}
|
||||
party={updatedParty}
|
||||
new={props.new || false}
|
||||
editable={party.editable}
|
||||
raidGroups={props.raidGroups}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-2x;
|
||||
margin: $unit-4x auto 0 auto;
|
||||
margin: $unit-4x auto $unit-10x auto;
|
||||
max-width: $grid-width;
|
||||
|
||||
@include breakpoint(phone) {
|
||||
|
|
@ -22,8 +22,7 @@
|
|||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
margin: 0 auto $unit-2x;
|
||||
margin-bottom: $unit-12x;
|
||||
min-height: 10vh;
|
||||
min-height: 20vh;
|
||||
max-width: $unit * 94;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
|
@ -32,22 +31,6 @@
|
|||
padding: 0 $unit;
|
||||
}
|
||||
|
||||
.noRemixes,
|
||||
.noDescription {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-2x;
|
||||
margin: 0 auto;
|
||||
padding: $unit-4x 0;
|
||||
|
||||
h3 {
|
||||
font-size: $font-small;
|
||||
font-weight: $medium;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $font-regular;
|
||||
line-height: $font-regular * 1.2;
|
||||
|
|
@ -147,6 +130,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
.noRemixes,
|
||||
.noDescription {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 20vh;
|
||||
gap: $unit-2x;
|
||||
margin: 0 auto;
|
||||
padding: $unit-4x 0;
|
||||
|
||||
h3 {
|
||||
font-size: $font-small;
|
||||
font-weight: $medium;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.PartyInfo {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -2,12 +2,8 @@ import React, { useEffect, useState } from 'react'
|
|||
import { useRouter } from 'next/router'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import classNames from 'classnames'
|
||||
import clonedeep from 'lodash.clonedeep'
|
||||
|
||||
import Linkify from 'react-linkify'
|
||||
import LiteYouTubeEmbed from 'react-lite-youtube-embed'
|
||||
import reactStringReplace from 'react-string-replace'
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
import Button from '~components/common/Button'
|
||||
import SegmentedControl from '~components/common/SegmentedControl'
|
||||
|
|
@ -27,6 +23,7 @@ import type { DetailsObject } from 'types'
|
|||
import RemixIcon from '~public/icons/Remix.svg'
|
||||
import EditIcon from '~public/icons/Edit.svg'
|
||||
import styles from './index.module.scss'
|
||||
import Editor from '~components/common/Editor'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
|
|
@ -55,42 +52,39 @@ const PartyFooter = (props: Props) => {
|
|||
|
||||
// State: Data
|
||||
const [remixes, setRemixes] = useState<Party[]>([])
|
||||
const [embeddedDescription, setEmbeddedDescription] =
|
||||
useState<React.ReactNode>()
|
||||
const [sanitizedDescription, setSanitizedDescription] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
// Extract the video IDs from the description
|
||||
if (partySnapshot.description) {
|
||||
const videoIds = extractYoutubeVideoIds(partySnapshot.description)
|
||||
|
||||
// Fetch the video titles for each ID
|
||||
const fetchPromises = videoIds.map(({ id }) => fetchYoutubeData(id))
|
||||
|
||||
// Wait for all the video titles to be fetched
|
||||
Promise.all(fetchPromises).then((videoTitles) => {
|
||||
// Replace the video URLs in the description with LiteYoutubeEmbed elements
|
||||
const newDescription = reactStringReplace(
|
||||
partySnapshot.description,
|
||||
youtubeUrlRegex,
|
||||
(match, i) => (
|
||||
<LiteYouTubeEmbed
|
||||
key={`${match}-${i}`}
|
||||
id={match}
|
||||
title={videoTitles[i]}
|
||||
wrapperClass={styles.youtube}
|
||||
playerClass={styles.playerButton}
|
||||
/>
|
||||
)
|
||||
)
|
||||
|
||||
// Update the state with the new description
|
||||
setEmbeddedDescription(newDescription)
|
||||
})
|
||||
const purified = DOMPurify.sanitize(partySnapshot.description)
|
||||
setSanitizedDescription(purified)
|
||||
} else {
|
||||
setEmbeddedDescription('')
|
||||
setSanitizedDescription('')
|
||||
}
|
||||
}, [partySnapshot.description])
|
||||
|
||||
// Extract the video IDs from the description
|
||||
// const videoIds = extractYoutubeVideoIds(partySnapshot.description)
|
||||
// Fetch the video titles for each ID
|
||||
// const fetchPromises = videoIds.map(({ id }) => fetchYoutubeData(id))
|
||||
// // Wait for all the video titles to be fetched
|
||||
// Promise.all(fetchPromises).then((videoTitles) => {
|
||||
// // Replace the video URLs in the description with LiteYoutubeEmbed elements
|
||||
// const newDescription = reactStringReplace(
|
||||
// partySnapshot.description,
|
||||
// youtubeUrlRegex,
|
||||
// (match, i) => (
|
||||
// <LiteYouTubeEmbed
|
||||
// key={`${match}-${i}`}
|
||||
// id={match}
|
||||
// title={videoTitles[i]}
|
||||
// wrapperClass={styles.youtube}
|
||||
// playerClass={styles.playerButton}
|
||||
// />
|
||||
// )
|
||||
// )
|
||||
// Update the state with the new description
|
||||
|
||||
async function fetchYoutubeData(videoId: string) {
|
||||
return await youtube
|
||||
.getVideoById(videoId, { maxResults: 1 })
|
||||
|
|
@ -213,14 +207,17 @@ const PartyFooter = (props: Props) => {
|
|||
)
|
||||
|
||||
const descriptionSection = (
|
||||
<section className={styles.description}>
|
||||
{partySnapshot &&
|
||||
partySnapshot.description &&
|
||||
partySnapshot.description.length > 0 && (
|
||||
<Linkify>{embeddedDescription}</Linkify>
|
||||
<>
|
||||
{props.party &&
|
||||
props.party.description &&
|
||||
props.party.description.length > 0 && (
|
||||
<Editor
|
||||
content={props.party.description}
|
||||
key={props.party?.shortcode}
|
||||
/>
|
||||
)}
|
||||
{(!partySnapshot || !partySnapshot.description) && (
|
||||
<div className={styles.noDescription}>
|
||||
<section className={styles.noDescription}>
|
||||
<h3>{t('footer.description.empty')}</h3>
|
||||
{props.editable && (
|
||||
<EditPartyModal
|
||||
|
|
@ -236,9 +233,9 @@ const PartyFooter = (props: Props) => {
|
|||
/>
|
||||
</EditPartyModal>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
|
||||
const remixesSection = (
|
||||
|
|
|
|||
|
|
@ -75,5 +75,9 @@
|
|||
background: var(--dark-hover-bg);
|
||||
border-color: var(--dark-bg);
|
||||
}
|
||||
|
||||
&.empty {
|
||||
background: var(--card-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,10 +74,11 @@ const CharacterRep = (props: Props) => {
|
|||
source = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/job-portraits/${slug}_${gender}.png`
|
||||
}
|
||||
|
||||
return props.job && props.job.id !== '-1' ? (
|
||||
<img alt={props.job ? props.job?.name[locale] : ''} src={source} />
|
||||
) : (
|
||||
''
|
||||
return (
|
||||
props.job &&
|
||||
props.job.id !== '-1' && (
|
||||
<img alt={props.job ? props.job?.name[locale] : ''} src={source} />
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -95,9 +96,9 @@ const CharacterRep = (props: Props) => {
|
|||
else if (gridCharacter.uncap_level > 2) suffix = '02'
|
||||
|
||||
if (character.element == 0) {
|
||||
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-main/${character.granblue_id}_${props.element}.jpg`
|
||||
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/character-main/${character.granblue_id}_${props.element}.jpg`
|
||||
} else {
|
||||
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-main/${character.granblue_id}_${suffix}.jpg`
|
||||
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/character-main/${character.granblue_id}_${suffix}.jpg`
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -117,6 +118,7 @@ const CharacterRep = (props: Props) => {
|
|||
className={classNames({
|
||||
[styles.protagonist]: true,
|
||||
[styles[`${numberToElement()}`]]: true,
|
||||
[styles.empty]: !props.job || props.job.id === '-1',
|
||||
})}
|
||||
>
|
||||
{generateMCImage()}
|
||||
|
|
|
|||
25
extensions/CustomMention/index.tsx
Normal file
25
extensions/CustomMention/index.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { mergeAttributes, Node } from '@tiptap/core'
|
||||
import Mention from '@tiptap/extension-mention'
|
||||
|
||||
export default Mention.extend({
|
||||
renderHTML({ node, HTMLAttributes }) {
|
||||
return [
|
||||
'a',
|
||||
mergeAttributes(
|
||||
{
|
||||
href: `https://gbf.wiki/${node.attrs.id.name.en}`,
|
||||
target: '_blank',
|
||||
},
|
||||
{ 'data-type': this.name },
|
||||
{ 'data-element': node.attrs.id.element.slug },
|
||||
{ tabindex: -1 },
|
||||
this.options.HTMLAttributes,
|
||||
HTMLAttributes
|
||||
),
|
||||
this.options.renderLabel({
|
||||
options: this.options,
|
||||
node,
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
951
package-lock.json
generated
951
package-lock.json
generated
File diff suppressed because it is too large
Load diff
19
package.json
19
package.json
|
|
@ -26,11 +26,22 @@
|
|||
"@radix-ui/react-toggle-group": "^1.0.1",
|
||||
"@radix-ui/react-tooltip": "^1.0.3",
|
||||
"@svgr/webpack": "^6.2.0",
|
||||
"@tiptap/extension-bubble-menu": "^2.0.3",
|
||||
"@tiptap/extension-highlight": "^2.0.3",
|
||||
"@tiptap/extension-link": "^2.0.3",
|
||||
"@tiptap/extension-mention": "^2.0.3",
|
||||
"@tiptap/extension-typography": "^2.0.3",
|
||||
"@tiptap/extension-youtube": "^2.0.3",
|
||||
"@tiptap/pm": "^2.0.3",
|
||||
"@tiptap/react": "^2.0.3",
|
||||
"@tiptap/starter-kit": "^2.0.3",
|
||||
"@tiptap/suggestion": "2.0.0-beta.91",
|
||||
"axios": "^0.25.0",
|
||||
"classnames": "^2.3.1",
|
||||
"cmdk": "^0.2.0",
|
||||
"cookies-next": "^2.1.1",
|
||||
"date-fns": "^2.29.3",
|
||||
"dompurify": "^3.0.4",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fix-date": "^1.1.6",
|
||||
"i18next": "^21.6.13",
|
||||
|
|
@ -54,9 +65,11 @@
|
|||
"react-lite-youtube-embed": "^2.3.52",
|
||||
"react-scroll": "^1.8.5",
|
||||
"react-string-replace": "^1.1.0",
|
||||
"remixicon-react": "^1.0.0",
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"sanitize-html": "^2.8.1",
|
||||
"sass": "^1.61.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"usehooks-ts": "^2.9.1",
|
||||
"uuid": "^9.0.0",
|
||||
"valtio": "^1.3.0",
|
||||
|
|
@ -72,6 +85,7 @@
|
|||
"@storybook/nextjs": "latest",
|
||||
"@storybook/react": "latest",
|
||||
"@storybook/testing-library": "latest",
|
||||
"@types/dompurify": "^3.0.2",
|
||||
"@types/lodash.clonedeep": "^4.5.6",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/node": "17.0.11",
|
||||
|
|
@ -90,5 +104,10 @@
|
|||
"sass-loader": "^13.2.2",
|
||||
"storybook": "latest",
|
||||
"typescript": "^4.5.5"
|
||||
},
|
||||
"overrides": {
|
||||
"@tiptap/extension-mention": {
|
||||
"@tiptap/suggestion": "2.0.0-beta.91"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
public/icons/remix/list-ordered-2.svg
Normal file
1
public/icons/remix/list-ordered-2.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5.75024 3.5H4.71733L3.25 3.89317V5.44582L4.25002 5.17782L4.25018 8.5H3V10H7V8.5H5.75024V3.5ZM10 4H21V6H10V4ZM10 11H21V13H10V11ZM10 18H21V20H10V18ZM2.875 15.625C2.875 14.4514 3.82639 13.5 5 13.5C6.17361 13.5 7.125 14.4514 7.125 15.625C7.125 16.1106 6.96183 16.5587 6.68747 16.9167L6.68271 16.9229L5.31587 18.5H7V20H3.00012L2.99959 18.8786L5.4717 16.035C5.5673 15.9252 5.625 15.7821 5.625 15.625C5.625 15.2798 5.34518 15 5 15C4.67378 15 4.40573 15.2501 4.37747 15.5688L4.3651 15.875H2.875V15.625Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 579 B |
|
|
@ -487,6 +487,7 @@
|
|||
"start_typing": "Start typing the name of a {{object}}",
|
||||
"min_length": "Type at least 3 characters",
|
||||
"no_results": "No results found for '{{query}}'",
|
||||
"no_results_generic": "No results found",
|
||||
"end_results": "No more results"
|
||||
},
|
||||
"placeholders": {
|
||||
|
|
|
|||
|
|
@ -485,6 +485,7 @@
|
|||
"start_typing": "{{object}}名を入力してください",
|
||||
"min_length": "3文字以上を入力してください",
|
||||
"no_results": "'{{query}}'の検索結果が見つかりませんでした",
|
||||
"no_results_generic": "検索結果が見つかりませんでした",
|
||||
"end_results": "検索結果これ以上ありません"
|
||||
},
|
||||
"placeholders": {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
html {
|
||||
background-color: var(--background);
|
||||
font-size: 62.5%;
|
||||
height: 100%;
|
||||
// height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,31 @@
|
|||
--menu-bg-item-hover: #{$menu--item--bg--light--hover};
|
||||
--menu-text-hover: #{$menu--text--light--hover};
|
||||
|
||||
// Light - Formatting menu
|
||||
--formatting-menu-bg: #{$formatting--menu--bg--light};
|
||||
--formatting-menu-item-bg: #{$formatting--menu--item--bg--light};
|
||||
--formatting-menu-item-bg-hover: #{$formatting--menu--item--bg--light--hover};
|
||||
--formatting-menu-item-bg-active: #{$formatting--menu--item--bg--dark--active};
|
||||
--formatting-menu-item-text: #{$formatting--menu--item--text--light};
|
||||
--formatting-menu-item-text-hover: #{$formatting--menu--item--text--light--hover};
|
||||
--formatting-menu-item-text-active: #{$formatting--menu--item--text--dark--active};
|
||||
|
||||
// Light - Toolbar
|
||||
--toolbar-bg: #{$toolbar--bg--light};
|
||||
--toolbar-divider-bg: #{$toolbar--divider--bg--light};
|
||||
--toolbar-item-bg: #{$toolbar--item--bg--light};
|
||||
--toolbar-item-bg-hover: #{$toolbar--item--bg--light--hover};
|
||||
--toolbar-item-bg-active: #{$toolbar--item--bg--light--active};
|
||||
--toolbar-item-text: #{$toolbar--item--text--light};
|
||||
--toolbar-item-text-hover: #{$toolbar--item--text--light--hover};
|
||||
--toolbar-item-text-active: #{$toolbar--item--text--light--active};
|
||||
|
||||
// Dark - Highlights
|
||||
--highlight-bg: #{$highlight--bg--light};
|
||||
--highlight-bg-hover: #{$highlight--bg--light--hover};
|
||||
--highlight-text: #{$highlight--text--light};
|
||||
--highlight-text-hover: #{$highlight--text--light--hover};
|
||||
|
||||
// Light - Placeholders
|
||||
--placeholder-bound-bg: #{$placeholder--bound--bg--light};
|
||||
--placeholder-bound-bg-hover: #{$placeholder--bound--bg--light--hover};
|
||||
|
|
@ -139,36 +164,48 @@
|
|||
--wind-text: #{$wind--text--light};
|
||||
--wind-raid-text: #{$wind--text--raid--light};
|
||||
--wind-text-hover: #{$wind--text--hover--light};
|
||||
--wind-shadow: #{$wind--shadow--light};
|
||||
--wind-shadow-hover: #{$wind--shadow--light--hover};
|
||||
|
||||
--fire-bg: #{$fire--bg--light};
|
||||
--fire-bg-hover: #{$fire--bg--hover--light};
|
||||
--fire-text: #{$fire--text--light};
|
||||
--fire-raid-text: #{$fire--text--raid--light};
|
||||
--fire-text-hover: #{$fire--text--hover--light};
|
||||
--fire-shadow: #{$fire--shadow--light};
|
||||
--fire-shadow-hover: #{$fire--shadow--light--hover};
|
||||
|
||||
--water-bg: #{$water--bg--light};
|
||||
--water-bg-hover: #{$water--bg--hover--light};
|
||||
--water-text: #{$water--text--light};
|
||||
--water-raid-text: #{$water--text--raid--light};
|
||||
--water-text-hover: #{$water--text--hover--light};
|
||||
--water-shadow: #{$water--shadow--light};
|
||||
--water-shadow-hover: #{$water--shadow--light--hover};
|
||||
|
||||
--earth-bg: #{$earth--bg--light};
|
||||
--earth-bg-hover: #{$earth--bg--hover--light};
|
||||
--earth-text: #{$earth--text--light};
|
||||
--earth-raid-text: #{$earth--text--raid--light};
|
||||
--earth-text-hover: #{$earth--text--hover--light};
|
||||
--earth-shadow: #{$earth--shadow--light};
|
||||
--earth-shadow-hover: #{$earth--shadow--light--hover};
|
||||
|
||||
--dark-bg: #{$dark--bg--light};
|
||||
--dark-bg-hover: #{$dark--bg--hover--light};
|
||||
--dark-text: #{$dark--text--light};
|
||||
--dark-raid-text: #{$dark--text--raid--light};
|
||||
--dark-text-hover: #{$dark--text--hover--light};
|
||||
--dark-shadow: #{$dark--shadow--light};
|
||||
--dark-shadow-hover: #{$dark--shadow--light--hover};
|
||||
|
||||
--light-bg: #{$light--bg--light};
|
||||
--light-bg-hover: #{$light--bg--hover--light};
|
||||
--light-text: #{$light--text--light};
|
||||
--light-raid-text: #{$light--text--raid--light};
|
||||
--light-text-hover: #{$light--text--hover--light};
|
||||
--light-shadow: #{$light--shadow--light};
|
||||
--light-shadow-hover: #{$light--shadow--light--hover};
|
||||
|
||||
// Gradients
|
||||
--hero-gradient: #{$hero--gradient--light};
|
||||
|
|
@ -221,6 +258,31 @@
|
|||
--menu-bg-item-hover: #{$menu--item--bg--dark--hover};
|
||||
--menu-text-hover: #{$menu--text--dark--hover};
|
||||
|
||||
// Dark - Formatting menu
|
||||
--formatting-menu-bg: #{$formatting--menu--bg--dark};
|
||||
--formatting-menu-item-bg: #{$formatting--menu--item--bg--dark};
|
||||
--formatting-menu-item-bg-hover: #{$formatting--menu--item--bg--dark--hover};
|
||||
--formatting-menu-item-bg-active: #{$formatting--menu--item--bg--dark--active};
|
||||
--formatting-menu-item-text: #{$formatting--menu--item--text--dark};
|
||||
--formatting-menu-item-text-hover: #{$formatting--menu--item--text--dark--hover};
|
||||
--formatting-menu-item-text-active: #{$formatting--menu--item--text--dark--active};
|
||||
|
||||
// Dark - Toolbar
|
||||
--toolbar-bg: #{$toolbar--bg--dark};
|
||||
--toolbar-divider-bg: #{$toolbar--divider--bg--dark};
|
||||
--toolbar-item-bg: #{$toolbar--item--bg--dark};
|
||||
--toolbar-item-bg-hover: #{$toolbar--item--bg--dark--hover};
|
||||
--toolbar-item-bg-active: #{$toolbar--item--bg--dark--active};
|
||||
--toolbar-item-text: #{$toolbar--item--text--dark};
|
||||
--toolbar-item-text-hover: #{$toolbar--item--text--dark--hover};
|
||||
--toolbar-item-text-active: #{$toolbar--item--text--dark--active};
|
||||
|
||||
// Dark - Highlights
|
||||
--highlight-bg: #{$highlight--bg--dark};
|
||||
--highlight-bg-hover: #{$highlight--bg--dark--hover};
|
||||
--highlight-text: #{$highlight--text--dark};
|
||||
--highlight-text-hover: #{$highlight--text--dark--hover};
|
||||
|
||||
// Dark - Placeholders
|
||||
--placeholder-bound-bg: #{$placeholder--bound--bg--dark};
|
||||
--placeholder-bound-bg-hover: #{$placeholder--bound--bg--dark--hover};
|
||||
|
|
@ -316,36 +378,48 @@
|
|||
--wind-text: #{$wind--text--dark};
|
||||
--wind-raid-text: #{$wind--text--raid--dark};
|
||||
--wind-text-hover: #{$wind--text--hover--dark};
|
||||
--wind-shadow: #{$wind--shadow--dark};
|
||||
--wind-shadow-hover: #{$wind--shadow--dark--hover};
|
||||
|
||||
--fire-bg: #{$fire--bg--dark};
|
||||
--fire-bg-hover: #{$fire--bg--hover--dark};
|
||||
--fire-text: #{$fire--text--dark};
|
||||
--fire-raid-text: #{$fire--text--raid--dark};
|
||||
--fire-text-hover: #{$fire--text--hover--dark};
|
||||
--fire-shadow: #{$fire--shadow--dark};
|
||||
--fire-shadow-hover: #{$fire--shadow--dark--hover};
|
||||
|
||||
--water-bg: #{$water--bg--dark};
|
||||
--water-bg-hover: #{$water--bg--hover--dark};
|
||||
--water-text: #{$water--text--dark};
|
||||
--water-raid-text: #{$water--text--raid--dark};
|
||||
--water-text-hover: #{$water--text--hover--dark};
|
||||
--water-shadow: #{$water--shadow--dark};
|
||||
--water-shadow-hover: #{$water--shadow--dark--hover};
|
||||
|
||||
--earth-bg: #{$earth--bg--dark};
|
||||
--earth-bg-hover: #{$earth--bg--hover--dark};
|
||||
--earth-text: #{$earth--text--dark};
|
||||
--earth-raid-text: #{$earth--text--raid--dark};
|
||||
--earth-text-hover: #{$earth--text--hover--dark};
|
||||
--earth-shadow: #{$earth--shadow--dark};
|
||||
--earth-shadow-hover: #{$earth--shadow--dark--hover};
|
||||
|
||||
--dark-bg: #{$dark--bg--dark};
|
||||
--dark-bg-hover: #{$dark--bg--hover--dark};
|
||||
--dark-text: #{$dark--text--dark};
|
||||
--dark-raid-text: #{$dark--text--raid--dark};
|
||||
--dark-text-hover: #{$dark--text--hover--dark};
|
||||
--dark-shadow: #{$dark--shadow--dark};
|
||||
--dark-shadow-hover: #{$dark--shadow--dark--hover};
|
||||
|
||||
--light-bg: #{$light--bg--dark};
|
||||
--light-bg-hover: #{$light--bg--hover--dark};
|
||||
--light-text: #{$light--text--dark};
|
||||
--light-raid-text: #{$light--text--raid--dark};
|
||||
--light-text-hover: #{$light--text--hover--dark};
|
||||
--light-shadow: #{$light--shadow--dark};
|
||||
--light-shadow-hover: #{$light--shadow--dark--hover};
|
||||
|
||||
// Gradients
|
||||
--hero-gradient: #{$hero--gradient--dark};
|
||||
|
|
|
|||
|
|
@ -79,6 +79,13 @@ $orange-75: #ffb461;
|
|||
$orange-80: #facea7;
|
||||
$orange-90: #ffebd9;
|
||||
|
||||
// Yellow -- Highlights
|
||||
$yellow-10: #4d3703;
|
||||
$yellow-30: #956d11;
|
||||
$yellow-50: #c8a657;
|
||||
$yellow-70: #fedc8d;
|
||||
$yellow-90: #ffed4c;
|
||||
|
||||
// Colors -- Interface
|
||||
$blue: #275dc5;
|
||||
$red: #ff6161;
|
||||
|
|
@ -96,6 +103,7 @@ $accent--yellow--light: #c89d39;
|
|||
$accent--yellow--dark: #f9cc64;
|
||||
$yellow-text-10: #a39200;
|
||||
$yellow-text-20: #ffed4c;
|
||||
$highlight-yellow: #ffed4c55;
|
||||
|
||||
$accent--yellow--00: #463805;
|
||||
$accent--yellow--20: #7f6a00;
|
||||
|
|
@ -336,6 +344,66 @@ $pill--bg--dark--hover: $grey-50;
|
|||
$pill--text--dark: $grey-100;
|
||||
$pill--text--dark--hover: $grey-00;
|
||||
|
||||
// Color Definitions: Formatting menu
|
||||
$formatting--menu--bg--light: $grey-30;
|
||||
$formatting--menu--bg--dark: $grey-10;
|
||||
|
||||
$formatting--menu--item--bg--light: $grey-30;
|
||||
$formatting--menu--item--bg--dark: $grey-20;
|
||||
|
||||
$formatting--menu--item--bg--light--hover: $grey-40;
|
||||
$formatting--menu--item--bg--dark--hover: $grey-30;
|
||||
|
||||
$formatting--menu--item--bg--light--active: $grey-50;
|
||||
$formatting--menu--item--bg--dark--active: $grey-40;
|
||||
|
||||
$formatting--menu--item--text--light: $grey-100;
|
||||
$formatting--menu--item--text--dark: $grey-00;
|
||||
|
||||
$formatting--menu--item--text--light--hover: $grey-100;
|
||||
$formatting--menu--item--text--dark--hover: $grey-00;
|
||||
|
||||
$formatting--menu--item--text--light--active: $grey-100;
|
||||
$formatting--menu--item--text--dark--active: $grey-00;
|
||||
|
||||
// Color Definitions: Toolbar
|
||||
$toolbar--bg--light: $grey-75;
|
||||
$toolbar--bg--dark: $grey-10;
|
||||
|
||||
$toolbar--divider--bg--light: $grey-70;
|
||||
$toolbar--divider--bg--dark: $grey-20;
|
||||
|
||||
$toolbar--item--bg--light: $grey-75;
|
||||
$toolbar--item--bg--dark: $grey-20;
|
||||
|
||||
$toolbar--item--bg--light--hover: $grey-70;
|
||||
$toolbar--item--bg--dark--hover: $grey-30;
|
||||
|
||||
$toolbar--item--bg--light--active: $accent--blue--light;
|
||||
$toolbar--item--bg--dark--active: $accent--blue--dark;
|
||||
|
||||
$toolbar--item--text--light: $grey-40;
|
||||
$toolbar--item--text--dark: $grey-80;
|
||||
|
||||
$toolbar--item--text--light--hover: $grey-30;
|
||||
$toolbar--item--text--dark--hover: $grey-90;
|
||||
|
||||
$toolbar--item--text--light--active: $grey-100;
|
||||
$toolbar--item--text--dark--active: $grey-00;
|
||||
|
||||
// Color Definitions: Highlights
|
||||
$highlight--bg--light: $yellow-70;
|
||||
$highlight--bg--dark: $yellow-50;
|
||||
|
||||
$highlight--bg--light--hover: $yellow-50;
|
||||
$highlight--bg--dark--hover: $yellow-70;
|
||||
|
||||
$highlight--text--light: $yellow-30;
|
||||
$highlight--text--dark: $yellow-10;
|
||||
|
||||
$highlight--text--light--hover: $yellow-10;
|
||||
$highlight--text--dark--hover: $yellow-30;
|
||||
|
||||
// Color Definitions: Element Toggle
|
||||
$toggle--bg--light: $grey-90;
|
||||
$toggle--bg--dark: $grey-15;
|
||||
|
|
@ -390,6 +458,12 @@ $wind--text--raid--dark: $wind-bg-10;
|
|||
$wind--text--hover--light: $wind-text-00;
|
||||
$wind--text--hover--dark: $wind-text-00;
|
||||
|
||||
$wind--shadow--light: fade-out($wind-text-20, 0.3);
|
||||
$wind--shadow--dark: fade-out($wind-text-20, 0.3);
|
||||
|
||||
$wind--shadow--light--hover: fade-out($wind-text-00, 0.3);
|
||||
$wind--shadow--dark--hover: fade-out($wind-text-00, 0.3);
|
||||
|
||||
// Color Definitions: Element / Fire
|
||||
$fire--bg--light: $fire-bg-10;
|
||||
$fire--bg--dark: $fire-bg-10;
|
||||
|
|
@ -406,6 +480,12 @@ $fire--text--raid--dark: $fire-bg-10;
|
|||
$fire--text--hover--light: $fire-text-00;
|
||||
$fire--text--hover--dark: $fire-text-00;
|
||||
|
||||
$fire--shadow--light: fade-out($fire-text-20, 0.3);
|
||||
$fire--shadow--dark: fade-out($fire-text-20, 0.3);
|
||||
|
||||
$fire--shadow--light--hover: fade-out($fire-text-00, 0.3);
|
||||
$fire--shadow--dark--hover: fade-out($fire-text-00, 0.3);
|
||||
|
||||
// Color Definitions: Element / Water
|
||||
$water--bg--light: $water-bg-10;
|
||||
$water--bg--dark: $water-bg-10;
|
||||
|
|
@ -422,6 +502,12 @@ $water--text--raid--dark: $water-bg-10;
|
|||
$water--text--hover--light: $water-text-00;
|
||||
$water--text--hover--dark: $water-text-00;
|
||||
|
||||
$water--shadow--light: fade-out($water-text-20, 0.3);
|
||||
$water--shadow--dark: fade-out($water-text-20, 0.3);
|
||||
|
||||
$water--shadow--light--hover: fade-out($water-text-00, 0.3);
|
||||
$water--shadow--dark--hover: fade-out($water-text-00, 0.3);
|
||||
|
||||
// Color Definitions: Element / Earth
|
||||
$earth--bg--light: $earth-bg-10;
|
||||
$earth--bg--dark: $earth-bg-10;
|
||||
|
|
@ -438,6 +524,12 @@ $earth--text--raid--dark: $earth-bg-10;
|
|||
$earth--text--hover--light: $earth-text-00;
|
||||
$earth--text--hover--dark: $earth-text-00;
|
||||
|
||||
$earth--shadow--light: fade-out($earth-text-20, 0.3);
|
||||
$earth--shadow--dark: fade-out($earth-text-20, 0.3);
|
||||
|
||||
$earth--shadow--light--hover: fade-out($earth-text-00, 0.3);
|
||||
$earth--shadow--dark--hover: fade-out($earth-text-00, 0.3);
|
||||
|
||||
// Color Definitions: Element / Dark
|
||||
$dark--bg--light: $dark-bg-10;
|
||||
$dark--bg--dark: $dark-bg-10;
|
||||
|
|
@ -454,6 +546,12 @@ $dark--text--raid--dark: $dark-bg-10;
|
|||
$dark--text--hover--light: $dark-text-00;
|
||||
$dark--text--hover--dark: $dark-text-00;
|
||||
|
||||
$dark--shadow--light: fade-out($dark-text-20, 0.3);
|
||||
$dark--shadow--dark: fade-out($dark-text-20, 0.3);
|
||||
|
||||
$dark--shadow--light--hover: fade-out($dark-text-00, 0.3);
|
||||
$dark--shadow--dark--hover: fade-out($dark-text-00, 0.3);
|
||||
|
||||
// Color Definitions: Element / Light
|
||||
$light--bg--light: $light-bg-10;
|
||||
$light--bg--dark: $light-bg-10;
|
||||
|
|
@ -470,6 +568,12 @@ $light--text--raid--dark: $light-bg-10;
|
|||
$light--text--hover--light: $light-text-00;
|
||||
$light--text--hover--dark: $light-text-00;
|
||||
|
||||
$light--shadow--light: fade-out($light-text-20, 0.3);
|
||||
$light--shadow--dark: fade-out($light-text-20, 0.3);
|
||||
|
||||
$light--shadow--light--hover: fade-out($light-text-00, 0.3);
|
||||
$light--shadow--dark--hover: fade-out($light-text-00, 0.3);
|
||||
|
||||
// Font-weight
|
||||
$normal: 400;
|
||||
$medium: 500;
|
||||
|
|
@ -497,6 +601,9 @@ $input-corner: $unit;
|
|||
$item-corner: $unit;
|
||||
$item-corner-small: $unit-half;
|
||||
|
||||
$bubble-menu-corner: $unit;
|
||||
$bubble-menu-item-corner: $unit-half * 1.5;
|
||||
|
||||
// Shadows
|
||||
$hover-stroke: 1px solid rgba(0, 0, 0, 0.1);
|
||||
$hover-shadow: rgba(0, 0, 0, 0.08) 0px 0px 14px;
|
||||
|
|
|
|||
10
types/GranblueElement.d.ts
vendored
Normal file
10
types/GranblueElement.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
interface GranblueElement {
|
||||
[key: string]: any
|
||||
id: number
|
||||
weaknessId: number
|
||||
name: {
|
||||
en: string
|
||||
ja: string
|
||||
}
|
||||
slug: string
|
||||
}
|
||||
|
|
@ -69,6 +69,16 @@ class Api {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
searchAll(query: string, locale: string) {
|
||||
const resourceUrl = `${this.url}/search`
|
||||
return axios.post(`${resourceUrl}`, {
|
||||
search: {
|
||||
query: query,
|
||||
locale: locale
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
check(resource: string, value: string) {
|
||||
const resourceUrl = `${this.url}/check/${resource}`
|
||||
|
|
|
|||
73
utils/elements.tsx
Normal file
73
utils/elements.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
export const elements: GranblueElement[] = [
|
||||
{
|
||||
id: 0,
|
||||
weaknessId: 0,
|
||||
name: {
|
||||
en: 'Null',
|
||||
ja: '無',
|
||||
},
|
||||
slug: 'null',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
weaknessId: 2,
|
||||
name: {
|
||||
en: 'Wind',
|
||||
ja: '風',
|
||||
},
|
||||
slug: 'wind',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
weaknessId: 3,
|
||||
name: {
|
||||
en: 'Fire',
|
||||
ja: '火',
|
||||
},
|
||||
slug: 'fire',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
weaknessId: 4,
|
||||
name: {
|
||||
en: 'Water',
|
||||
ja: '水',
|
||||
},
|
||||
slug: 'water',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
weaknessId: 1,
|
||||
name: {
|
||||
en: 'Earth',
|
||||
ja: '土',
|
||||
},
|
||||
slug: 'earth',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
weaknessId: 5,
|
||||
name: {
|
||||
en: 'Dark',
|
||||
ja: '闇',
|
||||
},
|
||||
slug: 'dark',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
weaknessId: 6,
|
||||
name: {
|
||||
en: 'Light',
|
||||
ja: '光',
|
||||
},
|
||||
slug: 'light',
|
||||
},
|
||||
]
|
||||
|
||||
export function numberToElement(value: number) {
|
||||
return elements.find((element) => element.id === value) || elements[0]
|
||||
}
|
||||
|
||||
export function stringToElement(value: string) {
|
||||
return elements.find((element) => element.name.en === value)
|
||||
}
|
||||
129
utils/mentionSuggestions.tsx
Normal file
129
utils/mentionSuggestions.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { ReactRenderer } from '@tiptap/react'
|
||||
import { MentionOptions } from '@tiptap/extension-mention'
|
||||
import { SuggestionKeyDownProps, SuggestionProps } from '@tiptap/suggestion'
|
||||
import tippy, { Instance as TippyInstance } from 'tippy.js'
|
||||
import { getCookie } from 'cookies-next'
|
||||
|
||||
import {
|
||||
MentionList,
|
||||
MentionRef,
|
||||
MentionSuggestion,
|
||||
} from '~components/MentionList'
|
||||
import api from '~utils/api'
|
||||
import { numberToElement } from '~utils/elements'
|
||||
import { get } from 'http'
|
||||
|
||||
interface RawSearchResponse {
|
||||
searchable_type: string
|
||||
granblue_id: string
|
||||
name_en: string
|
||||
name_jp: string
|
||||
element: number
|
||||
}
|
||||
|
||||
interface SearchResponse {
|
||||
name: {
|
||||
[key: string]: string
|
||||
en: string
|
||||
ja: string
|
||||
}
|
||||
type: string
|
||||
granblue_id: string
|
||||
element: GranblueElement
|
||||
}
|
||||
|
||||
function transform(object: RawSearchResponse) {
|
||||
const result: SearchResponse = {
|
||||
name: {
|
||||
en: object.name_en,
|
||||
ja: object.name_jp,
|
||||
},
|
||||
type: object.searchable_type.toLowerCase(),
|
||||
granblue_id: object.granblue_id,
|
||||
element: numberToElement(object.element),
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const mentionSuggestionOptions: MentionOptions['suggestion'] = {
|
||||
items: async ({ query }): Promise<MentionSuggestion[]> => {
|
||||
const locale = getCookie('NEXT_LOCALE')
|
||||
? (getCookie('NEXT_LOCALE') as string)
|
||||
: 'en'
|
||||
const response = await api.searchAll(query, locale)
|
||||
const results = response.data.results
|
||||
|
||||
return results
|
||||
.map((rawObject: RawSearchResponse, index: number) => {
|
||||
const object = transform(rawObject)
|
||||
return {
|
||||
granblue_id: object.granblue_id,
|
||||
element: object.element,
|
||||
type: object.type,
|
||||
name: {
|
||||
en: object.name.en,
|
||||
ja: object.name.ja,
|
||||
},
|
||||
}
|
||||
})
|
||||
.slice(0, 7)
|
||||
},
|
||||
|
||||
render: () => {
|
||||
let component: ReactRenderer<MentionRef> | undefined
|
||||
let popup: TippyInstance | undefined
|
||||
|
||||
return {
|
||||
onStart: (props) => {
|
||||
component = new ReactRenderer(MentionList, {
|
||||
props,
|
||||
editor: props.editor,
|
||||
})
|
||||
|
||||
popup = tippy('body', {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.body,
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
placement: 'bottom-start',
|
||||
})[0]
|
||||
},
|
||||
|
||||
onUpdate(props) {
|
||||
component?.updateProps(props)
|
||||
|
||||
popup?.setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
})
|
||||
},
|
||||
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
popup?.hide()
|
||||
return true
|
||||
}
|
||||
|
||||
if (!component?.ref) {
|
||||
return false
|
||||
}
|
||||
|
||||
return component?.ref.onKeyDown(props)
|
||||
},
|
||||
|
||||
onExit() {
|
||||
popup?.destroy()
|
||||
component?.destroy()
|
||||
|
||||
// Remove references to the old popup and component upon destruction/exit.
|
||||
// (This should prevent redundant calls to `popup.destroy()`, which Tippy
|
||||
// warns in the console is a sign of a memory leak, as the `suggestion`
|
||||
// plugin seems to call `onExit` both when a suggestion menu is closed after
|
||||
// a user chooses an option, *and* when the editor itself is destroyed.)
|
||||
popup = undefined
|
||||
component = undefined
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
Loading…
Reference in a new issue