Merge branch 'main' into release-notes-1.2.0

This commit is contained in:
Justin Edmund 2023-08-25 16:52:10 -07:00
commit b57d0de20e
28 changed files with 1343 additions and 163 deletions

View file

@ -270,7 +270,7 @@ const CharacterModal = ({
<Alert
message={
<span>
<Trans i18nKey="alerts.unsaved_changes.object">
<Trans i18nKey="alert.unsaved_changes.object">
You will lose all changes to{' '}
<strong>{{ objectName: gridCharacter.object.name[locale] }}</strong>{' '}
if you continue.
@ -281,9 +281,9 @@ const CharacterModal = ({
</span>
}
open={alertOpen}
primaryActionText="Close"
primaryActionText={t('alert.unsaved_changes.buttons.confirm')}
primaryAction={close}
cancelActionText="Nevermind"
cancelActionText={t('alert.unsaved_changes.buttons.cancel')}
cancelAction={() => setAlertOpen(false)}
/>
)

View file

@ -29,7 +29,7 @@
flex-direction: column;
gap: $unit-2x;
min-width: 20vw;
max-width: 30vw;
max-width: 32vw;
padding: $unit * 4;
@include breakpoint(tablet) {

View file

@ -46,6 +46,10 @@
flex-grow: 1;
}
&.no-shrink {
flex-shrink: 0;
}
&.blended {
background: transparent;
}
@ -304,6 +308,15 @@
}
}
&.notice {
background-color: var(--notice-button-bg);
color: var(--notice-button-text);
&:hover {
background-color: var(--notice-button-bg-hover);
}
}
&.destructive {
background: $error;
color: white;

View file

@ -1,5 +1,4 @@
import React, { useEffect, useRef, useState } from 'react'
import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio'
import { Trans, useTranslation } from 'react-i18next'
import classNames from 'classnames'
@ -20,7 +19,6 @@ import SegmentedControl from '~components/common/SegmentedControl'
import Segment from '~components/common/Segment'
import SwitchTableField from '~components/common/SwitchTableField'
import TableField from '~components/common/TableField'
import Textarea from '~components/common/Textarea'
import capitalizeFirstLetter from '~utils/capitalizeFirstLetter'
import type { DetailsObject } from 'types'
@ -384,7 +382,7 @@ const EditPartyModal = ({
<Alert
message={
<span>
<Trans i18nKey="alerts.unsaved_changes.party">
<Trans i18nKey="alert.unsaved_changes.party">
You will lose all changes to your party{' '}
<strong>
{{
@ -399,9 +397,9 @@ const EditPartyModal = ({
</span>
}
open={alertOpen}
primaryActionText="Close"
primaryActionText={t('alert.unsaved_changes.buttons.confirm')}
primaryAction={close}
cancelActionText="Nevermind"
cancelActionText={t('alert.unsaved_changes.buttons.cancel')}
cancelAction={() => setAlertOpen(false)}
/>
)

View file

@ -169,6 +169,7 @@ const Party = (props: Props) => {
if (details.guidebook1_id) payload.guidebook1_id = details.guidebook1_id
if (details.guidebook2_id) payload.guidebook2_id = details.guidebook2_id
if (details.guidebook3_id) payload.guidebook3_id = details.guidebook3_id
if (details.visibility) payload.visibility = details.visibility
if (getLocalId()) payload.local_id = getLocalId()
if (Object.keys(payload).length >= 1) return { party: payload }
@ -292,6 +293,7 @@ const Party = (props: Props) => {
appState.party.favorited = team.favorited
appState.party.remix = team.remix
appState.party.remixes = team.remixes
appState.party.visibility = team.visibility
appState.party.sourceParty = team.source_party
appState.party.created_at = team.created_at
appState.party.updated_at = team.updated_at

View file

@ -1,5 +1,5 @@
// Libraries
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next'
@ -33,12 +33,14 @@ interface Props {
editable: boolean
deleteTeamCallback: () => void
remixTeamCallback: () => void
teamVisibilityCallback: () => void
}
const PartyDropdown = ({
editable,
deleteTeamCallback,
remixTeamCallback,
teamVisibilityCallback,
}: Props) => {
// Localization
const { t } = useTranslation('common')
@ -81,6 +83,11 @@ const PartyDropdown = ({
// Methods: Event handlers
// Dialogs / Visibility
function visibilityCallback() {
teamVisibilityCallback()
}
// Alerts / Delete team
function openDeleteTeamAlert() {
setDeleteAlertOpen(true)
@ -125,6 +132,9 @@ const PartyDropdown = ({
const items = (
<>
<DropdownMenuGroup>
<DropdownMenuItem onClick={visibilityCallback}>
<span>{t('dropdown.party.visibility')}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={copyToClipboard}>
<span>{t('dropdown.party.copy')}</span>
</DropdownMenuItem>

View file

@ -260,16 +260,7 @@ const PartyFooter = (props: Props) => {
return partySnapshot?.remixes.map((party, i) => {
return (
<GridRep
id={party.id}
shortcode={party.shortcode}
name={party.name}
createdAt={new Date(party.created_at)}
raid={party.raid}
grid={party.weapons}
user={party.user}
favorited={party.favorited}
fullAuto={party.full_auto}
autoGuard={party.auto_guard}
party={party}
key={`party-${i}`}
loading={false}
onClick={goTo}

View file

@ -18,6 +18,58 @@
}
}
.notice {
align-items: center;
background: var(--notice-bg);
border-radius: $card-corner;
display: flex;
gap: $unit-2x;
font-size: $font-regular;
padding: $unit-4x;
overflow: hidden;
@include breakpoint(small-tablet) {
flex-direction: column;
gap: $unit;
padding: $unit-2x;
}
p {
color: var(--notice-text);
flex-grow: 1;
}
.icon {
align-items: center;
background-color: var(--notice-button-bg);
border-radius: $full-corner;
display: flex;
justify-content: center;
height: $unit-6x;
width: $unit-6x;
flex-shrink: 0;
svg {
fill: var(--notice-text);
width: $unit-3x;
height: $unit-3x;
}
}
.buttons {
justify-content: flex-end;
display: flex;
flex-shrink: 0;
gap: $unit;
@include breakpoint(small-tablet) {
flex-direction: column;
justify-content: center;
width: 100%;
}
}
}
.details {
box-sizing: border-box;
display: block;

View file

@ -19,10 +19,14 @@ import { formatTimeAgo } from '~utils/timeAgo'
import RemixTeamAlert from '~components/dialogs/RemixTeamAlert'
import RemixedToast from '~components/toasts/RemixedToast'
import PartyVisibilityDialog from '~components/party/PartyVisibilityDialog'
import UrlCopiedToast from '~components/toasts/UrlCopiedToast'
import EditIcon from '~public/icons/Edit.svg'
import RemixIcon from '~public/icons/Remix.svg'
import SaveIcon from '~public/icons/Save.svg'
import PrivateIcon from '~public/icons/Private.svg'
import UnlistedIcon from '~public/icons/Unlisted.svg'
import type { DetailsObject } from 'types'
@ -50,8 +54,10 @@ const PartyHeader = (props: Props) => {
// State: Component
const [detailsOpen, setDetailsOpen] = useState(false)
const [copyToastOpen, setCopyToastOpen] = useState(false)
const [remixAlertOpen, setRemixAlertOpen] = useState(false)
const [remixToastOpen, setRemixToastOpen] = useState(false)
const [visibilityDialogOpen, setVisibilityDialogOpen] = useState(false)
const userClass = classNames({
[styles.user]: true,
@ -122,12 +128,29 @@ const PartyHeader = (props: Props) => {
setDetailsOpen(open)
}
// Dialogs: Visibility
function visibilityDialogCallback() {
setVisibilityDialogOpen(true)
}
function handleVisibilityDialogChange(open: boolean) {
setVisibilityDialogOpen(open)
}
// Actions: Remix team
function remixTeamCallback() {
setRemixToastOpen(true)
props.remixCallback()
}
// Actions: Copy URL
function copyToClipboard() {
if (router.asPath.split('/')[1] === 'p') {
navigator.clipboard.writeText(window.location.href)
setCopyToastOpen(true)
}
}
// Alerts: Remix team
function openRemixTeamAlert() {
setRemixAlertOpen(true)
@ -146,6 +169,15 @@ const PartyHeader = (props: Props) => {
setRemixToastOpen(false)
}
// Toasts / Copy URL
function handleCopyToastOpenChanged(open: boolean) {
setCopyToastOpen(!open)
}
function handleCopyToastCloseClicked() {
setCopyToastOpen(false)
}
// Rendering
const userBlock = (username?: string, picture?: string, element?: string) => {
@ -298,6 +330,50 @@ const PartyHeader = (props: Props) => {
)
}
// Render: Notice
const unlistedNotice = (
<div className={styles.notice}>
<div className={styles.icon}>
<UnlistedIcon />
</div>
<p>{t('party.notices.unlisted')}</p>
<div className={styles.buttons}>
<Button
bound={true}
className="notice no-shrink"
key="copy_link"
text={t('party.notices.buttons.copy_link')}
onClick={copyToClipboard}
/>
<Button
bound={true}
className="notice no-shrink"
key="change_visibility"
text={t('party.notices.buttons.change_visibility')}
onClick={() => handleVisibilityDialogChange(true)}
/>
</div>
</div>
)
const privateNotice = (
<div className={styles.notice}>
<div className={styles.icon}>
<PrivateIcon />
</div>
<p>{t('party.notices.private')}</p>
<div className={styles.buttons}>
<Button
bound={true}
className="notice"
key="change_visibility"
text={t('party.notices.buttons.change_visibility')}
onClick={() => handleVisibilityDialogChange(true)}
/>
</div>
</div>
)
// Render: Buttons
const saveButton = () => {
return (
@ -358,6 +434,8 @@ const PartyHeader = (props: Props) => {
return (
<>
<header className={styles.wrapper}>
{party.visibility == 2 && unlistedNotice}
{party.visibility == 3 && privateNotice}
<section className={styles.info}>
<div className={styles.left}>
<div className={styles.header}>
@ -399,6 +477,7 @@ const PartyHeader = (props: Props) => {
editable={props.editable}
deleteTeamCallback={props.deleteCallback}
remixTeamCallback={props.remixCallback}
teamVisibilityCallback={visibilityDialogCallback}
/>
)}
</div>
@ -412,6 +491,13 @@ const PartyHeader = (props: Props) => {
<section className={styles.tokens}>{renderTokens()}</section>
</header>
<PartyVisibilityDialog
open={visibilityDialogOpen}
value={party.visibility as 1 | 2 | 3}
onOpenChange={handleVisibilityDialogChange}
updateParty={props.updateCallback}
/>
<RemixTeamAlert
creator={props.editable}
name={partySnapshot.name ? partySnapshot.name : t('no_title')}
@ -426,6 +512,12 @@ const PartyHeader = (props: Props) => {
onOpenChange={handleRemixToastOpenChanged}
onCloseClick={handleRemixToastCloseClicked}
/>
<UrlCopiedToast
open={copyToastOpen}
onOpenChange={handleCopyToastOpenChanged}
onCloseClick={handleCopyToastCloseClicked}
/>
</>
)
}

View file

@ -0,0 +1,83 @@
.content {
display: flex;
flex-direction: column;
gap: $unit-4x;
padding: 0 $unit-4x $unit-2x;
.description {
color: var(--text-primary);
font-size: $font-regular;
}
.radioGroup {
display: flex;
flex-direction: column;
gap: $unit-2x;
.radioSet {
display: flex;
gap: $unit;
.radioItem {
align-items: center;
background: var(--radio-button-bg);
border-radius: $full-corner;
border: none;
display: flex;
border: 2px solid transparent;
box-sizing: border-box;
justify-content: center;
height: $unit-4x;
width: $unit-4x;
min-height: $unit-4x;
min-width: $unit-4x;
&:focus {
outline: 2px solid var(--radio-active-bg);
&:hover {
outline-color: var(--radio-active-bg-hover);
}
}
[data-state='checked'] {
background-color: var(--radio-active-bg);
border-radius: $full-corner;
display: block;
height: $unit-2x;
width: $unit-2x;
}
&[data-state='checked']:hover [data-state='checked'] {
background-color: var(--radio-active-bg-hover);
}
&:hover {
background: var(--radio-button-bg-hover);
cursor: pointer;
}
}
label {
display: flex;
flex-direction: column;
gap: $unit-half;
&:hover {
cursor: pointer;
}
h4 {
color: var(--text-primary);
font-size: $font-regular;
font-weight: $bold;
}
p {
color: var(--text-tertiary);
font-size: $font-small;
}
}
}
}
}

View file

@ -0,0 +1,303 @@
import React, { useEffect, useRef, useState } from 'react'
import { useSnapshot } from 'valtio'
import { useTranslation } from 'react-i18next'
import debounce from 'lodash.debounce'
import * as RadioGroup from '@radix-ui/react-radio-group'
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 type { DetailsObject } from 'types'
import type { DialogProps } from '@radix-ui/react-dialog'
import { appState } from '~utils/appState'
import styles from './index.module.scss'
interface Props extends DialogProps {
open: boolean
value: 1 | 2 | 3
onOpenChange?: (open: boolean) => void
updateParty: (details: DetailsObject) => Promise<any>
}
const EditPartyModal = ({
open,
value,
updateParty,
onOpenChange,
...props
}: Props) => {
// Set up translation
const { t } = useTranslation('common')
// Set up reactive state
const { party } = useSnapshot(appState)
// Refs
const headerRef = React.createRef<HTMLDivElement>()
const topContainerRef = React.createRef<HTMLDivElement>()
const footerRef = React.createRef<HTMLDivElement>()
const radioItemRef = [
React.createRef<HTMLButtonElement>(),
React.createRef<HTMLButtonElement>(),
React.createRef<HTMLButtonElement>(),
]
// States: Component
const [alertOpen, setAlertOpen] = useState(false)
const [errors, setErrors] = useState<{ [key: string]: string }>({
name: '',
description: '',
})
// States: Data
const [visibility, setVisibility] = useState(1)
// Hooks
useEffect(() => {
setVisibility(party.visibility)
}, [value])
// Methods: Event handlers (Dialog)
function handleOpenChange() {
if (hasBeenModified() && open) {
setAlertOpen(true)
} else if (!hasBeenModified() && open) {
close()
} else {
if (onOpenChange) onOpenChange(true)
}
}
function close() {
setAlertOpen(false)
setVisibility(party.visibility)
if (onOpenChange) onOpenChange(false)
}
function onEscapeKeyDown(event: KeyboardEvent) {
event.preventDefault()
handleOpenChange()
}
function onOpenAutoFocus(event: Event) {
event.preventDefault()
}
// Methods: Event handlers (Fields)
function handleValueChange(value: string) {
const newVisibility = parseInt(value)
setVisibility(newVisibility)
}
// Handlers
function handleScroll(event: React.UIEvent<HTMLDivElement, UIEvent>) {
const scrollTop = event.currentTarget.scrollTop
const scrollHeight = event.currentTarget.scrollHeight
const clientHeight = event.currentTarget.clientHeight
if (topContainerRef && topContainerRef.current)
manipulateHeaderShadow(topContainerRef.current, scrollTop)
if (footerRef && footerRef.current)
manipulateFooterShadow(
footerRef.current,
scrollTop,
scrollHeight,
clientHeight
)
}
function manipulateHeaderShadow(header: HTMLDivElement, scrollTop: number) {
const boxShadowBase = '0 2px 8px'
const maxValue = 50
if (scrollTop >= 0) {
const input = scrollTop > maxValue ? maxValue : scrollTop
const boxShadowOpacity = mapRange(input, 0, maxValue, 0.0, 0.16)
const borderOpacity = mapRange(input, 0, maxValue, 0.0, 0.24)
header.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, ${boxShadowOpacity})`
header.style.borderBottomColor = `rgba(0, 0, 0, ${borderOpacity})`
}
}
function manipulateFooterShadow(
footer: HTMLDivElement,
scrollTop: number,
scrollHeight: number,
clientHeight: number
) {
const boxShadowBase = '0 -2px 8px'
const minValue = scrollHeight - 200
const currentScroll = scrollTop + clientHeight
if (currentScroll >= minValue) {
const input = currentScroll < minValue ? minValue : currentScroll
const boxShadowOpacity = mapRange(
input,
minValue,
scrollHeight,
0.16,
0.0
)
const borderOpacity = mapRange(input, minValue, scrollHeight, 0.24, 0.0)
footer.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, ${boxShadowOpacity})`
footer.style.borderTopColor = `rgba(0, 0, 0, ${borderOpacity})`
}
}
const calculateFooterShadow = debounce(() => {
const boxShadowBase = '0 -2px 8px'
const scrollable = document.querySelector(`.${styles.scrollable}`)
const footer = footerRef
if (footer && footer.current) {
if (scrollable) {
if (scrollable.clientHeight >= scrollable.scrollHeight) {
footer.current.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, 0)`
footer.current.style.borderTopColor = `rgba(0, 0, 0, 0)`
} else {
footer.current.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, 0.16)`
footer.current.style.borderTopColor = `rgba(0, 0, 0, 0.24)`
}
} else {
footer.current.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, 0)`
footer.current.style.borderTopColor = `rgba(0, 0, 0, 0)`
}
}
}, 100)
useEffect(() => {
window.addEventListener('resize', calculateFooterShadow)
calculateFooterShadow()
return () => {
window.removeEventListener('resize', calculateFooterShadow)
}
}, [calculateFooterShadow])
function mapRange(
value: number,
low1: number,
high1: number,
low2: number,
high2: number
) {
return low2 + ((high2 - low2) * (value - low1)) / (high1 - low1)
}
// Methods: Modification checking
function hasBeenModified() {
return visibility !== party.visibility
}
// Methods: Data methods
async function updateDetails(event: React.MouseEvent) {
const details: DetailsObject = {
visibility: visibility,
}
await updateParty(details)
if (onOpenChange) onOpenChange(false)
}
// Methods: Rendering methods
function renderRadioItem(value: string, label: string) {
return (
<div className={styles.radioSet}>
<RadioGroup.Item
className={styles.radioItem}
value={value}
id={label}
tabIndex={parseInt(value)}
ref={radioItemRef[parseInt(value)]}
>
<RadioGroup.Indicator className={styles.radioIndicator} />
</RadioGroup.Item>
<label htmlFor={label}>
<h4>{t(`modals.team_visibility.options.${label}`)}</h4>
<p>{t(`modals.team_visibility.descriptions.${label}`)}</p>
</label>
</div>
)
}
const confirmationAlert = (
<Alert
message={t('modals.team_visibility.alerts.unsaved_changes.message')}
open={alertOpen}
primaryActionText={t(
'modals.team_visibility.alerts.unsaved_changes.buttons.confirm'
)}
primaryAction={close}
cancelActionText={t(
'modals.team_visibility.alerts.unsaved_changes.buttons.cancel'
)}
cancelAction={() => setAlertOpen(false)}
/>
)
return (
<>
{confirmationAlert}
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>{props.children}</DialogTrigger>
<DialogContent
className="changeVisibility"
onEscapeKeyDown={onEscapeKeyDown}
onOpenAutoFocus={onOpenAutoFocus}
>
<DialogHeader
title={t('modals.team_visibility.title')}
ref={headerRef}
/>
<div className={styles.content}>
<p className={styles.description}>
{t('modals.team_visibility.description')}
</p>
<RadioGroup.Root
className={styles.radioGroup}
defaultValue={`${visibility}`}
aria-label={t('modals.team_visibility.label')}
onValueChange={handleValueChange}
>
{renderRadioItem('1', 'public')}
{renderRadioItem('2', 'unlisted')}
{renderRadioItem('3', 'private')}
</RadioGroup.Root>
</div>
<DialogFooter
ref={footerRef}
rightElements={[
<Button
bound={true}
onClick={() => onOpenChange && onOpenChange(false)}
key="cancel"
text={t('buttons.cancel')}
/>,
<Button
bound={true}
key="confirm"
onClick={updateDetails}
text={t('modals.team_visibility.buttons.confirm')}
/>,
]}
/>
</DialogContent>
</Dialog>
</>
)
}
export default EditPartyModal

View file

@ -47,32 +47,32 @@
}
&.fire {
background: var(--fire-hover-bg);
background: var(--fire-portrait-bg);
border-color: var(--fire-bg);
}
&.water {
background: var(--water-hover-bg);
background: var(--water-portrait-bg);
border-color: var(--water-bg);
}
&.wind {
background: var(--wind-hover-bg);
background: var(--wind-portrait-bg);
border-color: var(--wind-bg);
}
&.earth {
background: var(--earth-hover-bg);
background: var(--earth-portrait-bg);
border-color: var(--earth-bg);
}
&.light {
background: var(--light-hover-bg);
background: var(--light-portrait-bg);
border-color: var(--light-bg);
}
&.dark {
background: var(--dark-hover-bg);
background: var(--dark-portrait-bg);
border-color: var(--dark-bg);
}

View file

@ -6,11 +6,15 @@
display: grid;
grid-template-rows: 1fr 1fr;
gap: $unit;
padding: $unit-2x;
padding: $unit-2x $unit-2x 0 $unit-2x;
min-width: 320px;
width: 100%;
opacity: 1;
@include breakpoint(phone) {
padding-bottom: $unit-2x;
}
&.visible {
transition: opacity 0.3s ease-in-out;
opacity: 1;
@ -29,6 +33,10 @@
text-decoration: none;
}
.indicators {
opacity: 1;
}
.weaponGrid {
cursor: pointer;
@ -46,7 +54,70 @@
}
}
& > .weaponGrid {
.gridContainer {
aspect-ratio: 2/0.95;
width: 100%;
}
.characterGrid {
aspect-ratio: 2/0.95;
display: flex;
justify-content: space-between;
.protagonist {
border-width: 1px;
border-style: solid;
&.fire {
background: var(--fire-portrait-bg);
border-color: var(--fire-bg);
}
&.water {
background: var(--water-portrait-bg);
border-color: var(--water-bg);
}
&.wind {
background: var(--wind-portrait-bg);
border-color: var(--wind-bg);
}
&.earth {
background: var(--earth-portrait-bg);
border-color: var(--earth-bg);
}
&.light {
background: var(--light-portrait-bg);
border-color: var(--light-bg);
}
&.dark {
background: var(--dark-portrait-bg);
border-color: var(--dark-bg);
}
&.empty {
background: var(--card-bg);
}
}
.grid {
background: var(--background);
border-radius: $item-corner-small;
aspect-ratio: 69/142;
list-style: none;
height: calc(100% - $unit-half);
img {
border-radius: $item-corner-small;
width: 100%;
}
}
}
.weaponGrid {
aspect-ratio: 2/0.95;
display: grid;
grid-template-columns: 1fr 3.36fr; /* left column takes up 1 fraction, right column takes up 3 fractions */
@ -54,7 +125,7 @@
.weapon {
background: var(--unit-bg);
border-radius: 4px;
border-radius: $item-corner-small;
}
.mainhand.weapon {
@ -91,6 +162,51 @@
}
}
.summonGrid {
aspect-ratio: 2/0.94;
display: flex;
gap: $unit;
justify-content: space-between;
.summon,
.mainSummon {
background: var(--background);
border-radius: $item-corner-small;
img {
border-radius: $item-corner-small;
width: 100%;
}
}
.mainSummon {
aspect-ratio: 56/97;
display: grid;
grid-column: 1 / 2; /* spans one column */
}
.summons {
display: grid; /* make the right-images container a grid */
grid-template-columns: repeat(
2,
1fr
); /* create 3 columns, each taking up 1 fraction */
grid-template-rows: repeat(
2,
1fr
); /* create 3 rows, each taking up 1 fraction */
gap: $unit;
aspect-ratio: 83/100;
// column-gap: $unit;
// row-gap: $unit-2x;
}
.summon {
aspect-ratio: 184 / 138;
display: grid;
}
}
.details {
display: flex;
flex-direction: column;
@ -104,6 +220,7 @@
padding-bottom: 1px;
text-overflow: ellipsis;
white-space: nowrap;
min-height: 24px;
max-width: 258px; // Can we not do this?
&.empty {
@ -124,6 +241,18 @@
gap: calc($unit / 2);
}
.icon {
display: flex;
align-items: center;
justify-content: center;
width: $unit * 2.5;
height: $unit * 2.5;
svg {
fill: var(--text-tertiary);
}
}
button svg {
width: 14px;
height: 14px;
@ -157,6 +286,7 @@
}
time {
line-height: 18px;
white-space: nowrap;
}
@ -234,4 +364,41 @@
}
}
}
.indicators {
display: flex;
flex-direction: row;
gap: $unit;
margin-top: $unit * -1;
margin-bottom: $unit-fourth;
justify-content: center;
opacity: 0;
@include breakpoint(phone) {
display: none;
}
li {
flex-grow: 1;
text-indent: -9999px;
padding: $unit 0;
.indicator {
transition: background-color 0.12s ease-in-out;
height: $unit;
border-radius: $unit-half;
background-color: var(--button-contained-bg-hover);
}
span {
text-indent: -9999px;
position: absolute;
}
&:hover .indicator,
&.active .indicator {
background-color: var(--text-secondary);
}
}
}
}

View file

@ -10,29 +10,24 @@ import { accountState } from '~utils/accountState'
import { formatTimeAgo } from '~utils/timeAgo'
import Button from '~components/common/Button'
import Tooltip from '~components/common/Tooltip'
import SaveIcon from '~public/icons/Save.svg'
import PrivateIcon from '~public/icons/Private.svg'
import UnlistedIcon from '~public/icons/Unlisted.svg'
import ShieldIcon from '~public/icons/Shield.svg'
import styles from './index.module.scss'
interface Props {
shortcode: string
id: string
name: string
raid: Raid
grid: GridWeapon[]
user?: User
fullAuto: boolean
autoGuard: boolean
favorited: boolean
party: Party
loading: boolean
createdAt: Date
onClick: (shortcode: string) => void
onSave?: (partyId: string, favorited: boolean) => void
}
const GridRep = (props: Props) => {
const GridRep = ({ party, loading, onClick, onSave }: Props) => {
const numWeapons: number = 9
const numSummons: number = 6
const { account } = useSnapshot(accountState)
@ -42,27 +37,42 @@ const GridRep = (props: Props) => {
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const [visible, setVisible] = useState(false)
const [currentView, setCurrentView] = useState<
'characters' | 'weapons' | 'summons'
>('weapons')
const [mainhand, setMainhand] = useState<Weapon>()
const [weapons, setWeapons] = useState<GridArray<Weapon>>({})
const [grid, setGrid] = useState<GridArray<GridWeapon>>({})
const [weaponGrid, setWeaponGrid] = useState<GridArray<GridWeapon>>({})
const [characters, setCharacters] = useState<GridArray<Character>>({})
const [characterGrid, setCharacterGrid] = useState<GridArray<GridCharacter>>(
{}
)
const [mainSummon, setMainSummon] = useState<GridSummon>()
const [friendSummon, setFriendSummon] = useState<GridSummon>()
const [summons, setSummons] = useState<GridArray<Summon>>({})
const [summonGrid, setSummonGrid] = useState<GridArray<GridSummon>>({})
const gridRepStyles = classNames({
// Style construction
const gridRepClasses = classNames({
[styles.gridRep]: true,
[styles.visible]: visible,
[styles.hidden]: !visible,
})
const titleClass = classNames({
empty: !props.name,
empty: !party.name,
})
const raidClass = classNames({
[styles.raid]: true,
[styles.empty]: !props.raid,
[styles.empty]: !party.raid,
})
const userClass = classNames({
[styles.user]: true,
[styles.empty]: !props.user,
[styles.empty]: !party.user,
})
const mainhandClasses = classNames({
@ -75,8 +85,22 @@ const GridRep = (props: Props) => {
[styles.grid]: true,
})
const protagonistClasses = classNames({
[styles.protagonist]: true,
[styles.grid]: true,
[styles[`${numberToElement()}`]]: true,
[styles.empty]: !party.job || party.job.id === '-1',
})
const characterClasses = classNames({
[styles.character]: true,
[styles.grid]: true,
})
// Hooks
useEffect(() => {
if (props.loading) {
if (loading) {
setVisible(false)
} else {
const timeout = setTimeout(() => {
@ -84,7 +108,7 @@ const GridRep = (props: Props) => {
}, 150)
return () => clearTimeout(timeout)
}
}, [props.loading])
}, [loading])
useEffect(() => {
setVisible(false) // Trigger fade out
@ -99,7 +123,7 @@ const GridRep = (props: Props) => {
const gridWeapons = Array(numWeapons)
let foundMainhand = false
for (const [key, value] of Object.entries(props.grid)) {
for (const [key, value] of Object.entries(party.weapons)) {
if (value.position == -1) {
setMainhand(value.object)
foundMainhand = true
@ -114,18 +138,74 @@ const GridRep = (props: Props) => {
}
setWeapons(newWeapons)
setGrid(gridWeapons)
}, [props.grid])
setWeaponGrid(gridWeapons)
}, [party])
function navigate() {
props.onClick(props.shortcode)
useEffect(() => {
const newCharacters = Array(3)
const gridCharacters = Array(3)
if (party.characters) {
for (const [key, value] of Object.entries(party.characters)) {
if (value.position != null) {
newCharacters[value.position] = value.object
gridCharacters[value.position] = value
}
}
setCharacters(newCharacters)
setCharacterGrid(gridCharacters)
}
}, [party])
useEffect(() => {
const newSummons = Array(numSummons)
const gridSummons = Array(numSummons)
if (party.summons) {
for (const [key, value] of Object.entries(party.summons)) {
if (value.main) {
setMainSummon(value)
} else if (value.friend) {
setFriendSummon(value)
} else if (!value.main && !value.friend && value.position != null) {
newSummons[value.position] = value.object
gridSummons[value.position] = value
}
}
setSummons(newSummons)
setSummonGrid(gridSummons)
}
}, [party])
// Convert element to string
function numberToElement() {
switch (mainhand?.element || weaponGrid[0]?.element) {
case 1:
return 'wind'
case 2:
return 'fire'
case 3:
return 'water'
case 4:
return 'earth'
case 5:
return 'dark'
case 6:
return 'light'
default:
return ''
}
}
// Methods: Image generation
function generateMainhandImage() {
let url = ''
if (mainhand) {
const weapon = Object.values(props.grid).find(
const weapon = Object.values(party.weapons).find(
(w) => w && w.object.id === mainhand.id
)
@ -136,18 +216,18 @@ const GridRep = (props: Props) => {
}
}
return mainhand && props.grid[0] ? (
return mainhand && party.weapons[0] ? (
<img alt={mainhand.name[locale]} src={url} />
) : (
''
)
}
function generateGridImage(position: number) {
function generateWeaponGridImage(position: number) {
let url = ''
const weapon = weapons[position]
const gridWeapon = grid[position]
const gridWeapon = weaponGrid[position]
if (weapon && gridWeapon) {
if (weapon.element == 0 && gridWeapon.element) {
@ -164,19 +244,163 @@ const GridRep = (props: Props) => {
)
}
function generateMCImage() {
let source = ''
if (party.job) {
const slug = party.job.name.en.replaceAll(' ', '-').toLowerCase()
const gender = party.user?.gender == 1 ? 'b' : 'a'
source = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/job-portraits/${slug}_${gender}.png`
}
return (
party.job &&
party.job.id !== '-1' && (
<img alt={party.job ? party.job?.name[locale] : ''} src={source} />
)
)
}
function generateCharacterGridImage(position: number) {
let url = ''
const gridCharacter = characterGrid[position]
const character = characters[position]
if (character && gridCharacter) {
// Change the image based on the uncap level
let suffix = '01'
if (gridCharacter.transcendence_step > 0) suffix = '04'
else if (gridCharacter.uncap_level >= 5) suffix = '03'
else if (gridCharacter.uncap_level > 2) suffix = '02'
if (gridCharacter.object.granblue_id === '3030182000') {
let element = 1
if (mainhand && mainhand.element) {
element = mainhand.element
}
suffix = `${suffix}_0${element}`
}
const url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/character-main/${character.granblue_id}_${suffix}.jpg`
return (
characters[position] && (
<img alt={characters[position]?.name[locale]} src={url} />
)
)
}
}
function generateMainSummonImage(position: 'main' | 'friend') {
let url = ''
const upgradedSummons = [
'2040094000',
'2040100000',
'2040080000',
'2040098000',
'2040090000',
'2040084000',
'2040003000',
'2040056000',
'2040020000',
'2040034000',
'2040028000',
'2040027000',
'2040046000',
'2040047000',
]
const summon = position === 'main' ? mainSummon : friendSummon
if (summon) {
// Change the image based on the uncap level
let suffix = ''
if (summon.object.uncap.xlb && summon.uncap_level == 6) {
if (summon.transcendence_step >= 1 && summon.transcendence_step < 5) {
suffix = '_03'
} else if (summon.transcendence_step === 5) {
suffix = '_04'
}
} else if (
upgradedSummons.indexOf(summon.object.granblue_id.toString()) != -1 &&
summon.uncap_level == 5
) {
suffix = '_02'
}
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-main/${summon.object.granblue_id}${suffix}.jpg`
}
return summon && <img alt={summon.object.name[locale]} src={url} />
}
function generateSummonGridImage(position: number) {
let url = ''
const gridSummon = summonGrid[position]
const summon = gridSummon?.object
const upgradedSummons = [
'2040094000',
'2040100000',
'2040080000',
'2040098000',
'2040090000',
'2040084000',
'2040003000',
'2040056000',
'2040020000',
'2040034000',
'2040028000',
'2040027000',
'2040046000',
'2040047000',
]
if (summon && gridSummon) {
// Change the image based on the uncap level
let suffix = ''
if (gridSummon.object.uncap.xlb && gridSummon.uncap_level == 6) {
if (
gridSummon.transcendence_step >= 1 &&
gridSummon.transcendence_step < 5
) {
suffix = '_03'
} else if (gridSummon.transcendence_step === 5) {
suffix = '_04'
}
} else if (
upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 &&
gridSummon.uncap_level == 5
) {
suffix = '_02'
}
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg`
}
return (
summons[position] && (
<img alt={summons[position]?.name[locale]} src={url} />
)
)
}
function sendSaveData() {
if (props.onSave) props.onSave(props.id, props.favorited)
if (onSave) onSave(party.id, party.favorited)
}
const userImage = () => {
if (props.user && props.user.avatar) {
if (party.user && party.user.avatar) {
return (
<img
alt={props.user.avatar.picture}
className={`profile ${props.user.avatar.element}`}
srcSet={`/profile/${props.user.avatar.picture}.png,
/profile/${props.user.avatar.picture}@2x.png 2x`}
src={`/profile/${props.user.avatar.picture}.png`}
alt={party.user.avatar.picture}
className={`profile ${party.user.avatar.element}`}
srcSet={`/profile/${party.user.avatar.picture}.png,
/profile/${party.user.avatar.picture}@2x.png 2x`}
src={`/profile/${party.user.avatar.picture}.png`}
/>
)
} else
@ -194,29 +418,103 @@ const GridRep = (props: Props) => {
const attribution = () => (
<span className={userClass}>
{userImage()}
{props.user ? props.user.username : t('no_user')}
{party.user ? party.user.username : t('no_user')}
</span>
)
function fullAutoString() {
const fullAutoElement = (
<span className={styles.fullAuto}>
{` · ${t('party.details.labels.full_auto')}`}
</span>
)
const renderWeaponGrid = (
<div className={styles.weaponGrid}>
<div className={mainhandClasses}>{generateMainhandImage()}</div>
const autoGuardElement = (
<span className={styles.autoGuard}>
<ShieldIcon />
</span>
)
<ul className={styles.weapons}>
{Array.from(Array(numWeapons)).map((x, i) => {
return (
<li
key={`${party.shortcode}-weapon-${i}`}
className={weaponClasses}
>
{generateWeaponGridImage(i)}
</li>
)
})}
</ul>
</div>
)
return (
<div className={styles.auto}>
{fullAutoElement}
{props.autoGuard ? autoGuardElement : ''}
const renderCharacterGrid = (
<div className={styles.characterGrid}>
<div className={protagonistClasses}>{generateMCImage()}</div>
{Array.from(Array(3)).map((x, i) => {
return (
<li
key={`${party.shortcode}-chara-${i}`}
className={characterClasses}
>
{generateCharacterGridImage(i)}
</li>
)
})}
</div>
)
const renderSummonGrid = (
<div className={styles.summonGrid}>
<div className={styles.mainSummon}>{generateMainSummonImage('main')}</div>
<ul className={styles.summons}>
{Array.from(Array(numSummons)).map((x, i) => {
return (
<li key={`summons-${i}`} className={styles.summon}>
{generateSummonGridImage(i)}
</li>
)
})}
</ul>
<div className={styles.mainSummon}>
{generateMainSummonImage('friend')}
</div>
)
</div>
)
const favoriteButton = (
<Link href="#">
<Button
className={classNames({
save: true,
saved: party.favorited,
})}
leftAccessoryIcon={<SaveIcon className="stroke" />}
active={party.favorited}
bound={true}
size="small"
onClick={sendSaveData}
/>
</Link>
)
function buttonArea() {
if (
account.authorized &&
((party.user && account.user && account.user.id !== party.user.id) ||
!party.user)
) {
return favoriteButton
} else if (party.visibility === 2) {
return (
<Tooltip content={t('party.tooltips.unlisted')}>
<span className={styles.icon}>
<UnlistedIcon />
</span>
</Tooltip>
)
} else if (party.visibility === 3) {
return (
<Tooltip content={t('party.tooltips.private')}>
<span className={styles.icon}>
<PrivateIcon />
</span>
</Tooltip>
)
}
}
const detailsWithUsername = (
@ -224,73 +522,84 @@ const GridRep = (props: Props) => {
<div className={styles.top}>
<div className={styles.info}>
<h2 className={titleClass}>
{props.name ? props.name : t('no_title')}
{party.name ? party.name : t('no_title')}
</h2>
<div className={styles.properties}>
<span className={raidClass}>
{props.raid ? props.raid.name[locale] : t('no_raid')}
{party.raid ? party.raid.name[locale] : t('no_raid')}
</span>
{props.fullAuto && (
{party.full_auto && (
<span className={styles.fullAuto}>
{` · ${t('party.details.labels.full_auto')}`}
</span>
)}
{props.raid && props.raid.group.extra && (
{party.raid && party.raid.group.extra && (
<span className={styles.extra}>{` · EX`}</span>
)}
</div>
</div>
{account.authorized &&
((props.user && account.user && account.user.id !== props.user.id) ||
!props.user) ? (
<Link href="#">
<Button
className={classNames({
save: true,
saved: props.favorited,
})}
leftAccessoryIcon={<SaveIcon className="stroke" />}
active={props.favorited}
bound={true}
size="small"
onClick={sendSaveData}
/>
</Link>
) : (
''
)}
{buttonArea()}
</div>
<div className={styles.attributed}>
{attribution()}
<time
className={styles.lastUpdated}
dateTime={props.createdAt.toISOString()}
dateTime={new Date(party.created_at).toISOString()}
>
{formatTimeAgo(props.createdAt, locale)}
{formatTimeAgo(new Date(party.created_at), locale)}
</time>
</div>
</div>
)
return (
<Link legacyBehavior href={`/p/${props.shortcode}`}>
<a className={gridRepStyles}>
{detailsWithUsername}
<div className={styles.weaponGrid}>
<div className={mainhandClasses}>{generateMainhandImage()}</div>
function changeView(view: 'characters' | 'weapons' | 'summons') {
setCurrentView(view)
}
<ul className={styles.weapons}>
{Array.from(Array(numWeapons)).map((x, i) => {
return (
<li key={`${props.shortcode}-${i}`} className={weaponClasses}>
{generateGridImage(i)}
</li>
)
})}
</ul>
</div>
</a>
return (
<Link
href={`/p/${party.shortcode}`}
className={gridRepClasses}
onMouseLeave={() => changeView('weapons')}
>
{detailsWithUsername}
<div className={styles.gridContainer}>
{currentView === 'characters'
? renderCharacterGrid
: currentView === 'summons'
? renderSummonGrid
: renderWeaponGrid}
</div>
<ul className={styles.indicators}>
<li
className={classNames({
[styles.active]: currentView === 'characters',
})}
onMouseEnter={() => changeView('characters')}
>
<div className={styles.indicator} />
<span>Characters</span>
</li>
<li
className={classNames({
[styles.active]: currentView === 'weapons',
})}
onMouseEnter={() => changeView('weapons')}
>
<div className={styles.indicator} />
<span>Weapons</span>
</li>
<li
className={classNames({
[styles.active]: currentView === 'summons',
})}
onMouseEnter={() => changeView('summons')}
>
<div className={styles.indicator} />
<span>Summons</span>
</li>
</ul>
</Link>
)
}

View file

@ -444,7 +444,7 @@ const WeaponModal = ({
<Alert
message={
<span>
<Trans i18nKey="alerts.unsaved_changes.object">
<Trans i18nKey="alert.unsaved_changes.object">
You will lose all changes to{' '}
<strong>{{ objectName: gridWeapon.object.name[locale] }}</strong> if
you continue.
@ -455,9 +455,9 @@ const WeaponModal = ({
</span>
}
open={alertOpen}
primaryActionText="Close"
primaryActionText={t('alert.unsaved_changes.buttons.confirm')}
primaryAction={close}
cancelActionText="Nevermind"
cancelActionText={t('alert.unsaved_changes.buttons.cancel')}
cancelAction={() => setAlertOpen(false)}
/>
)

View file

@ -255,16 +255,7 @@ const ProfileRoute: React.FC<Props> = ({
return parties.map((party, i) => {
return (
<GridRep
id={party.id}
shortcode={party.shortcode}
name={party.name}
createdAt={new Date(party.created_at)}
raid={party.raid}
grid={party.weapons}
user={party.user}
favorited={party.favorited}
fullAuto={party.full_auto}
autoGuard={party.auto_guard}
party={party}
key={`party-${i}`}
loading={isLoading}
onClick={goTo}

View file

@ -294,16 +294,7 @@ const SavedRoute: React.FC<Props> = ({
return parties.map((party, i) => {
return (
<GridRep
id={party.id}
shortcode={party.shortcode}
name={party.name}
createdAt={new Date(party.created_at)}
raid={party.raid}
grid={party.weapons}
user={party.user}
favorited={party.favorited}
fullAuto={party.full_auto}
autoGuard={party.auto_guard}
party={party}
key={`party-${i}`}
loading={isLoading}
onClick={goTo}

View file

@ -308,16 +308,7 @@ const TeamsRoute: React.FC<Props> = ({
return parties.map((party, i) => {
return (
<GridRep
id={party.id}
shortcode={party.shortcode}
name={party.name}
createdAt={new Date(party.created_at)}
raid={party.raid}
grid={party.weapons}
user={party.user}
favorited={party.favorited}
fullAuto={party.full_auto}
autoGuard={party.auto_guard}
party={party}
key={`party-${i}`}
loading={isLoading}
onClick={goTo}

3
public/icons/Private.svg Normal file
View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.625 4.27273C3.625 2.46525 5.13604 1 7 1C8.86396 1 10.375 2.46525 10.375 4.27273V6.45455H10.4056C11.01 6.45455 11.5 6.94454 11.5 7.54897V11.9056C11.5 12.51 11.01 13 10.4056 13H3.59443C2.98999 13 2.5 12.51 2.5 11.9056V7.54897C2.5 6.94454 2.98999 6.45455 3.59443 6.45455H3.625V4.27273ZM5.3125 6.45455H8.6875V4.27273C8.6875 3.4678 8.30444 2.63636 7 2.63636C5.69557 2.63636 5.3125 3.45827 5.3125 4.27273V6.45455Z" />
</svg>

After

Width:  |  Height:  |  Size: 532 B

4
public/icons/Public.svg Normal file
View file

@ -0,0 +1,4 @@
<svg viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.04515 9.5C8.42586 9.5 9.54515 8.38071 9.54515 7C9.54515 5.61929 8.42586 4.5 7.04515 4.5C5.66444 4.5 4.54515 5.61929 4.54515 7C4.54515 8.38071 5.66444 9.5 7.04515 9.5ZM7.04515 8.5C7.87358 8.5 8.54515 7.82843 8.54515 7C8.54515 6.17157 7.87358 5.5 7.04515 5.5C6.21672 5.5 5.54515 6.17157 5.54515 7C5.54515 7.82843 6.21672 8.5 7.04515 8.5Z" />
<path d="M13.2118 6.44776C10.1648 1.67656 4.08822 1.61674 0.814821 6.43618C0.611758 6.73515 0.607725 7.12607 0.803991 7.42954C3.9948 12.3633 10.0268 12.435 13.2119 7.41171C13.3986 7.11736 13.3994 6.7415 13.2118 6.44776Z" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 719 B

View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.9358 6.38178C14.0712 6.14109 13.9858 5.83624 13.7451 5.70088C13.5044 5.56552 13.1996 5.6509 13.0642 5.89159C11.5427 8.59686 9.24574 9.87242 7.00066 9.86173C4.753 9.85102 2.45409 8.5506 0.934206 5.88876C0.79728 5.64896 0.49188 5.56556 0.252076 5.70248C0.012272 5.83941 -0.0711275 6.14481 0.0657981 6.38461C0.666775 7.43713 1.38868 8.31027 2.19043 8.99743C2.16108 9.0253 2.13467 9.0572 2.11204 9.09296L1.57636 9.93929C1.42867 10.1726 1.4981 10.4815 1.73143 10.6292C1.96476 10.7769 2.27364 10.7074 2.42132 10.4741L2.957 9.62777C2.96384 9.61698 2.9702 9.60602 2.97611 9.59493C3.33838 9.83773 3.71284 10.0462 4.09629 10.2199C4.08317 10.2433 4.07177 10.268 4.0623 10.2941L3.71972 11.2353C3.62528 11.4948 3.75907 11.7817 4.01856 11.8761C4.27805 11.9706 4.56497 11.8368 4.65942 11.5773L5.00199 10.6361C5.01037 10.613 5.01695 10.5898 5.02181 10.5665C5.50693 10.7115 6.00176 10.8035 6.5004 10.8416C6.50014 10.8483 6.5 10.855 6.5 10.8618V11.8634C6.5 12.1395 6.72386 12.3634 7 12.3634C7.27614 12.3634 7.5 12.1395 7.5 11.8634V10.8618L7.49975 10.8458C7.99944 10.8117 8.49534 10.7233 8.9815 10.5811C8.98592 10.5995 8.99142 10.6179 8.99805 10.6361L9.34063 11.5773C9.43507 11.8368 9.72199 11.9706 9.98148 11.8761C10.241 11.7817 10.3748 11.4948 10.2803 11.2353L9.93775 10.2941C9.93055 10.2743 9.92222 10.2552 9.91288 10.2369C10.2967 10.0644 10.6715 9.85639 11.0341 9.6131L11.043 9.62777L11.5787 10.4741C11.7264 10.7074 12.0353 10.7769 12.2686 10.6292C12.5019 10.4815 12.5714 10.1726 12.4237 9.93929L11.888 9.09296C11.8689 9.06272 11.847 9.03523 11.823 9.01061C12.6198 8.32285 13.3376 7.44546 13.9358 6.38178Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -12,11 +12,15 @@
},
"alert": {
"confirm_logout": "Are you sure you want to log out?",
"incompatible_weapon": "You've selected a weapon that can't be added to the Additional Weapon slots.",
"unsaved_changes": {
"party": "You will lose all changes to your party <strong>{objectName}</strong> if you continue.<br/><br/>Are you sure you want to continue without saving?",
"object": "You will lose all changes to <strong>{objectName}</strong> if you continue.<br/><br/>Are you sure you want to continue without saving?"
},
"incompatible_weapon": "You've selected a weapon that can't be added to the Additional Weapon slots."
"party": "You will lose all changes to your party <strong>{{objectName}}</strong> if you continue.<br/><br/>Are you sure you want to continue without saving?",
"object": "You will lose all changes to <strong>{{objectName}}</strong> if you continue.<br/><br/>Are you sure you want to continue without saving?",
"buttons": {
"confirm": "Continue without saving",
"cancel": "Nevermind"
}
}
},
"ax": {
"no_skill": "No AX Skill",
@ -65,6 +69,7 @@
},
"dropdown": {
"party": {
"visibility": "Change team visibility",
"copy": "Copy link to team",
"delete": "Delete team",
"remix": "Remix team"
@ -397,6 +402,33 @@
"remove": "Remove summon"
}
},
"team_visibility": {
"title": "Change team visibility",
"label": "Team visibility",
"description": "Change who can see this team and where it shows up on the site",
"alerts": {
"unsaved_changes": {
"message": "Are you sure you want to continue without changing your team's visibility?",
"buttons": {
"confirm": "Continue without saving",
"cancel": "Nevermind"
}
}
},
"options": {
"public": "Public",
"unlisted": "Unlisted",
"private": "Private"
},
"descriptions": {
"public": "Visible to everyone and appears on the Teams page",
"unlisted": "Only visible to people with the link and does not appear on the Teams page",
"private": "Only visible to you and does not appear on the Teams page"
},
"buttons": {
"confirm": "Change visibility"
}
},
"weapon": {
"title": "Modify Weapon",
"buttons": {
@ -459,6 +491,18 @@
"with_count_one": "{{count}} turn",
"with_count_other": "{{count}} turns"
}
},
"notices": {
"unlisted": "This party is unlisted. Only people with the link with can see it.",
"private": "This party is private. Only you can see it.",
"buttons": {
"copy_link": "Copy link",
"change_visibility": "Change visibility"
}
},
"tooltips": {
"unlisted": "This party is unlisted",
"private": "This party is private"
}
},
"proficiencies": {

View file

@ -12,9 +12,15 @@
},
"alert": {
"confirm_logout": "ログアウトしますか",
"unsaved_changes_party": "<strong>「{objectName}」</strong>という編成の変更は保存していません。<br/><br/>変更を保存せずに続けますか?",
"unsaved_changes_object": "<strong>「{objectName}」</strong>の変更は保存していません。<br/><br/>変更を保存せずに続けますか?",
"incompatible_weapon": "Additional Weaponsに装備できない武器を入れました。"
"incompatible_weapon": "Additional Weaponsに装備できない武器を入れました。",
"unsaved_changes": {
"party": "<strong>「{{objectName}}」</strong>という編成の変更は保存していません。<br/><br/>変更を保存せずに続けますか?",
"object": "<strong>「{{objectName}}」</strong>の変更は保存していません。<br/><br/>変更を保存せずに続けますか?",
"buttons": {
"confirm": "変更せずに続ける",
"cancel": "キャンセル"
}
}
},
"ax": {
"no_skill": "EXスキルなし",
@ -63,6 +69,7 @@
},
"dropdown": {
"party": {
"visibility": "編成のプライバシー設定を変更",
"copy": "編成のリンクをコピー",
"delete": "編成を削除",
"remix": "編成をリミックス"
@ -395,6 +402,33 @@
"remove": "召喚石を削除する"
}
},
"team_visibility": {
"title": "編成のプライバシー設定を変更",
"label": "プライバシー設定",
"description": "この編成は誰が共有できるかと編成一覧に表示されるかを変更できます",
"alerts": {
"unsaved_changes": {
"message": "編成のプライバシー設定を変更せずに続けますか?",
"buttons": {
"confirm": "変更せずに続ける",
"cancel": "キャンセル"
}
}
},
"options": {
"public": "公開",
"unlisted": "限定公開",
"private": "非公開"
},
"descriptions": {
"public": "誰でも自由に共有可能。編成一覧に表示します。",
"unlisted": "リンクが知っている人のみが共有可能。編成一覧に表示しません。",
"private": "自分自身だけが可視。編成一覧に表示しません。"
},
"buttons": {
"confirm": "プライバシー設定を変更"
}
},
"weapon": {
"title": "武器変更",
"buttons": {
@ -453,10 +487,23 @@
"minutes": "分",
"seconds": "秒"
},
"turns": {
"with_count_one": "{{count}}ターン",
"with_count_other": "{{count}}ターン"
}
},
"notices": {
"unlisted": "この編成は限定公開でリンクが知っている人のみが共有可能",
"private": "この編成は未公開で自分自身だけが可視",
"buttons": {
"copy_link": "リンクをコピー",
"change_visibility": "プライバシー設定を変更"
}
},
"tooltips": {
"unlisted": "この編成は限定公開",
"private": "この編成は未公開"
}
},
"proficiencies": {

View file

@ -81,6 +81,11 @@
--notice-bg: #{$notice--bg--light};
--notice-text: #{$notice--text--light};
--notice-button-bg: #{$notice--button--bg--light};
--notice-button-bg-hover: #{$notice--button--bg--light--hover};
--notice-button-text: #{$notice--button--text--light};
--notice-button-text-hover: #{$notice--button--text--light--hover};
// Light - Buttons
--button-bg: #{$button--bg--light};
--button-bg-hover: #{$button--bg--light--hover};
@ -112,6 +117,13 @@
--slider-thumb-shadow: #{$slider--thumb--shadow--light};
--slider-thumb-shadow-hover: #{$slider--thumb--shadow--light--hover};
// Light - Radio Buttons
--radio-button-bg: #{$radio--bg--light};
--radio-button-bg-hover: #{$radio--bg--light--hover};
--radio-active-bg: #{$radio--active--bg--light};
--radio-active-bg-hover: #{$radio--active--bg--light--hover};
// Light - About
--link-item-bg: #{$link--item--bg--light};
--link-item-image-color: #{$link--item--bg--image--light};
@ -173,6 +185,7 @@
--wind-bg: #{$wind--bg--light};
--wind-bg-hover: #{$wind--bg--hover--light};
--wind-portrait-bg: #{$wind--portrait--bg--light};
--wind-text: #{$wind--text--light};
--wind-raid-text: #{$wind--text--raid--light};
--wind-text-hover: #{$wind--text--hover--light};
@ -181,6 +194,7 @@
--fire-bg: #{$fire--bg--light};
--fire-bg-hover: #{$fire--bg--hover--light};
--fire-portrait-bg: #{$fire--portrait--bg--light};
--fire-text: #{$fire--text--light};
--fire-raid-text: #{$fire--text--raid--light};
--fire-text-hover: #{$fire--text--hover--light};
@ -189,6 +203,7 @@
--water-bg: #{$water--bg--light};
--water-bg-hover: #{$water--bg--hover--light};
--water-portrait-bg: #{$water--portrait--bg--light};
--water-text: #{$water--text--light};
--water-raid-text: #{$water--text--raid--light};
--water-text-hover: #{$water--text--hover--light};
@ -197,6 +212,7 @@
--earth-bg: #{$earth--bg--light};
--earth-bg-hover: #{$earth--bg--hover--light};
--earth-portrait-bg: #{$earth--portrait--bg--light};
--earth-text: #{$earth--text--light};
--earth-raid-text: #{$earth--text--raid--light};
--earth-text-hover: #{$earth--text--hover--light};
@ -205,6 +221,7 @@
--dark-bg: #{$dark--bg--light};
--dark-bg-hover: #{$dark--bg--hover--light};
--dark-portrait-bg: #{$dark--portrait--bg--light};
--dark-text: #{$dark--text--light};
--dark-raid-text: #{$dark--text--raid--light};
--dark-text-hover: #{$dark--text--hover--light};
@ -213,6 +230,7 @@
--light-bg: #{$light--bg--light};
--light-bg-hover: #{$light--bg--hover--light};
--light-portrait-bg: #{$light--portrait--bg--light};
--light-text: #{$light--text--light};
--light-raid-text: #{$light--text--raid--light};
--light-text-hover: #{$light--text--hover--light};
@ -307,6 +325,11 @@
--notice-bg: #{$notice--bg--dark};
--notice-text: #{$notice--text--dark};
--notice-button-bg: #{$notice--button--bg--dark};
--notice-button-bg-hover: #{$notice--button--bg--dark--hover};
--notice-button-text: #{$notice--button--text--dark};
--notice-button-text-hover: #{$notice--button--text--dark--hover};
// Dark - Buttons
--button-bg: #{$button--bg--dark};
--button-bg-hover: #{$button--bg--dark--hover};
@ -338,6 +361,13 @@
--slider-thumb-shadow: #{$slider--thumb--shadow--dark};
--slider-thumb-shadow-hover: #{$slider--thumb--shadow--dark--hover};
// Dark - Radio Buttons
--radio-button-bg: #{$radio--bg--dark};
--radio-button-bg-hover: #{$radio--bg--dark--hover};
--radio-active-bg: #{$radio--active--bg--dark};
--radio-active-bg-hover: #{$radio--active--bg--dark--hover};
// Dark - About
--link-item-bg: #{$link--item--bg--dark};
--link-item-image-color: #{$link--item--bg--image--dark};
@ -399,6 +429,7 @@
--wind-bg: #{$wind--bg--dark};
--wind-bg-hover: #{$wind--bg--hover--dark};
--wind-portrait-bg: #{$wind--portrait--bg--dark};
--wind-text: #{$wind--text--dark};
--wind-raid-text: #{$wind--text--raid--dark};
--wind-text-hover: #{$wind--text--hover--dark};
@ -407,6 +438,7 @@
--fire-bg: #{$fire--bg--dark};
--fire-bg-hover: #{$fire--bg--hover--dark};
--fire-portrait-bg: #{$fire--portrait--bg--dark};
--fire-text: #{$fire--text--dark};
--fire-raid-text: #{$fire--text--raid--dark};
--fire-text-hover: #{$fire--text--hover--dark};
@ -415,6 +447,7 @@
--water-bg: #{$water--bg--dark};
--water-bg-hover: #{$water--bg--hover--dark};
--water-portrait-bg: #{$water--portrait--bg--dark};
--water-text: #{$water--text--dark};
--water-raid-text: #{$water--text--raid--dark};
--water-text-hover: #{$water--text--hover--dark};
@ -423,6 +456,7 @@
--earth-bg: #{$earth--bg--dark};
--earth-bg-hover: #{$earth--bg--hover--dark};
--earth-portrait-bg: #{$earth--portrait--bg--dark};
--earth-text: #{$earth--text--dark};
--earth-raid-text: #{$earth--text--raid--dark};
--earth-text-hover: #{$earth--text--hover--dark};
@ -431,6 +465,7 @@
--dark-bg: #{$dark--bg--dark};
--dark-bg-hover: #{$dark--bg--hover--dark};
--dark-portrait-bg: #{$dark--portrait--bg--dark};
--dark-text: #{$dark--text--dark};
--dark-raid-text: #{$dark--text--raid--dark};
--dark-text-hover: #{$dark--text--hover--dark};
@ -439,6 +474,7 @@
--light-bg: #{$light--bg--dark};
--light-bg-hover: #{$light--bg--hover--dark};
--light-portrait-bg: #{$light--portrait--bg--dark};
--light-text: #{$light--text--dark};
--light-raid-text: #{$light--text--raid--dark};
--light-text-hover: #{$light--text--hover--dark};

View file

@ -107,10 +107,13 @@ $yellow-text-20: #ffed4c;
$highlight-yellow: #ffed4c55;
$accent--yellow--00: #463805;
$accent--yellow--20: #7f6a00;
$accent--yellow--10: #6c5a01;
$accent--yellow--20: #776300;
$accent--yellow--40: #a39200;
$accent--yellow--60: #c89d39;
$accent--yellow--70: #d1aa4f;
$accent--yellow--80: #deb351;
$accent--yellow--90: #e6bd5e;
$accent--yellow--100: #f9cc64;
$selected--item--bg--dark: #f9cc645d;
@ -222,6 +225,18 @@ $notice--bg--dark: $accent--yellow--00;
$notice--text--light: $accent--yellow--20;
$notice--text--dark: $accent--yellow--100;
$notice--button--bg--light: $accent--yellow--80;
$notice--button--bg--dark: $accent--yellow--20;
$notice--button--bg--light--hover: $accent--yellow--70;
$notice--button--bg--dark--hover: $accent--yellow--10;
$notice--button--text--light: $accent--yellow--10;
$notice--button--text--dark: $accent--yellow--90;
$notice--button--text--light--hover: $accent--yellow--00;
$notice--button--text--dark--hover: $accent--yellow--100;
// Color Definitions: Button
$button--bg--light: $grey-80;
$button--bg--light--hover: $grey-100;
@ -440,6 +455,19 @@ $icon--secondary--color--dark: $grey-50;
$icon--secondary--hover--light: $grey-50;
$icon--secondary--hover--dark: $grey-70;
// Color Definitions: Radio Buttons
$radio--bg--light: $grey-75;
$radio--bg--dark: $grey-10;
$radio--bg--light--hover: $grey-70;
$radio--bg--dark--hover: $grey-00;
$radio--active--bg--light: $accent--blue--light;
$radio--active--bg--dark: $accent--blue--dark;
$radio--active--bg--light--hover: $accent--blue--light--focus;
$radio--active--bg--dark--hover: $accent--blue--dark--focus;
// Color Definitions: Tag
$tag--bg--light: $grey-60;
$tag--bg--dark: $grey-00;
@ -450,6 +478,9 @@ $tag--text--dark: $grey-50;
$wind--bg--light: $wind-bg-10;
$wind--bg--dark: $wind-bg-10;
$wind--portrait--bg--light: $wind-bg-20;
$wind--portrait--bg--dark: $wind-bg-20;
$wind--bg--hover--light: $wind-bg-00;
$wind--bg--hover--dark: $wind-bg-00;
@ -494,6 +525,9 @@ $null--shadow--dark--hover: fade-out($grey-10, 0.3);
$fire--bg--light: $fire-bg-10;
$fire--bg--dark: $fire-bg-10;
$fire--portrait--bg--light: $fire-bg-20;
$fire--portrait--bg--dark: $fire-bg-20;
$fire--bg--hover--light: $fire-bg-00;
$fire--bg--hover--dark: $fire-bg-00;
@ -516,6 +550,9 @@ $fire--shadow--dark--hover: fade-out($fire-text-00, 0.3);
$water--bg--light: $water-bg-10;
$water--bg--dark: $water-bg-10;
$water--portrait--bg--light: $water-bg-20;
$water--portrait--bg--dark: $water-bg-20;
$water--bg--hover--light: $water-bg-00;
$water--bg--hover--dark: $water-bg-00;
@ -538,6 +575,9 @@ $water--shadow--dark--hover: fade-out($water-text-00, 0.3);
$earth--bg--light: $earth-bg-10;
$earth--bg--dark: $earth-bg-10;
$earth--portrait--bg--light: $earth-bg-20;
$earth--portrait--bg--dark: $earth-bg-20;
$earth--bg--hover--light: $earth-bg-00;
$earth--bg--hover--dark: $earth-bg-00;
@ -560,6 +600,9 @@ $earth--shadow--dark--hover: fade-out($earth-text-00, 0.3);
$dark--bg--light: $dark-bg-10;
$dark--bg--dark: $dark-bg-10;
$dark--portrait--bg--light: $dark-bg-20;
$dark--portrait--bg--dark: $dark-bg-20;
$dark--bg--hover--light: $dark-bg-00;
$dark--bg--hover--dark: $dark-bg-00;
@ -582,6 +625,9 @@ $dark--shadow--dark--hover: fade-out($dark-text-00, 0.3);
$light--bg--light: $light-bg-10;
$light--bg--dark: $light-bg-10;
$light--portrait--bg--light: $light-bg-20;
$light--portrait--bg--dark: $light-bg-20;
$light--bg--hover--light: $light-bg-00;
$light--bg--hover--dark: $light-bg-00;

1
types/Party.d.ts vendored
View file

@ -43,6 +43,7 @@ interface Party {
local_id?: string
remix: boolean
remixes: Party[]
visibility: number
created_at: string
updated_at: string
}

1
types/index.d.ts vendored
View file

@ -41,6 +41,7 @@ export type DetailsObject = {
job?: Job
extra?: boolean
guidebooks?: string[]
visibility?: number
}
export type ExtendedMastery = {

View file

@ -61,6 +61,7 @@ interface AppState {
favorited: boolean
remix: boolean
remixes: Party[]
visibility: number
sourceParty?: Party
created_at: string
updated_at: string
@ -128,6 +129,7 @@ export const initialAppState: AppState = {
favorited: false,
remix: false,
remixes: [],
visibility: 1,
sourceParty: undefined,
created_at: '',
updated_at: '',