Compare commits
24 commits
main
...
descriptio
| Author | SHA1 | Date | |
|---|---|---|---|
| 849cea035b | |||
| 9de4fcca83 | |||
| bca541a857 | |||
| 7ee1c3e857 | |||
| 5a12cf2fc1 | |||
| 6cb607ed7a | |||
| 9650c34957 | |||
| 98c072655d | |||
| b32caf8790 | |||
| b641091695 | |||
| d950d3a935 | |||
| b1c8fb1a76 | |||
| 52af8995e6 | |||
| ddfab4b2c0 | |||
| ce6e8c6471 | |||
| a76729bbc6 | |||
| eaf6da97b8 | |||
| 00f117c118 | |||
| 0fb674b150 | |||
| 97ea666561 | |||
| b3b3c5c960 | |||
| ed01ea6955 | |||
| 277a248ba1 | |||
| 63846089a4 |
31 changed files with 2049 additions and 104 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -49,7 +49,7 @@ dist/
|
||||||
# Instructions will be provided to download these from the game
|
# Instructions will be provided to download these from the game
|
||||||
public/images/weapon*
|
public/images/weapon*
|
||||||
public/images/summon*
|
public/images/summon*
|
||||||
public/images/chara*
|
public/images/character*
|
||||||
public/images/job*
|
public/images/job*
|
||||||
public/images/awakening*
|
public/images/awakening*
|
||||||
public/images/ax*
|
public/images/ax*
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,9 @@ root
|
||||||
├─ accessory-square/
|
├─ accessory-square/
|
||||||
├─ awakening/
|
├─ awakening/
|
||||||
├─ ax/
|
├─ ax/
|
||||||
├─ chara-main/
|
├─ character-main/
|
||||||
├─ chara-grid/
|
├─ character-grid/
|
||||||
├─ chara-square/
|
├─ character-square/
|
||||||
├─ guidebooks/
|
├─ guidebooks/
|
||||||
├─ jobs/
|
├─ jobs/
|
||||||
├─ job-icons/
|
├─ 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 == 5) suffix = '03'
|
||||||
else if (gridCharacter.uncap_level > 2) suffix = '02'
|
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 = () => {
|
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) {
|
switch (type) {
|
||||||
case 'character':
|
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
|
break
|
||||||
case 'weapon':
|
case 'weapon':
|
||||||
src = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${id}.jpg`
|
src = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${id}.jpg`
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ const CharacterConflictModal = (props: Props) => {
|
||||||
suffix = `${suffix}_0${element}`
|
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) {
|
function openChange(open: boolean) {
|
||||||
|
|
|
||||||
|
|
@ -364,7 +364,7 @@ const CharacterModal = ({
|
||||||
title={gridCharacter.object.name[locale]}
|
title={gridCharacter.object.name[locale]}
|
||||||
subtitle={t('modals.characters.title')}
|
subtitle={t('modals.characters.title')}
|
||||||
image={{
|
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],
|
alt: gridCharacter.object.name[locale],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,10 @@ const CharacterResult = (props: Props) => {
|
||||||
const character = props.data
|
const character = props.data
|
||||||
|
|
||||||
const characterUrl = () => {
|
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') {
|
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
|
return url
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,7 @@ const CharacterUnit = ({
|
||||||
suffix = `${suffix}_0${element}`
|
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)
|
setImageUrl(imgSrc)
|
||||||
|
|
|
||||||
234
components/common/Editor/index.module.scss
Normal file
234
components/common/Editor/index.module.scss
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: var(--toolbar-item-bg);
|
||||||
|
border-radius: $bubble-menu-item-corner;
|
||||||
|
color: var(--toolbar-item-text);
|
||||||
|
font-weight: $medium;
|
||||||
|
font-size: $font-small;
|
||||||
|
padding: $unit-half $unit;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--toolbar-item-bg-hover);
|
||||||
|
color: var(--toolbar-item-text-hover);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: var(--toolbar-item-bg-active);
|
||||||
|
color: var(--toolbar-item-text-active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
180
components/common/Editor/index.tsx
Normal file
180
components/common/Editor/index.tsx
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
import { ComponentProps, useCallback } 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 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 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) => {
|
||||||
|
const router = useRouter()
|
||||||
|
const locale = router.locale || 'en'
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
Link,
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!editor) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.wrapper}>
|
||||||
|
{editor && editable === true && (
|
||||||
|
<nav className={styles.toolbar}>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||||
|
className={editor.isActive('bold') ? styles.active : ''}
|
||||||
|
>
|
||||||
|
bold
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
|
className={editor.isActive('italic') ? styles.active : ''}
|
||||||
|
>
|
||||||
|
italic
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||||
|
className={editor.isActive('strike') ? styles.active : ''}
|
||||||
|
>
|
||||||
|
strike
|
||||||
|
</button>
|
||||||
|
<div className={styles.divider} />
|
||||||
|
<button
|
||||||
|
onClick={setLink}
|
||||||
|
className={editor.isActive('link') ? styles.active : ''}
|
||||||
|
>
|
||||||
|
+ link
|
||||||
|
</button>
|
||||||
|
<button onClick={addYoutubeVideo}>+ youtube</button>
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
<EditorContent editor={editor} />
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Editor.defaultProps = {
|
||||||
|
bound: false,
|
||||||
|
editable: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Editor
|
||||||
|
|
@ -14,7 +14,7 @@ interface Props
|
||||||
imageAlt?: string
|
imageAlt?: string
|
||||||
imageClass?: string
|
imageClass?: string
|
||||||
imageSrc?: string[]
|
imageSrc?: string[]
|
||||||
onValueChange: (value?: string) => void
|
onValueChange: (value?: string | number | readonly string[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputTableField = ({
|
const InputTableField = ({
|
||||||
|
|
@ -25,10 +25,12 @@ const InputTableField = ({
|
||||||
imageSrc,
|
imageSrc,
|
||||||
...props
|
...props
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [inputValue, setInputValue] = useState('')
|
const [inputValue, setInputValue] = useState<
|
||||||
|
string | number | readonly string[]
|
||||||
|
>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.value) setInputValue(`${props.value}`)
|
if (props.value !== undefined) setInputValue(props.value)
|
||||||
}, [props.value])
|
}, [props.value])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -54,7 +56,7 @@ const InputTableField = ({
|
||||||
<Input
|
<Input
|
||||||
className={props.className}
|
className={props.className}
|
||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
value={inputValue ? `${inputValue}` : ''}
|
value={inputValue !== undefined ? inputValue : ''}
|
||||||
step={1}
|
step={1}
|
||||||
tabIndex={props.tabIndex}
|
tabIndex={props.tabIndex}
|
||||||
bound={true}
|
bound={true}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
font-size: $font-tiny;
|
font-size: $font-tiny;
|
||||||
font-weight: $bold;
|
font-weight: $bold;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
min-width: 3rem;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: $unit-three-fourth ($unit * 1.5);
|
padding: $unit-three-fourth ($unit * 1.5);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
|
||||||
|
|
@ -199,14 +199,18 @@ const FilterModal = (props: Props) => {
|
||||||
setMinWeaponCount(value)
|
setMinWeaponCount(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMaxButtonsCountValueChange(value?: string) {
|
function handleMaxButtonsCountValueChange(
|
||||||
|
value?: string | number | readonly string[]
|
||||||
|
) {
|
||||||
if (!value) return
|
if (!value) return
|
||||||
setMaxButtonsCount(parseInt(value))
|
setMaxButtonsCount(value as number)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMaxTurnsCountValueChange(value?: string) {
|
function handleMaxTurnsCountValueChange(
|
||||||
|
value?: string | number | readonly string[]
|
||||||
|
) {
|
||||||
if (!value) return
|
if (!value) return
|
||||||
setMaxTurnsCount(parseInt(value))
|
setMaxTurnsCount(value as number)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNameQualityValueChange(value?: boolean) {
|
function handleNameQualityValueChange(value?: boolean) {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,13 @@ import classNames from 'classnames'
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
|
|
||||||
import Alert from '~components/common/Alert'
|
import Alert from '~components/common/Alert'
|
||||||
|
import Button from '~components/common/Button'
|
||||||
import { Dialog, DialogTrigger } from '~components/common/Dialog'
|
import { Dialog, DialogTrigger } from '~components/common/Dialog'
|
||||||
import DialogHeader from '~components/common/DialogHeader'
|
import DialogHeader from '~components/common/DialogHeader'
|
||||||
import DialogFooter from '~components/common/DialogFooter'
|
import DialogFooter from '~components/common/DialogFooter'
|
||||||
import DialogContent from '~components/common/DialogContent'
|
import DialogContent from '~components/common/DialogContent'
|
||||||
import Button from '~components/common/Button'
|
|
||||||
import DurationInput from '~components/common/DurationInput'
|
import DurationInput from '~components/common/DurationInput'
|
||||||
|
import Editor from '~components/common/Editor'
|
||||||
import Input from '~components/common/Input'
|
import Input from '~components/common/Input'
|
||||||
import InputTableField from '~components/common/InputTableField'
|
import InputTableField from '~components/common/InputTableField'
|
||||||
import RaidCombobox from '~components/raids/RaidCombobox'
|
import RaidCombobox from '~components/raids/RaidCombobox'
|
||||||
|
|
@ -24,6 +25,7 @@ import Textarea from '~components/common/Textarea'
|
||||||
import capitalizeFirstLetter from '~utils/capitalizeFirstLetter'
|
import capitalizeFirstLetter from '~utils/capitalizeFirstLetter'
|
||||||
import type { DetailsObject } from 'types'
|
import type { DetailsObject } from 'types'
|
||||||
import type { DialogProps } from '@radix-ui/react-dialog'
|
import type { DialogProps } from '@radix-ui/react-dialog'
|
||||||
|
import type { JSONContent } from '@tiptap/core'
|
||||||
|
|
||||||
import { appState } from '~utils/appState'
|
import { appState } from '~utils/appState'
|
||||||
|
|
||||||
|
|
@ -129,6 +131,10 @@ const EditPartyModal = ({
|
||||||
setErrors(newErrors)
|
setErrors(newErrors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleEditorUpdate(content: JSONContent) {
|
||||||
|
setDescription(JSON.stringify(content))
|
||||||
|
}
|
||||||
|
|
||||||
function handleChargeAttackChanged(checked: boolean) {
|
function handleChargeAttackChanged(checked: boolean) {
|
||||||
setChargeAttack(checked)
|
setChargeAttack(checked)
|
||||||
}
|
}
|
||||||
|
|
@ -153,22 +159,23 @@ const EditPartyModal = ({
|
||||||
if (!isNaN(value)) setClearTime(value)
|
if (!isNaN(value)) setClearTime(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTurnCountChanged(value?: string) {
|
function handleTurnCountChanged(value?: string | number | readonly string[]) {
|
||||||
if (!value) return
|
if (value === null || value === undefined) return
|
||||||
const numericalValue = parseInt(value)
|
setTurnCount(value as number)
|
||||||
if (!isNaN(numericalValue)) setTurnCount(numericalValue)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleButtonCountChanged(value?: string) {
|
function handleButtonCountChanged(
|
||||||
if (!value) return
|
value?: string | number | readonly string[]
|
||||||
const numericalValue = parseInt(value)
|
) {
|
||||||
if (!isNaN(numericalValue)) setButtonCount(numericalValue)
|
if (value === null || value === undefined) return
|
||||||
|
setButtonCount(value as number)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChainCountChanged(value?: string) {
|
function handleChainCountChanged(
|
||||||
if (!value) return
|
value?: string | number | readonly string[]
|
||||||
const numericalValue = parseInt(value)
|
) {
|
||||||
if (!isNaN(numericalValue)) setChainCount(numericalValue)
|
if (value === null || value === undefined) return
|
||||||
|
setChainCount(value as number)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTextAreaChanged(event: React.ChangeEvent<HTMLDivElement>) {
|
function handleTextAreaChanged(event: React.ChangeEvent<HTMLDivElement>) {
|
||||||
|
|
@ -291,7 +298,6 @@ const EditPartyModal = ({
|
||||||
function hasBeenModified() {
|
function hasBeenModified() {
|
||||||
const nameChanged =
|
const nameChanged =
|
||||||
name !== party.name && !(name === '' && party.name === undefined)
|
name !== party.name && !(name === '' && party.name === undefined)
|
||||||
|
|
||||||
const descriptionChanged =
|
const descriptionChanged =
|
||||||
description !== party.description &&
|
description !== party.description &&
|
||||||
!(description === '' && party.description === undefined)
|
!(description === '' && party.description === undefined)
|
||||||
|
|
@ -306,6 +312,21 @@ const EditPartyModal = ({
|
||||||
const buttonCountChanged = buttonCount !== party.buttonCount
|
const buttonCountChanged = buttonCount !== party.buttonCount
|
||||||
const chainCountChanged = chainCount !== party.chainCount
|
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 (
|
return (
|
||||||
nameChanged ||
|
nameChanged ||
|
||||||
descriptionChanged ||
|
descriptionChanged ||
|
||||||
|
|
@ -332,13 +353,12 @@ const EditPartyModal = ({
|
||||||
setFullAuto(party.fullAuto)
|
setFullAuto(party.fullAuto)
|
||||||
setChargeAttack(party.chargeAttack)
|
setChargeAttack(party.chargeAttack)
|
||||||
setClearTime(party.clearTime)
|
setClearTime(party.clearTime)
|
||||||
if (party.turnCount) setTurnCount(party.turnCount)
|
if (party.turnCount !== undefined) setTurnCount(party.turnCount)
|
||||||
if (party.buttonCount) setButtonCount(party.buttonCount)
|
if (party.buttonCount !== undefined) setButtonCount(party.buttonCount)
|
||||||
if (party.chainCount) setChainCount(party.chainCount)
|
if (party.chainCount !== undefined) setChainCount(party.chainCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateDetails(event: React.MouseEvent) {
|
async function updateDetails(event: React.MouseEvent) {
|
||||||
const descriptionValue = descriptionInput.current?.innerHTML
|
|
||||||
const details: DetailsObject = {
|
const details: DetailsObject = {
|
||||||
fullAuto: fullAuto,
|
fullAuto: fullAuto,
|
||||||
autoGuard: autoGuard,
|
autoGuard: autoGuard,
|
||||||
|
|
@ -349,7 +369,7 @@ const EditPartyModal = ({
|
||||||
turnCount: turnCount,
|
turnCount: turnCount,
|
||||||
chainCount: chainCount,
|
chainCount: chainCount,
|
||||||
name: name,
|
name: name,
|
||||||
description: descriptionValue,
|
description: description,
|
||||||
raid: raid,
|
raid: raid,
|
||||||
extra: extra,
|
extra: extra,
|
||||||
}
|
}
|
||||||
|
|
@ -457,6 +477,15 @@ const EditPartyModal = ({
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const editorField = (
|
||||||
|
<Editor
|
||||||
|
bound={true}
|
||||||
|
content={description}
|
||||||
|
editable={true}
|
||||||
|
onUpdate={handleEditorUpdate}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
const chargeAttackField = (
|
const chargeAttackField = (
|
||||||
<SwitchTableField
|
<SwitchTableField
|
||||||
name="charge_attack"
|
name="charge_attack"
|
||||||
|
|
@ -560,7 +589,7 @@ const EditPartyModal = ({
|
||||||
{nameField}
|
{nameField}
|
||||||
{raidField}
|
{raidField}
|
||||||
{extraNotice()}
|
{extraNotice()}
|
||||||
{descriptionField}
|
{editorField}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $unit-2x;
|
gap: $unit-2x;
|
||||||
margin: $unit-4x auto 0 auto;
|
margin: $unit-4x auto $unit-10x auto;
|
||||||
max-width: $grid-width;
|
max-width: $grid-width;
|
||||||
|
|
||||||
@include breakpoint(phone) {
|
@include breakpoint(phone) {
|
||||||
|
|
@ -22,8 +22,7 @@
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
margin: 0 auto $unit-2x;
|
margin: 0 auto $unit-2x;
|
||||||
margin-bottom: $unit-12x;
|
min-height: 20vh;
|
||||||
min-height: 10vh;
|
|
||||||
max-width: $unit * 94;
|
max-width: $unit * 94;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -32,22 +31,6 @@
|
||||||
padding: 0 $unit;
|
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 {
|
p {
|
||||||
font-size: $font-regular;
|
font-size: $font-regular;
|
||||||
line-height: $font-regular * 1.2;
|
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 {
|
.PartyInfo {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,8 @@ import React, { useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
import classNames from 'classnames'
|
|
||||||
import clonedeep from 'lodash.clonedeep'
|
import clonedeep from 'lodash.clonedeep'
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
import Linkify from 'react-linkify'
|
|
||||||
import LiteYouTubeEmbed from 'react-lite-youtube-embed'
|
|
||||||
import reactStringReplace from 'react-string-replace'
|
|
||||||
|
|
||||||
import Button from '~components/common/Button'
|
import Button from '~components/common/Button'
|
||||||
import SegmentedControl from '~components/common/SegmentedControl'
|
import SegmentedControl from '~components/common/SegmentedControl'
|
||||||
|
|
@ -27,6 +23,7 @@ import type { DetailsObject } from 'types'
|
||||||
import RemixIcon from '~public/icons/Remix.svg'
|
import RemixIcon from '~public/icons/Remix.svg'
|
||||||
import EditIcon from '~public/icons/Edit.svg'
|
import EditIcon from '~public/icons/Edit.svg'
|
||||||
import styles from './index.module.scss'
|
import styles from './index.module.scss'
|
||||||
|
import Editor from '~components/common/Editor'
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -55,42 +52,39 @@ const PartyFooter = (props: Props) => {
|
||||||
|
|
||||||
// State: Data
|
// State: Data
|
||||||
const [remixes, setRemixes] = useState<Party[]>([])
|
const [remixes, setRemixes] = useState<Party[]>([])
|
||||||
const [embeddedDescription, setEmbeddedDescription] =
|
const [sanitizedDescription, setSanitizedDescription] = useState('')
|
||||||
useState<React.ReactNode>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Extract the video IDs from the description
|
|
||||||
if (partySnapshot.description) {
|
if (partySnapshot.description) {
|
||||||
const videoIds = extractYoutubeVideoIds(partySnapshot.description)
|
const purified = DOMPurify.sanitize(partySnapshot.description)
|
||||||
|
setSanitizedDescription(purified)
|
||||||
// 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)
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
setEmbeddedDescription('')
|
setSanitizedDescription('')
|
||||||
}
|
}
|
||||||
}, [partySnapshot.description])
|
}, [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) {
|
async function fetchYoutubeData(videoId: string) {
|
||||||
return await youtube
|
return await youtube
|
||||||
.getVideoById(videoId, { maxResults: 1 })
|
.getVideoById(videoId, { maxResults: 1 })
|
||||||
|
|
@ -213,14 +207,14 @@ const PartyFooter = (props: Props) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const descriptionSection = (
|
const descriptionSection = (
|
||||||
<section className={styles.description}>
|
<>
|
||||||
{partySnapshot &&
|
{partySnapshot &&
|
||||||
partySnapshot.description &&
|
partySnapshot.description &&
|
||||||
partySnapshot.description.length > 0 && (
|
partySnapshot.description.length > 0 && (
|
||||||
<Linkify>{embeddedDescription}</Linkify>
|
<Editor content={appState.party.description} />
|
||||||
)}
|
)}
|
||||||
{(!partySnapshot || !partySnapshot.description) && (
|
{(!partySnapshot || !partySnapshot.description) && (
|
||||||
<div className={styles.noDescription}>
|
<section className={styles.noDescription}>
|
||||||
<h3>{t('footer.description.empty')}</h3>
|
<h3>{t('footer.description.empty')}</h3>
|
||||||
{props.editable && (
|
{props.editable && (
|
||||||
<EditPartyModal
|
<EditPartyModal
|
||||||
|
|
@ -236,9 +230,9 @@ const PartyFooter = (props: Props) => {
|
||||||
/>
|
/>
|
||||||
</EditPartyModal>
|
</EditPartyModal>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
const remixesSection = (
|
const remixesSection = (
|
||||||
|
|
|
||||||
|
|
@ -95,9 +95,9 @@ const CharacterRep = (props: Props) => {
|
||||||
else if (gridCharacter.uncap_level > 2) suffix = '02'
|
else if (gridCharacter.uncap_level > 2) suffix = '02'
|
||||||
|
|
||||||
if (character.element == 0) {
|
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 {
|
} 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`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})
|
||||||
916
package-lock.json
generated
916
package-lock.json
generated
File diff suppressed because it is too large
Load diff
16
package.json
16
package.json
|
|
@ -26,11 +26,20 @@
|
||||||
"@radix-ui/react-toggle-group": "^1.0.1",
|
"@radix-ui/react-toggle-group": "^1.0.1",
|
||||||
"@radix-ui/react-tooltip": "^1.0.3",
|
"@radix-ui/react-tooltip": "^1.0.3",
|
||||||
"@svgr/webpack": "^6.2.0",
|
"@svgr/webpack": "^6.2.0",
|
||||||
|
"@tiptap/extension-bubble-menu": "^2.0.3",
|
||||||
|
"@tiptap/extension-link": "^2.0.3",
|
||||||
|
"@tiptap/extension-mention": "^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",
|
"axios": "^0.25.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
"cookies-next": "^2.1.1",
|
"cookies-next": "^2.1.1",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
|
"dompurify": "^3.0.4",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fix-date": "^1.1.6",
|
"fix-date": "^1.1.6",
|
||||||
"i18next": "^21.6.13",
|
"i18next": "^21.6.13",
|
||||||
|
|
@ -57,11 +66,17 @@
|
||||||
"resolve-url-loader": "^5.0.0",
|
"resolve-url-loader": "^5.0.0",
|
||||||
"sanitize-html": "^2.8.1",
|
"sanitize-html": "^2.8.1",
|
||||||
"sass": "^1.61.0",
|
"sass": "^1.61.0",
|
||||||
|
"tippy.js": "^6.3.7",
|
||||||
"usehooks-ts": "^2.9.1",
|
"usehooks-ts": "^2.9.1",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"valtio": "^1.3.0",
|
"valtio": "^1.3.0",
|
||||||
"youtube-api-v3-wrapper": "^2.3.0"
|
"youtube-api-v3-wrapper": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@tiptap/extension-mention": {
|
||||||
|
"@tiptap/suggestion": "2.0.0-beta.91"
|
||||||
|
}
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@storybook/addon-essentials": "latest",
|
"@storybook/addon-essentials": "latest",
|
||||||
"@storybook/addon-interactions": "latest",
|
"@storybook/addon-interactions": "latest",
|
||||||
|
|
@ -72,6 +87,7 @@
|
||||||
"@storybook/nextjs": "latest",
|
"@storybook/nextjs": "latest",
|
||||||
"@storybook/react": "latest",
|
"@storybook/react": "latest",
|
||||||
"@storybook/testing-library": "latest",
|
"@storybook/testing-library": "latest",
|
||||||
|
"@types/dompurify": "^3.0.2",
|
||||||
"@types/lodash.clonedeep": "^4.5.6",
|
"@types/lodash.clonedeep": "^4.5.6",
|
||||||
"@types/lodash.debounce": "^4.0.6",
|
"@types/lodash.debounce": "^4.0.6",
|
||||||
"@types/node": "17.0.11",
|
"@types/node": "17.0.11",
|
||||||
|
|
|
||||||
|
|
@ -487,6 +487,7 @@
|
||||||
"start_typing": "Start typing the name of a {{object}}",
|
"start_typing": "Start typing the name of a {{object}}",
|
||||||
"min_length": "Type at least 3 characters",
|
"min_length": "Type at least 3 characters",
|
||||||
"no_results": "No results found for '{{query}}'",
|
"no_results": "No results found for '{{query}}'",
|
||||||
|
"no_results_generic": "No results found",
|
||||||
"end_results": "No more results"
|
"end_results": "No more results"
|
||||||
},
|
},
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
|
||||||
|
|
@ -485,6 +485,7 @@
|
||||||
"start_typing": "{{object}}名を入力してください",
|
"start_typing": "{{object}}名を入力してください",
|
||||||
"min_length": "3文字以上を入力してください",
|
"min_length": "3文字以上を入力してください",
|
||||||
"no_results": "'{{query}}'の検索結果が見つかりませんでした",
|
"no_results": "'{{query}}'の検索結果が見つかりませんでした",
|
||||||
|
"no_results_generic": "検索結果が見つかりませんでした",
|
||||||
"end_results": "検索結果これ以上ありません"
|
"end_results": "検索結果これ以上ありません"
|
||||||
},
|
},
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
html {
|
html {
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
font-size: 62.5%;
|
font-size: 62.5%;
|
||||||
height: 100%;
|
// height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,25 @@
|
||||||
--menu-bg-item-hover: #{$menu--item--bg--light--hover};
|
--menu-bg-item-hover: #{$menu--item--bg--light--hover};
|
||||||
--menu-text-hover: #{$menu--text--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};
|
||||||
|
|
||||||
// Light - Placeholders
|
// Light - Placeholders
|
||||||
--placeholder-bound-bg: #{$placeholder--bound--bg--light};
|
--placeholder-bound-bg: #{$placeholder--bound--bg--light};
|
||||||
--placeholder-bound-bg-hover: #{$placeholder--bound--bg--light--hover};
|
--placeholder-bound-bg-hover: #{$placeholder--bound--bg--light--hover};
|
||||||
|
|
@ -139,36 +158,48 @@
|
||||||
--wind-text: #{$wind--text--light};
|
--wind-text: #{$wind--text--light};
|
||||||
--wind-raid-text: #{$wind--text--raid--light};
|
--wind-raid-text: #{$wind--text--raid--light};
|
||||||
--wind-text-hover: #{$wind--text--hover--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: #{$fire--bg--light};
|
||||||
--fire-bg-hover: #{$fire--bg--hover--light};
|
--fire-bg-hover: #{$fire--bg--hover--light};
|
||||||
--fire-text: #{$fire--text--light};
|
--fire-text: #{$fire--text--light};
|
||||||
--fire-raid-text: #{$fire--text--raid--light};
|
--fire-raid-text: #{$fire--text--raid--light};
|
||||||
--fire-text-hover: #{$fire--text--hover--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: #{$water--bg--light};
|
||||||
--water-bg-hover: #{$water--bg--hover--light};
|
--water-bg-hover: #{$water--bg--hover--light};
|
||||||
--water-text: #{$water--text--light};
|
--water-text: #{$water--text--light};
|
||||||
--water-raid-text: #{$water--text--raid--light};
|
--water-raid-text: #{$water--text--raid--light};
|
||||||
--water-text-hover: #{$water--text--hover--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: #{$earth--bg--light};
|
||||||
--earth-bg-hover: #{$earth--bg--hover--light};
|
--earth-bg-hover: #{$earth--bg--hover--light};
|
||||||
--earth-text: #{$earth--text--light};
|
--earth-text: #{$earth--text--light};
|
||||||
--earth-raid-text: #{$earth--text--raid--light};
|
--earth-raid-text: #{$earth--text--raid--light};
|
||||||
--earth-text-hover: #{$earth--text--hover--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: #{$dark--bg--light};
|
||||||
--dark-bg-hover: #{$dark--bg--hover--light};
|
--dark-bg-hover: #{$dark--bg--hover--light};
|
||||||
--dark-text: #{$dark--text--light};
|
--dark-text: #{$dark--text--light};
|
||||||
--dark-raid-text: #{$dark--text--raid--light};
|
--dark-raid-text: #{$dark--text--raid--light};
|
||||||
--dark-text-hover: #{$dark--text--hover--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: #{$light--bg--light};
|
||||||
--light-bg-hover: #{$light--bg--hover--light};
|
--light-bg-hover: #{$light--bg--hover--light};
|
||||||
--light-text: #{$light--text--light};
|
--light-text: #{$light--text--light};
|
||||||
--light-raid-text: #{$light--text--raid--light};
|
--light-raid-text: #{$light--text--raid--light};
|
||||||
--light-text-hover: #{$light--text--hover--light};
|
--light-text-hover: #{$light--text--hover--light};
|
||||||
|
--light-shadow: #{$light--shadow--light};
|
||||||
|
--light-shadow-hover: #{$light--shadow--light--hover};
|
||||||
|
|
||||||
// Gradients
|
// Gradients
|
||||||
--hero-gradient: #{$hero--gradient--light};
|
--hero-gradient: #{$hero--gradient--light};
|
||||||
|
|
@ -221,6 +252,25 @@
|
||||||
--menu-bg-item-hover: #{$menu--item--bg--dark--hover};
|
--menu-bg-item-hover: #{$menu--item--bg--dark--hover};
|
||||||
--menu-text-hover: #{$menu--text--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 - Placeholders
|
// Dark - Placeholders
|
||||||
--placeholder-bound-bg: #{$placeholder--bound--bg--dark};
|
--placeholder-bound-bg: #{$placeholder--bound--bg--dark};
|
||||||
--placeholder-bound-bg-hover: #{$placeholder--bound--bg--dark--hover};
|
--placeholder-bound-bg-hover: #{$placeholder--bound--bg--dark--hover};
|
||||||
|
|
@ -316,36 +366,48 @@
|
||||||
--wind-text: #{$wind--text--dark};
|
--wind-text: #{$wind--text--dark};
|
||||||
--wind-raid-text: #{$wind--text--raid--dark};
|
--wind-raid-text: #{$wind--text--raid--dark};
|
||||||
--wind-text-hover: #{$wind--text--hover--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: #{$fire--bg--dark};
|
||||||
--fire-bg-hover: #{$fire--bg--hover--dark};
|
--fire-bg-hover: #{$fire--bg--hover--dark};
|
||||||
--fire-text: #{$fire--text--dark};
|
--fire-text: #{$fire--text--dark};
|
||||||
--fire-raid-text: #{$fire--text--raid--dark};
|
--fire-raid-text: #{$fire--text--raid--dark};
|
||||||
--fire-text-hover: #{$fire--text--hover--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: #{$water--bg--dark};
|
||||||
--water-bg-hover: #{$water--bg--hover--dark};
|
--water-bg-hover: #{$water--bg--hover--dark};
|
||||||
--water-text: #{$water--text--dark};
|
--water-text: #{$water--text--dark};
|
||||||
--water-raid-text: #{$water--text--raid--dark};
|
--water-raid-text: #{$water--text--raid--dark};
|
||||||
--water-text-hover: #{$water--text--hover--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: #{$earth--bg--dark};
|
||||||
--earth-bg-hover: #{$earth--bg--hover--dark};
|
--earth-bg-hover: #{$earth--bg--hover--dark};
|
||||||
--earth-text: #{$earth--text--dark};
|
--earth-text: #{$earth--text--dark};
|
||||||
--earth-raid-text: #{$earth--text--raid--dark};
|
--earth-raid-text: #{$earth--text--raid--dark};
|
||||||
--earth-text-hover: #{$earth--text--hover--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: #{$dark--bg--dark};
|
||||||
--dark-bg-hover: #{$dark--bg--hover--dark};
|
--dark-bg-hover: #{$dark--bg--hover--dark};
|
||||||
--dark-text: #{$dark--text--dark};
|
--dark-text: #{$dark--text--dark};
|
||||||
--dark-raid-text: #{$dark--text--raid--dark};
|
--dark-raid-text: #{$dark--text--raid--dark};
|
||||||
--dark-text-hover: #{$dark--text--hover--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: #{$light--bg--dark};
|
||||||
--light-bg-hover: #{$light--bg--hover--dark};
|
--light-bg-hover: #{$light--bg--hover--dark};
|
||||||
--light-text: #{$light--text--dark};
|
--light-text: #{$light--text--dark};
|
||||||
--light-raid-text: #{$light--text--raid--dark};
|
--light-raid-text: #{$light--text--raid--dark};
|
||||||
--light-text-hover: #{$light--text--hover--dark};
|
--light-text-hover: #{$light--text--hover--dark};
|
||||||
|
--light-shadow: #{$light--shadow--dark};
|
||||||
|
--light-shadow-hover: #{$light--shadow--dark--hover};
|
||||||
|
|
||||||
// Gradients
|
// Gradients
|
||||||
--hero-gradient: #{$hero--gradient--dark};
|
--hero-gradient: #{$hero--gradient--dark};
|
||||||
|
|
|
||||||
|
|
@ -336,6 +336,53 @@ $pill--bg--dark--hover: $grey-50;
|
||||||
$pill--text--dark: $grey-100;
|
$pill--text--dark: $grey-100;
|
||||||
$pill--text--dark--hover: $grey-00;
|
$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: Element Toggle
|
// Color Definitions: Element Toggle
|
||||||
$toggle--bg--light: $grey-90;
|
$toggle--bg--light: $grey-90;
|
||||||
$toggle--bg--dark: $grey-15;
|
$toggle--bg--dark: $grey-15;
|
||||||
|
|
@ -390,6 +437,12 @@ $wind--text--raid--dark: $wind-bg-10;
|
||||||
$wind--text--hover--light: $wind-text-00;
|
$wind--text--hover--light: $wind-text-00;
|
||||||
$wind--text--hover--dark: $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
|
// Color Definitions: Element / Fire
|
||||||
$fire--bg--light: $fire-bg-10;
|
$fire--bg--light: $fire-bg-10;
|
||||||
$fire--bg--dark: $fire-bg-10;
|
$fire--bg--dark: $fire-bg-10;
|
||||||
|
|
@ -406,6 +459,12 @@ $fire--text--raid--dark: $fire-bg-10;
|
||||||
$fire--text--hover--light: $fire-text-00;
|
$fire--text--hover--light: $fire-text-00;
|
||||||
$fire--text--hover--dark: $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
|
// Color Definitions: Element / Water
|
||||||
$water--bg--light: $water-bg-10;
|
$water--bg--light: $water-bg-10;
|
||||||
$water--bg--dark: $water-bg-10;
|
$water--bg--dark: $water-bg-10;
|
||||||
|
|
@ -422,6 +481,12 @@ $water--text--raid--dark: $water-bg-10;
|
||||||
$water--text--hover--light: $water-text-00;
|
$water--text--hover--light: $water-text-00;
|
||||||
$water--text--hover--dark: $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
|
// Color Definitions: Element / Earth
|
||||||
$earth--bg--light: $earth-bg-10;
|
$earth--bg--light: $earth-bg-10;
|
||||||
$earth--bg--dark: $earth-bg-10;
|
$earth--bg--dark: $earth-bg-10;
|
||||||
|
|
@ -438,6 +503,12 @@ $earth--text--raid--dark: $earth-bg-10;
|
||||||
$earth--text--hover--light: $earth-text-00;
|
$earth--text--hover--light: $earth-text-00;
|
||||||
$earth--text--hover--dark: $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
|
// Color Definitions: Element / Dark
|
||||||
$dark--bg--light: $dark-bg-10;
|
$dark--bg--light: $dark-bg-10;
|
||||||
$dark--bg--dark: $dark-bg-10;
|
$dark--bg--dark: $dark-bg-10;
|
||||||
|
|
@ -454,6 +525,12 @@ $dark--text--raid--dark: $dark-bg-10;
|
||||||
$dark--text--hover--light: $dark-text-00;
|
$dark--text--hover--light: $dark-text-00;
|
||||||
$dark--text--hover--dark: $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
|
// Color Definitions: Element / Light
|
||||||
$light--bg--light: $light-bg-10;
|
$light--bg--light: $light-bg-10;
|
||||||
$light--bg--dark: $light-bg-10;
|
$light--bg--dark: $light-bg-10;
|
||||||
|
|
@ -470,6 +547,12 @@ $light--text--raid--dark: $light-bg-10;
|
||||||
$light--text--hover--light: $light-text-00;
|
$light--text--hover--light: $light-text-00;
|
||||||
$light--text--hover--dark: $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
|
// Font-weight
|
||||||
$normal: 400;
|
$normal: 400;
|
||||||
$medium: 500;
|
$medium: 500;
|
||||||
|
|
@ -497,6 +580,9 @@ $input-corner: $unit;
|
||||||
$item-corner: $unit;
|
$item-corner: $unit;
|
||||||
$item-corner-small: $unit-half;
|
$item-corner-small: $unit-half;
|
||||||
|
|
||||||
|
$bubble-menu-corner: $unit;
|
||||||
|
$bubble-menu-item-corner: $unit-half * 1.5;
|
||||||
|
|
||||||
// Shadows
|
// Shadows
|
||||||
$hover-stroke: 1px solid rgba(0, 0, 0, 0.1);
|
$hover-stroke: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
$hover-shadow: rgba(0, 0, 0, 0.08) 0px 0px 14px;
|
$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
|
||||||
|
}
|
||||||
|
|
@ -70,6 +70,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) {
|
check(resource: string, value: string) {
|
||||||
const resourceUrl = `${this.url}/check/${resource}`
|
const resourceUrl = `${this.url}/check/${resource}`
|
||||||
return axios.post(resourceUrl, {
|
return axios.post(resourceUrl, {
|
||||||
|
|
|
||||||
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