Migrate to CSS modules (#335) (#336)

* Modify next.js to re-enable CSS modules

* Rename all files and fix imports

* Renaming index.scss files to index.module.scss
* Changing `import from` to `import styles from`

* Fix dialog styles

* Fix button styles

* Fix dropdown styles

* Fix overlay styles

* Fix segmented control styles

* Fix auth modals

* Fix input styles

* Fix grid rep styles

* Extract language switch component

* Fix party header styles

* Fix header styles

* Fix filter bar styles

* Fix token styles

* Remove tag style from globals

This moved to DropdownMenuItem as thats the only place it's currently used

* Add some shades of purple

* Fix tooltip styles

* Fix star styles

* Fix unit styles

* Fix grid styles

* Fix job styles

* Combine Input and CharLimitedFieldset

We fixed the input component and added a character counter to it, so we don't need a separate CharLimitedFieldSet anymore.

The input component has been simplified to *just* be an input component, so it no longer displays an error. We will make a new component for error handling and labeling. It will probably be an improvement on our custom Fieldset somehow.

* (WIP) Update auth modals for new Input

These rely on error handling and so will need to be fixed more in the future

* Clean up button component some more

Here we add a floating prop for displaying buttons on top of things, like in units. We also renamed contained to bound to match other components and added an icon size.

* Fix styles for perpetuity icon overlay

* Update units for floating button display

* Fix weapon skill overlay

* Add a specific variable for the save UI red

* Fix save button states

* Update raid combobox triggers

* Fix segmented controls

* Fix popover triggers

This is mostly a duplicate of SelectTrigger but CSS modules are deeply stupid, so we have to duplicate the code.

* Fix select classes

* Fix select item classes

* Fix context menus

* Remove console.log

* Update filter bar button

* Updated Select and SelectItem

Part of this was combining PictureSelectItem and SelectItem, so the former has been removed.

* Updated TableField and SelectTableField

* Updated toasts

* Updated AccountModal

* Added new themes and variables

* Fix hovercards

* Extracted header into HovercardHeader component

* Button improvements

* Allow for passing className to left and right accessory
* Rename contained to bound
* Rename buttonSize to size
* Add custom button styles

* Fix search filters

* Update styles for all search filters
* Make search filters function better on mobile
* Small refactor on individual filter bar files to extract individual search filter rendering into variables

* Update search modal styles

* Update input

Make a consistent height with select triggers and fix props

* Fix ExtraSummons and rename to ExtraSummonsGrid

* Fix search result item styles

* Update party footer

* Add segmented control to swap between remixes and description
* Fix styles

* Add local transition to overlay

* Pass down class name to Popover

* Other style changes for raid combobox
* Local keyframe animation

* Fix slider and switch components

* Update table field components

The structure of TableField's image props have changed

* Update PartyHeader and DropdownMenuItem

* Remove extraneous states and hooks from PartyHeader
* Only show PartyDropdown if we are looking at an existing party
* Add destructive prop for DropdownMenuItem
* Remove extraneous classes from PartyDropdown
* Localize dropdown contents

* Fix alert styles and overlays

* Update alert styles
* Fix Overlay component to take onClick event handler as a prop

* Add local animation to Tooltip

* Update GridRep

* Update job-related components

* Update select component

* Align the popover
* Pass down classes from props
* Adds local animation
* Remove modal style
* Add full width style

* Update RaidCombobox and RaidItem

Also removes RaidSelect, which has been removed

* Update object reps for mobile

* Update static pages

* Update extra weapons section

* ExtraContainer split into ExtraContainerItem
* Updated Guidebook result item, grid and unit
* Updated extra weapons grid and weapon grid

* Add missing animations to Toast

* Moved components to a new filters folder

* Fix Youtube and empty state in PartyFooter

* Fixed Youtube embed styles
* Added empty state for description tab

* Extracted filter bar user info into a new component

* Removed LabelledInput

* Added new Textarea component

This is a content editable div to prepare for when we add tagging and formatting

* Fix placeholders in SummonUnit

* Add extra colors to WeaponUnit

* Updated WeaponLabelIcon styles

* Update button prop labels

* Update auth components

Just moving import order and changing an unused class name

* Increase visibility of segmented control on static page

* Update FilterBar location and more

* Updates FilterBar import location
* Extracts user info into UserInfo component

* Update localizations

* Update button prop labels

* Update UncapIndicator display styles

* Update ExtraSummons to ExtraSummonsGrid

* Use small-tablet breakpoint for party reps

* Update Input and InputTableField

* Added error and label to input, in a fieldset
* Updated prop labels in InputTableField

* Center text on triggers on small screen sizes

* Update SelectGroup styles

* Update GridRep

* Remove link to user's profile—it was very distracting
* Increase mainhand max height so it doesn't appear too small when reps are larger

* Update SegmentedControl

* Forward refs to SegmentedControl
* Allow passing of className via props
* Specific styles for RaidCombobox and something else
* Use small-tablet breakpoint

* Update Segment styles

Notably, there's a nice transition now

* Remove unused style import

* Add custom Button styles

* Update proficiency typing

* Update PartyHeader and fix behavior

* Send true to editable prop is party is editable
* Fix turn count token display
* Fix party name style
* Add custom classes to various Buttons
* Only show PartyDropdown if a party is new
* Determine which buttons to show based on editable prop, not snapshot
* Remove unused code from Header
* Make new button route shallowly

* Add small-tablet breakpoint

* Update themes and variables

* Update globals.scss

* Don't show <img> when there is no icon

* Add prop for destructive dropdown menu items

* Update localizations

* Remove unused code

Dependencies and components that were no longer used

* Add lodash.isequal

We didn't end up using it but it might come in handy in the future

* Add custom styles for remixed pill

This pill displays when a party is a remix. We shrunk it so it wasn't quite the size of a normal small button, and then added disabled states for if the original party was deleted

* Use CSS modules with Command

We don't really use all of these exports, but we made it so that className gets passed properly to `styles` when we do

* Update DialogContent

* Shrunk max-height to 60vh, and remove it for search
* Added an explicit width, as using min/max-width interferes with the contentEditable div in EditPartyModal
* Added custom styles for EditPartyModal
* Removed unused styles

* Revert Command changes

This seems to rely on these specific styles and it works, so we'll leave it alone for now.

* Give visual focus state to close button

* Update DurationInput and remove old classes

* Update Input

* Add fieldsetClasses prop
* Fallback to an empty string if value is undefined
* Fix focus ring to be consistent with our other custom focus rings
* Fix placeholder color

* Hide text overflow in trigger

The Popover trigger (specifically for RaidCombobox) would stretch or break lines when given a long value. This makes it so that the text will always stay on one line and hide its overflow with an ellipsis if necessary

* Passes along the autoFocus prop to Select

This passes along the autoFocus prop to the root Select component, and exposes it in SelectTableField

* Fixes bug with SliderTablefield control

This fixes a bug where the SliderTableField's slider was not changing the input's value.

We essentially let the parent component control the value so the component is only ever reading from props, instead of using its stored state as a display.

* Fix placeholder text and formatting

This fixes Textarea's placeholder text to be consistent with Input, as well as allows us to use new lines in the placeholder

* Update ErrorSection styles

* Update FilterModal

* Fixes spacing of interactive elements in FilterBar so they don't stretch according to content anymore
* Adds new `persistFilters` prop that determines whether the FilterBar should persist any filters to the user's cookies
* Uses defaultFilterset prop to populate the default filter set instead of importing the actual "default filter set" and using it directly

* Update FilterModal

* Adds a notice alerting users that filters on profiles and the saved page do not persist
* Exposes `persistFilters` prop that will be passed to FilterBar and used to determine if the notice should be displayed
* Autofocuses the first select on the page

* Fix visual bugs in GridRep

* Fixes the mainhand height not always being full height when the container was being responsively resized
* Adjusts the color of empty grid rectangles for dark and light mode and when being hovered over

* Remove unused code

* Update EditPartyModal

* Directly adds shadow code from DialogHeader since this dialog behaves slightly differently. In the future, we'd like to reconcile this so that the code only appears once
* Changes rendering functions to be properties
* Add DialogHeader and DialogFooter
* Implement Textarea component instead of raw textarea
* Removed unused code

* Update Party component

* Moves tab state management to the parent to prevent flickering and re-rendering
* Fixes local ID saving so that unauth users can make parties again
* Fixes the saving and display of numeric values (button count, chain count, turn count)

* Add functionality to PartyFooter buttons

The "Edit info" and "Remix" buttons now have their proper functionality in PartyFooter, matching how they behave in PartyHeader

* Update PartyHeader

* Fixes the display of numeric properties (button count, turn count, chain count)
* Refactors remixed pill/button so that it displays a message if the original party was deleted

* Add missing localization

* Fix raid keyboard navigation

* We added a plain "raid" style that our keyboard navigation code can hook onto, so that you can navigate the RaidCombobox raid list with the up and down arrow keys
* Fixed the raid item background color when hovering or focused
* Removed unused code

* Add class to fieldset instead of input

* Don't show quick summon icon on subaura summons

* Update styles for extra weapon units

* Implement filter changes

User profiles and saved teams won't use a user's filter cookies or persist filters anymore

* Add missing localization for "Loading..."

* Add tab management to pages

Tab management was previously handled by `Party` but things are smoother and less flicker-y if we handle them on the pages themselves

* Update localizations

* Extract createLocalId into a util

We extracted createLocalId into a method outside of the new page. Now, it can be used as a fallback when fetching the local ID if that local ID doesn't exist yet

* Add permissive filter set

This is the default filter set on user profiles and the saved teams page

* Add a bunch of new colors and theme variables

* Notice variables for FilterModal
* Unit background variables for GridRep
* An array of accent yellow colors
* Modified disabled button values in dark theme
* Modified extra purple text color in dark theme

* Change NotFound to be a class instead of ID

* Move slideRight animation into Toast component

* Remove keyframes.scss

Unfortunately, CSS modules makes it unreasonably difficult to have a central repository of CSS animations and reuse them, so we have copied these into the stylesheets of components that use them.

* Remove keyframes.scss from globals

* Update styles for conflict modals

* The actual styles for these were in DialogContent and had been deleted, so we fetched them from a previous commit
* Conflict modals get added to the exception that gives them a taller max height
* We can probably combine the meat of these into a ConflictDiagram component

* Add keys to conflict buttons

* Fix conflict CSS

Was accidentally adding it to a declaration that was setting min-height instead of max-height

* Fix character conflict modal only appearing once

We weren't changing the modal open state to false

* Alert overlays should display over modals

We were using the same Overlay with no changes, so alerts would display over modals without an overlay behind them

* Add missing localization for earring errors

* Normalize over mastery object

The over mastery object was sometimes 0-index, sometimes 1-index. This normalizes it to be 1-indexed, even though that is a little silly. I think this is the lesser amount of work though, since normalizing against 0-index might require API changes

* Fix ExtendedMasterySelect styles

* Fix RingSelect styles and functionality

* Updates styles for CSS modules
* Updates for normalized 1-index object
* Properly falls back to 0 value if value is not set

* Normalize 1-index for over mastery

* Fix AwakeningSelectWithInput styles and functionality

* Adapts styles for CSS modules
* Properly sends validity
* Reordered errors

* Fix SelectWithInput styles and functionality

* Adapts styles for CSS modules
* Add name to errors
* Properly sends validity

* Add extra modifier styles to Input/Select

* Update CharacterModal

* Adapts styles for CSS modules
* Adds an alert if the user tries to close a dialog with changes without saving
* Uses constants instead of functions for rendering helpers
* Fixes validation

* Reset values when the dialog is closed

The way we handle state means that we will keep old, unsaved values around if we don't do this

* Move GridWeaponObject to types

* Add unsaved changes localizations

* Localize unsaved changes alert

* Increase spacing of range mod style

* Update ElementToggle to use CSS modules

* Refactor WeaponKeySelect

No longer makes an API call for each instantiation—instead we use the weapon keys downloaded on the server

* Update AxSelect for CSS modules

* Update weapon should happen in WeaponUnit

Previously, this happened in WeaponModal. It happens in CharacterUnit on that end, so this change brings us in line with how we're doing things elsewhere

* Update WeaponModal to incorporate latest changes

* Adds unsaved changes alert
* Updates to use refactored WeaponKeySelect
* Moves api code to parent via a updateWeapon prop
* Updates to use DialogHeader and DialogFooter
* Makes rendering functions into constants

* Set grid weapon element when downloaded

* Make things that should be bound, bound

* Update elemental colors

This makes elemental accent colors themed more consistently

* Add confirmation alert to Edit Party modal

* Fix how description is tested for changes

* Fix footer shadow in EditPartyModal

* Fix footer shadows for all other modals

Also removes default box-shadow and border-top

* Fix awakening modification check

Awakening wasn't being set when the modal loaded, so it was testing the gridWeapon value against undefined

* Use new element variables

* h5 in globals
* Buttons

* Don't show icon for balanced character awakening

Also, remove old CSS

* Small cleanup of parseInt

* Fix weapon element logic

We had broken null weapons changing sprites when the element was changed, and the change detection was also broken. Some more stringent logic checks fixed both.

* Fix more raid color stuff

This should be it for real this time

* Show AX section in WeaponHovercard

Was testing truthy/falsy which meant id 0 made it not display

* Fix padding so focus ring isn't cut off

* Refactor Header and add logout confirmation

* Fix page navigation when filtering collections

There was a bug that kept page navigation from working properly when filtering. Things would load multiple times, or load the wrong thing, or not navigate properly. That should all be fixed now.

* Fix styles for when a collection has no teams

* Fix Nextjs build errors
This commit is contained in:
Justin Edmund 2023-07-04 00:43:49 -07:00 committed by GitHub
parent 8cbdb1838d
commit 9c3c36e81b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
301 changed files with 8267 additions and 6976 deletions

View file

@ -1,4 +1,4 @@
.ToggleGroup {
.group {
$height: 36px;
background-color: var(--toggle-bg);
@ -15,7 +15,7 @@
height: auto;
}
.ToggleItem {
.item {
background: var(--toggle-bg);
border: none;
border-radius: 18px;
@ -47,32 +47,32 @@
&.fire {
background: var(--fire-bg);
color: var(--fire-hover-text);
color: var(--fire-text-bg);
}
&.water {
background: var(--water-bg);
color: var(--water-hover-text);
color: var(--water-text-bg);
}
&.earth {
background: var(--earth-bg);
color: var(--earth-hover-text);
color: var(--earth-text-bg);
}
&.wind {
background: var(--wind-bg);
color: var(--wind-hover-text);
color: var(--wind-text-bg);
}
&.dark {
background: var(--dark-bg);
color: var(--dark-hover-text);
color: var(--dark-text-bg);
}
&.light {
background: var(--light-bg);
color: var(--light-hover-text);
color: var(--light-text-bg);
}
}
}

View file

@ -1,74 +1,114 @@
import React from 'react'
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import classNames from 'classnames'
import * as ToggleGroup from '@radix-ui/react-toggle-group'
import './index.scss'
import styles from './index.module.scss'
interface Props {
currentElement: number
sendValue: (value: string) => void
sendValue: (value: number) => void
}
const ElementToggle = (props: Props) => {
const ElementToggle = ({ currentElement, sendValue, ...props }: Props) => {
// Router and localization
const router = useRouter()
const { t } = useTranslation('common')
const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const { t } = useTranslation('common')
// State: Component
const [element, setElement] = useState(currentElement)
// Methods: Handlers
const handleElementChange = (value: string) => {
const newElement = parseInt(value)
setElement(newElement)
sendValue(newElement)
}
// Methods: Rendering
return (
<ToggleGroup.Root
className="ToggleGroup"
className={styles.group}
type="single"
defaultValue={`${props.currentElement}`}
value={`${element}`}
aria-label="Element"
onValueChange={props.sendValue}
onValueChange={handleElementChange}
>
<ToggleGroup.Item
className={`ToggleItem ${locale}`}
className={classNames({
[styles.item]: true,
[styles[`${locale}`]]: true,
})}
value="0"
aria-label="null"
>
{t('elements.null')}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem wind ${locale}`}
className={classNames({
[styles.item]: true,
[styles.wind]: true,
[styles[`${locale}`]]: true,
})}
value="1"
aria-label="wind"
>
{t('elements.wind')}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem fire ${locale}`}
className={classNames({
[styles.item]: true,
[styles.fire]: true,
[styles[`${locale}`]]: true,
})}
value="2"
aria-label="fire"
>
{t('elements.fire')}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem water ${locale}`}
className={classNames({
[styles.item]: true,
[styles.water]: true,
[styles[`${locale}`]]: true,
})}
value="3"
aria-label="water"
>
{t('elements.water')}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem earth ${locale}`}
className={classNames({
[styles.item]: true,
[styles.earth]: true,
[styles[`${locale}`]]: true,
})}
value="4"
aria-label="earth"
>
{t('elements.earth')}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem dark ${locale}`}
className={classNames({
[styles.item]: true,
[styles.dark]: true,
[styles[`${locale}`]]: true,
})}
value="5"
aria-label="dark"
>
{t('elements.dark')}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem light ${locale}`}
className={classNames({
[styles.item]: true,
[styles.light]: true,
[styles[`${locale}`]]: true,
})}
value="6"
aria-label="light"
>

View file

@ -1,4 +1,4 @@
section.Error {
.error {
align-items: center;
display: flex;
flex-direction: column;
@ -9,14 +9,13 @@ section.Error {
height: 60vh;
text-align: center;
.Code {
.code {
color: var(--text-secondary);
font-size: $font-tiny;
font-weight: $bold;
}
.Button {
margin-top: $unit-2x;
width: fit-content;
p {
margin-bottom: $unit-4x;
}
}

View file

@ -5,7 +5,7 @@ import { useTranslation } from 'next-i18next'
import Button from '~components/common/Button'
import { ResponseStatus } from '~types'
import './index.scss'
import styles from './index.module.scss'
interface Props {
status: ResponseStatus
@ -24,7 +24,7 @@ const ErrorSection = ({ status }: Props) => {
const errorBody = () => {
return (
<>
<div className="Code">{status.code}</div>
<div className={styles.code}>{status.code}</div>
<h1>{t(`errors.${statusText}.title`)}</h1>
<p>{t(`errors.${statusText}.description`)}</p>
</>
@ -32,7 +32,7 @@ const ErrorSection = ({ status }: Props) => {
}
return (
<section className="Error">
<section className={styles.error}>
{errorBody()}
{[401, 404].includes(status.code) ? (
<Link href="/new">

View file

@ -1,143 +0,0 @@
.FilterBar {
align-items: center;
background: var(--bar-bg);
border-radius: $card-corner;
box-sizing: border-box;
display: flex;
flex-direction: row;
gap: $unit-2x;
margin: 0 auto;
margin-top: 7px; // Line up with HeaderMenu
padding: $unit * 2;
position: sticky;
transition: box-shadow 0.24s ease-in-out;
top: $unit * 4;
width: 100%;
max-width: 996px;
min-height: 80px;
@include breakpoint(tablet) {
position: static;
flex-direction: column;
width: 100%;
}
@include breakpoint(phone) {
min-height: auto;
}
.Filters {
display: flex;
box-sizing: border-box;
flex-direction: row;
flex-grow: 1;
gap: $unit;
width: auto;
@include breakpoint(tablet) {
flex-direction: column;
width: 100%;
}
.Button.Filter.Blended {
&.FiltersActive .Accessory svg {
fill: var(--accent-blue);
stroke: none;
}
&:hover {
background: var(--button-bg);
}
.Accessory svg {
fill: none;
stroke: var(--button-text);
width: 18px;
height: 18px;
}
}
}
&.shadow {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.14);
}
h1 {
color: var(--text-primary);
font-size: $font-regular;
font-weight: $normal;
flex-grow: 1;
text-align: left;
}
select,
.SelectTrigger {
// background: url("/icons/Chevron.svg"), $grey-90;
// background-repeat: no-repeat;
// background-position-y: center;
// background-position-x: 95%;
// background-size: $unit * 1.5;
background-color: var(--select-contained-bg);
font-size: $font-small;
margin: 0;
max-width: 200px;
&:hover {
background-color: var(--select-contained-bg-hover);
}
@include breakpoint(tablet) {
width: 100%;
max-width: inherit;
text-align: center;
}
}
.SelectTrigger {
width: 100%;
span {
font-size: $font-small;
}
}
.Filter.Button {
justify-content: center;
.Text {
display: none;
width: auto;
@include breakpoint(tablet) {
display: block;
}
@include breakpoint(phone) {
display: block;
}
}
}
.UserInfo {
align-items: center;
display: flex;
flex-direction: row;
flex-grow: 1;
gap: $unit * 1.5;
img {
$diameter: $unit * 6;
border-radius: calc($diameter / 2);
height: $diameter;
width: $diameter;
&.gran {
background-color: #cee7fe;
}
&.djeeta {
background-color: #ffe1fe;
}
}
}
}

View file

@ -1,15 +0,0 @@
.Dialog {
.Filter.DialogContent {
overflow: hidden;
.TableField .Right .SelectTrigger.Table {
width: $unit-20x;
min-width: auto;
}
}
.DialogFooter .Buttons .Button.Blended {
padding-left: 0;
padding-right: 0;
}
}

View file

@ -1,5 +1,6 @@
.GridRep {
.gridRep {
aspect-ratio: 3/2;
border: 1px solid transparent;
border-radius: $card-corner;
box-sizing: border-box;
display: grid;
@ -11,18 +12,18 @@
&:hover {
background: var(--grid-rep-hover);
border: 1px solid rgba(0, 0, 0, 0.1);
a {
text-decoration: none;
}
h2,
.Grid {
.weaponGrid {
cursor: pointer;
}
.Grid .Weapon {
box-shadow: inset 0 0 0 1px var(--grid-border-color);
.weapon {
background: var(--unit-bg-hover);
}
}
@include breakpoint(phone) {
@ -34,25 +35,25 @@
}
}
& > .Grid {
& > .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 */
grid-gap: $unit; /* add a gap of 8px between grid items */
.Weapon {
background: var(--card-bg);
.weapon {
background: var(--unit-bg);
border-radius: 4px;
}
.Mainhand.Weapon {
.mainhand.weapon {
aspect-ratio: 73/153;
display: grid;
grid-column: 1 / 2; /* spans one column */
max-height: 140px;
height: calc(100% - $unit-fourth);
}
.GridWeapons {
.weapons {
display: grid; /* make the right-images container a grid */
grid-template-columns: repeat(
3,
@ -67,26 +68,27 @@
// row-gap: $unit-2x;
}
.Grid.Weapon {
.grid.weapon {
aspect-ratio: 280 / 160;
display: grid;
}
.Mainhand.Weapon img[src*='jpg'],
.Grid.Weapon img[src*='jpg'] {
.mainhand.weapon img[src*='jpg'],
.grid.weapon img[src*='jpg'] {
border-radius: 4px;
width: 100%;
}
}
.Details {
.details {
display: flex;
flex-direction: column;
gap: calc($unit / 2);
gap: $unit;
h2 {
color: var(--text-primary);
font-size: $font-regular;
font-weight: $bold;
overflow: hidden;
padding-bottom: 1px;
text-overflow: ellipsis;
@ -117,16 +119,21 @@
}
}
.attributed,
.bottom {
display: flex;
flex-direction: row;
gap: $unit-half;
justify-content: space-between;
a.user:hover {
color: var(--link-text-hover);
}
}
.Properties,
.bottom {
flex-direction: column;
}
.user {
flex-grow: 1;
}
@ -134,23 +141,44 @@
.user,
.raid,
time {
color: $grey-55;
color: var(--text-tertiary);
font-size: $font-small;
}
.Properties {
time {
white-space: nowrap;
}
.properties {
display: flex;
font-size: $font-small;
gap: $unit-half;
.raid {
white-space: nowrap;
text-overflow: ellipsis;
}
.auto {
flex: 1;
display: inline-flex;
gap: $unit-half;
flex-direction: row;
margin-left: $unit-half;
flex-wrap: nowrap;
}
.full_auto {
.fullAuto {
color: var(--full-auto-label-text);
white-space: nowrap;
}
.auto_guard {
.extra {
color: var(--extra-purple-light-text);
white-space: nowrap;
}
.autoGuard {
display: inline-block;
width: 12px;
height: 12px;
@ -162,7 +190,6 @@
.raid {
color: var(--text-primary);
margin-bottom: calc($unit / 2);
&.empty {
color: var(--text-tertiary);

View file

@ -13,7 +13,7 @@ import Button from '~components/common/Button'
import SaveIcon from '~public/icons/Save.svg'
import ShieldIcon from '~public/icons/Shield.svg'
import './index.scss'
import styles from './index.module.scss'
interface Props {
shortcode: string
@ -26,7 +26,6 @@ interface Props {
autoGuard: boolean
favorited: boolean
createdAt: Date
displayUser?: boolean | false
onClick: (shortcode: string) => void
onSave?: (partyId: string, favorited: boolean) => void
}
@ -50,13 +49,23 @@ const GridRep = (props: Props) => {
})
const raidClass = classNames({
raid: true,
empty: !props.raid,
[styles.raid]: true,
[styles.empty]: !props.raid,
})
const userClass = classNames({
user: true,
empty: !props.user,
[styles.user]: true,
[styles.empty]: !props.user,
})
const mainhandClasses = classNames({
[styles.weapon]: true,
[styles.mainhand]: true,
})
const weaponClasses = classNames({
[styles.weapon]: true,
[styles.grid]: true,
})
useEffect(() => {
@ -156,76 +165,52 @@ const GridRep = (props: Props) => {
)
}
const linkedAttribution = () => (
<Link href={`/${props.user ? props.user.username : '#'}`}>
const attribution = () => (
<span className={userClass}>
{userImage()}
{props.user ? props.user.username : t('no_user')}
</span>
</Link>
)
const unlinkedAttribution = () => (
<div className={userClass}>
{userImage()}
{props.user ? props.user.username : t('no_user')}
</div>
)
function fullAutoString() {
const fullAutoElement = (
<span className="full_auto">
<span className={styles.fullAuto}>
{` · ${t('party.details.labels.full_auto')}`}
</span>
)
const autoGuardElement = (
<span className="auto_guard">
<span className={styles.autoGuard}>
<ShieldIcon />
</span>
)
return (
<div className="auto">
<div className={styles.auto}>
{fullAutoElement}
{props.autoGuard ? autoGuardElement : ''}
</div>
)
}
const details = (
<div className="Details">
<h2 className={titleClass}>{props.name ? props.name : t('no_title')}</h2>
<div className="bottom">
<div className="Properties">
<span className={raidClass}>
{props.raid ? props.raid.name[locale] : t('no_raid')}
</span>
{props.fullAuto ? fullAutoString() : ''}
</div>
<time className="last-updated" dateTime={props.createdAt.toISOString()}>
{formatTimeAgo(props.createdAt, locale)}
</time>
</div>
</div>
)
const detailsWithUsername = (
<div className="Details">
<div className="top">
<div className="info">
<div className={styles.details}>
<div className={styles.top}>
<div className={styles.info}>
<h2 className={titleClass}>
{props.name ? props.name : t('no_title')}
</h2>
<div className="Properties">
<div className={styles.properties}>
<span className={raidClass}>
{props.raid ? props.raid.name[locale] : t('no_raid')}
</span>
{props.fullAuto ? (
<span className="full_auto">
{props.fullAuto && (
<span className={styles.fullAuto}>
{` · ${t('party.details.labels.full_auto')}`}
</span>
) : (
''
)}
{props.raid && props.raid.group.extra && (
<span className={styles.extra}>{` · EX`}</span>
)}
</div>
</div>
@ -234,11 +219,14 @@ const GridRep = (props: Props) => {
!props.user) ? (
<Link href="#">
<Button
className="Save"
className={classNames({
save: true,
saved: props.favorited,
})}
leftAccessoryIcon={<SaveIcon className="stroke" />}
active={props.favorited}
contained={true}
buttonSize="small"
bound={true}
size="small"
onClick={sendSaveData}
/>
</Link>
@ -246,10 +234,13 @@ const GridRep = (props: Props) => {
''
)}
</div>
<div className="bottom">
{props.user ? linkedAttribution() : unlinkedAttribution()}
<div className={styles.attributed}>
{attribution()}
<time className="last-updated" dateTime={props.createdAt.toISOString()}>
<time
className={styles.lastUpdated}
dateTime={props.createdAt.toISOString()}
>
{formatTimeAgo(props.createdAt, locale)}
</time>
</div>
@ -258,15 +249,15 @@ const GridRep = (props: Props) => {
return (
<Link href={`/p/${props.shortcode}`}>
<a className="GridRep">
{props.displayUser ? detailsWithUsername : details}
<div className="Grid">
<div className="Mainhand Weapon">{generateMainhandImage()}</div>
<a className={styles.gridRep}>
{detailsWithUsername}
<div className={styles.weaponGrid}>
<div className={mainhandClasses}>{generateMainhandImage()}</div>
<ul className="GridWeapons">
<ul className={styles.weapons}>
{Array.from(Array(numWeapons)).map((x, i) => {
return (
<li key={`${props.shortcode}-${i}`} className="Grid Weapon">
<li key={`${props.shortcode}-${i}`} className={weaponClasses}>
{generateGridImage(i)}
</li>
)

View file

@ -1,4 +1,4 @@
.GridRepCollection {
.collection {
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));

View file

@ -1,18 +1,12 @@
import classNames from 'classnames'
import React from 'react'
import './index.scss'
import styles from './index.module.scss'
interface Props {
children: React.ReactNode
}
const GridRepCollection = (props: Props) => {
const classes = classNames({
GridRepCollection: true,
})
return <div className={classes}>{props.children}</div>
return <div className={styles.collection}>{props.children}</div>
}
export default GridRepCollection

View file

@ -1,4 +1,4 @@
#Header {
.header {
display: flex;
flex-direction: row;
margin-bottom: $unit;
@ -22,7 +22,7 @@
background: var(--placeholder-bg);
}
#DropdownWrapper {
.dropdownWrapper {
display: inline-block;
padding-bottom: $unit;
@ -32,8 +32,6 @@
}
&:hover {
// padding-right: $unit-4x;
.Button {
background: var(--button-bg-hover);
color: var(--button-text-hover);

View file

@ -1,40 +1,36 @@
import React, { useEffect, useState } from 'react'
import { subscribe, useSnapshot } from 'valtio'
import { setCookie, deleteCookie } from 'cookies-next'
import React, { useState } from 'react'
import { deleteCookie } from 'cookies-next'
import { useRouter } from 'next/router'
import { Trans, useTranslation } from 'next-i18next'
import { useTranslation } from 'next-i18next'
import classNames from 'classnames'
import clonedeep from 'lodash.clonedeep'
import Link from 'next/link'
import { accountState, initialAccountState } from '~utils/accountState'
import { appState, initialAppState } from '~utils/appState'
import { getLocalId } from '~utils/localId'
import { retrieveLocaleCookies } from '~utils/retrieveCookies'
import { setEditKey, storeEditKey } from '~utils/userToken'
import Alert from '~components/common/Alert'
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuLabel,
} from '~components/common/DropdownMenuContent'
import DropdownMenuGroup from '~components/common/DropdownMenuGroup'
import DropdownMenuLabel from '~components/common/DropdownMenuLabel'
import DropdownMenuItem from '~components/common/DropdownMenuItem'
import LanguageSwitch from '~components/LanguageSwitch'
import LoginModal from '~components/auth/LoginModal'
import SignupModal from '~components/auth/SignupModal'
import AccountModal from '~components/auth/AccountModal'
import Toast from '~components/common/Toast'
import Button from '~components/common/Button'
import Tooltip from '~components/common/Tooltip'
import * as Switch from '@radix-ui/react-switch'
import ChevronIcon from '~public/icons/Chevron.svg'
import MenuIcon from '~public/icons/Menu.svg'
import PlusIcon from '~public/icons/Add.svg'
import './index.scss'
import styles from './index.module.scss'
const Header = () => {
// Localization
@ -44,37 +40,14 @@ const Header = () => {
const router = useRouter()
const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const localeData = retrieveLocaleCookies()
// State management
const [remixToastOpen, setRemixToastOpen] = useState(false)
const [alertOpen, setAlertOpen] = useState(false)
const [loginModalOpen, setLoginModalOpen] = useState(false)
const [signupModalOpen, setSignupModalOpen] = useState(false)
const [settingsModalOpen, setSettingsModalOpen] = useState(false)
const [leftMenuOpen, setLeftMenuOpen] = useState(false)
const [rightMenuOpen, setRightMenuOpen] = useState(false)
const [languageChecked, setLanguageChecked] = useState(false)
const [name, setName] = useState('')
const [originalName, setOriginalName] = useState('')
// Snapshots
const { party: partySnapshot } = useSnapshot(appState)
// Subscribe to app state to listen for party name and
// unsubscribe when component is unmounted
const unsubscribe = subscribe(appState, () => {
const newName =
appState.party && appState.party.name ? appState.party.name : ''
setName(newName)
})
useEffect(() => () => unsubscribe(), [])
// Hooks
useEffect(() => {
setLanguageChecked(localeData === 'ja' ? true : false)
}, [localeData])
// Methods: Event handlers (Buttons)
function handleLeftMenuButtonClicked() {
@ -102,15 +75,6 @@ const Header = () => {
setRightMenuOpen(false)
}
// Methods: Event handlers (Remix toasts)
function handleRemixToastOpenChanged(open: boolean) {
setRemixToastOpen(open)
}
function handleRemixToastCloseClicked() {
setRemixToastOpen(false)
}
// Methods: Actions
function handleNewTeam(event: React.MouseEvent) {
event.preventDefault()
@ -118,15 +82,6 @@ const Header = () => {
closeRightMenu()
}
function changeLanguage(value: boolean) {
const language = value ? 'ja' : 'en'
const expiresAt = new Date()
expiresAt.setDate(expiresAt.getDate() + 120)
setCookie('NEXT_LOCALE', language, { path: '/', expires: expiresAt })
router.push(router.asPath, undefined, { locale: language })
}
function logout() {
// Close menu
closeRightMenu()
@ -153,15 +108,14 @@ const Header = () => {
})
// Push the root URL
router.push('/new')
router.push('/new', undefined, { shallow: true })
}
// Methods: Rendering
const profileImage = () => {
let image
const user = accountState.account.user
if (accountState.account.authorized && user) {
image = (
return (
<img
alt={user.username}
className={`profile ${user.avatar.element}`}
@ -171,7 +125,7 @@ const Header = () => {
/>
)
} else {
image = (
return (
<img
alt={t('no_user')}
className={`profile anonymous`}
@ -181,13 +135,10 @@ const Header = () => {
/>
)
}
return image
}
// Rendering: Buttons
const newButton = () => {
return (
const newButton = (
<Tooltip content={t('tooltips.new')}>
<Button
leftAccessoryIcon={<PlusIcon />}
@ -198,114 +149,50 @@ const Header = () => {
/>
</Tooltip>
)
}
const remixToast = () => {
return (
<Toast
altText={t('toasts.remixed', { title: originalName })}
open={remixToastOpen}
duration={2400}
type="foreground"
content={
<Trans i18nKey="toasts.remixed">
You remixed <strong>{{ title: originalName }}</strong>
</Trans>
}
onOpenChange={handleRemixToastOpenChanged}
onCloseClick={handleRemixToastCloseClicked}
/>
)
}
// Rendering: Modals
const settingsModal = () => {
const user = accountState.account.user
const logoutConfirmationAlert = (
<Alert
message={t('alert.confirm_logout')}
open={alertOpen}
primaryActionText="Log out"
primaryAction={logout}
cancelActionText="Nevermind"
cancelAction={() => setAlertOpen(false)}
/>
)
if (user) {
return (
const settingsModal = (
<>
{accountState.account.user && (
<AccountModal
open={settingsModalOpen}
username={user.username}
picture={user.avatar.picture}
gender={user.gender}
language={user.language}
theme={user.theme}
username={accountState.account.user.username}
picture={accountState.account.user.avatar.picture}
gender={accountState.account.user.gender}
language={accountState.account.user.language}
theme={accountState.account.user.theme}
onOpenChange={setSettingsModalOpen}
/>
)}
</>
)
}
}
const loginModal = () => {
return <LoginModal open={loginModalOpen} onOpenChange={setLoginModalOpen} />
}
const loginModal = (
<LoginModal open={loginModalOpen} onOpenChange={setLoginModalOpen} />
)
const signupModal = () => {
return (
const signupModal = (
<SignupModal open={signupModalOpen} onOpenChange={setSignupModalOpen} />
)
}
// Rendering: Compositing
const left = () => {
return (
<section>
<div id="DropdownWrapper">
<DropdownMenu
open={leftMenuOpen}
onOpenChange={handleLeftMenuOpenChange}
>
<DropdownMenuTrigger asChild>
<Button
leftAccessoryIcon={<MenuIcon />}
className={classNames({ Active: leftMenuOpen })}
blended={true}
onClick={handleLeftMenuButtonClicked}
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="Left">
{leftMenuItems()}
</DropdownMenuContent>
</DropdownMenu>
</div>
</section>
)
}
const right = () => {
return (
<section>
{newButton()}
<DropdownMenu
open={rightMenuOpen}
onOpenChange={handleRightMenuOpenChange}
>
<DropdownMenuTrigger asChild>
<Button
className={classNames({ Active: rightMenuOpen })}
leftAccessoryIcon={profileImage()}
rightAccessoryIcon={<ChevronIcon />}
rightAccessoryClassName="Arrow"
onClick={handleRightMenuButtonClicked}
blended={true}
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="Right">
{rightMenuItems()}
</DropdownMenuContent>
</DropdownMenu>
</section>
)
}
const leftMenuItems = () => {
return (
const authorizedLeftItems = (
<>
{accountState.account.authorized && accountState.account.user ? (
{accountState.account.user && (
<>
<DropdownMenuGroup className="MenuGroup">
<DropdownMenuItem className="MenuItem" onClick={closeLeftMenu}>
<DropdownMenuGroup>
<DropdownMenuItem onClick={closeLeftMenu}>
<Link
href={`/${accountState.account.user.username}` || ''}
passHref
@ -313,27 +200,33 @@ const Header = () => {
<span>{t('menu.profile')}</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem className="MenuItem" onClick={closeLeftMenu}>
<DropdownMenuItem onClick={closeLeftMenu}>
<Link href={`/saved` || ''}>{t('menu.saved')}</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
</>
) : (
''
)}
<DropdownMenuGroup className="MenuGroup">
<DropdownMenuItem className="MenuItem" onClick={closeLeftMenu}>
</>
)
const leftMenuItems = (
<>
{accountState.account.authorized &&
accountState.account.user &&
authorizedLeftItems}
<DropdownMenuGroup>
<DropdownMenuItem onClick={closeLeftMenu}>
<Link href="/teams">{t('menu.teams')}</Link>
</DropdownMenuItem>
<DropdownMenuItem className="MenuItem">
<DropdownMenuItem>
<div>
<span>{t('menu.guides')}</span>
<i className="tag">{t('coming_soon')}</i>
</div>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuGroup className="MenuGroup">
<DropdownMenuItem className="MenuItem" onClick={closeLeftMenu}>
<DropdownMenuGroup>
<DropdownMenuItem onClick={closeLeftMenu}>
<a
href={locale == 'ja' ? '/ja/about' : '/about'}
target="_blank"
@ -342,7 +235,7 @@ const Header = () => {
{t('about.segmented_control.about')}
</a>
</DropdownMenuItem>
<DropdownMenuItem className="MenuItem" onClick={closeLeftMenu}>
<DropdownMenuItem onClick={closeLeftMenu}>
<a
href={locale == 'ja' ? '/ja/updates' : '/updates'}
target="_blank"
@ -351,7 +244,7 @@ const Header = () => {
{t('about.segmented_control.updates')}
</a>
</DropdownMenuItem>
<DropdownMenuItem className="MenuItem" onClick={closeLeftMenu}>
<DropdownMenuItem onClick={closeLeftMenu}>
<a
href={locale == 'ja' ? '/ja/roadmap' : '/roadmap'}
target="_blank"
@ -363,54 +256,73 @@ const Header = () => {
</DropdownMenuGroup>
</>
)
}
const rightMenuItems = () => {
let items
const left = (
<section>
<div className={styles.dropdownWrapper}>
<DropdownMenu
open={leftMenuOpen}
onOpenChange={handleLeftMenuOpenChange}
>
<DropdownMenuTrigger asChild>
<Button
active={leftMenuOpen}
blended={true}
leftAccessoryIcon={<MenuIcon />}
onClick={handleLeftMenuButtonClicked}
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="Left">
{leftMenuItems}
</DropdownMenuContent>
</DropdownMenu>
</div>
</section>
)
const account = accountState.account
if (account.authorized && account.user) {
items = (
const authorizedRightItems = (
<>
<DropdownMenuGroup className="MenuGroup">
<DropdownMenuLabel className="MenuLabel">
{account.user ? `@${account.user.username}` : t('no_user')}
{accountState.account.user && (
<>
<DropdownMenuGroup>
<DropdownMenuLabel>
{`@${accountState.account.user.username}`}
</DropdownMenuLabel>
<DropdownMenuItem className="MenuItem" onClick={closeRightMenu}>
<Link href={`/${account.user.username}` || ''} passHref>
<DropdownMenuItem onClick={closeRightMenu}>
<Link
href={`/${accountState.account.user.username}` || ''}
passHref
>
<span>{t('menu.profile')}</span>
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup className="MenuGroup">
<DropdownMenuGroup>
<DropdownMenuItem
className="MenuItem"
onClick={() => setSettingsModalOpen(true)}
>
<span>{t('menu.settings')}</span>
</DropdownMenuItem>
<DropdownMenuItem className="MenuItem" onClick={logout}>
<DropdownMenuItem
onClick={() => setAlertOpen(true)}
destructive={true}
>
<span>{t('menu.logout')}</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</>
)}
</>
)
} else {
items = (
const unauthorizedRightItems = (
<>
<DropdownMenuGroup className="MenuGroup">
<DropdownMenuItem className="MenuItem language">
<DropdownMenuGroup>
<DropdownMenuItem className="language">
<span>{t('menu.language')}</span>
<Switch.Root
className="Switch"
onCheckedChange={changeLanguage}
checked={languageChecked}
>
<Switch.Thumb className="Thumb" />
<span className="left">JP</span>
<span className="right">EN</span>
</Switch.Root>
<LanguageSwitch />
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuGroup className="MenuGroup">
@ -429,18 +341,47 @@ const Header = () => {
</DropdownMenuGroup>
</>
)
}
return items
}
const rightMenuItems = (
<>
{accountState.account.authorized && accountState.account.user
? authorizedRightItems
: unauthorizedRightItems}
</>
)
const right = (
<section>
{newButton}
<DropdownMenu
open={rightMenuOpen}
onOpenChange={handleRightMenuOpenChange}
>
<DropdownMenuTrigger asChild>
<Button
className={classNames({ Active: rightMenuOpen })}
leftAccessoryIcon={profileImage()}
rightAccessoryIcon={<ChevronIcon />}
rightAccessoryClassName="Arrow"
onClick={handleRightMenuButtonClicked}
blended={true}
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="Right">
{rightMenuItems}
</DropdownMenuContent>
</DropdownMenu>
</section>
)
return (
<nav id="Header">
{left()}
{right()}
{settingsModal()}
{loginModal()}
{signupModal()}
<nav className={styles.header}>
{left}
{right}
{logoutConfirmationAlert}
{settingsModal}
{loginModal}
{signupModal}
</nav>
)
}

View file

@ -0,0 +1,57 @@
.root {
display: flex;
flex-direction: column;
gap: calc($unit / 2);
.title {
align-items: center;
display: flex;
flex-direction: row;
gap: $unit * 2;
h4 {
flex-grow: 1;
font-size: $font-medium;
line-height: 1.2;
min-width: 140px;
}
img {
height: auto;
width: 100px;
}
.image {
position: relative;
.perpetuity {
position: absolute;
background-image: url('/icons/perpetuity/filled.svg');
background-size: $unit-3x $unit-3x;
z-index: 20;
top: $unit-half * -1;
right: $unit-3x;
width: $unit-3x;
height: $unit-3x;
}
}
}
.subInfo {
align-items: center;
display: flex;
flex-direction: row;
gap: $unit * 2;
.icons {
display: flex;
flex-direction: row;
flex-grow: 1;
gap: $unit;
}
.UncapIndicator {
min-width: 100px;
}
}
}

View file

@ -0,0 +1,150 @@
import { useRouter } from 'next/router'
import UncapIndicator from '~components/uncap/UncapIndicator'
import WeaponLabelIcon from '~components/weapon/WeaponLabelIcon'
import styles from './index.module.scss'
interface Props {
gridObject: GridCharacter | GridSummon | GridWeapon
object: Character | Summon | Weapon
type: 'character' | 'summon' | 'weapon'
}
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const Proficiency = [
'none',
'sword',
'dagger',
'axe',
'spear',
'bow',
'staff',
'fist',
'harp',
'gun',
'katana',
]
const HovercardHeader = ({ gridObject, object, type, ...props }: Props) => {
const router = useRouter()
const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const overlay = () => {
if (type === 'character') {
const gridCharacter = gridObject as GridCharacter
if (gridCharacter.perpetuity) return <i className={styles.perpetuity} />
} else if (type === 'summon') {
const gridSummon = gridObject as GridSummon
if (gridSummon.quick_summon) return <i className={styles.quickSummon} />
}
}
const characterImage = () => {
const gridCharacter = gridObject as GridCharacter
const character = object as Character
// Change the image based on the uncap level
let suffix = '01'
if (gridCharacter.uncap_level == 6) suffix = '04'
else if (gridCharacter.uncap_level == 5) suffix = '03'
else if (gridCharacter.uncap_level > 2) suffix = '02'
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_${suffix}.jpg`
}
const summonImage = () => {
const summon = object as Summon
const gridSummon = gridObject as GridSummon
const upgradedSummons = [
'2040094000',
'2040100000',
'2040080000',
'2040098000',
'2040090000',
'2040084000',
'2040003000',
'2040056000',
]
let suffix = ''
if (
upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 &&
gridSummon.uncap_level == 5
) {
suffix = '_02'
} else if (
gridSummon.object.uncap.xlb &&
gridSummon.transcendence_step > 0
) {
suffix = '_03'
}
// Generate the correct source for the summon
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg`
}
const weaponImage = () => {
const gridWeapon = gridObject as GridWeapon
const weapon = object as Weapon
if (gridWeapon.object.element == 0 && gridWeapon.element)
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}_${gridWeapon.element}.jpg`
else
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`
}
const image = () => {
switch (type) {
case 'character':
return characterImage()
case 'summon':
return summonImage()
case 'weapon':
return weaponImage()
}
}
return (
<header className={styles.root}>
<div className={styles.title}>
<h4>{object.name[locale]}</h4>
<div className={styles.image}>
{overlay()}
<img alt={object.name[locale]} src={image()} />
</div>
</div>
<div className={styles.subInfo}>
<div className={styles.icons}>
<WeaponLabelIcon labelType={Element[object.element]} />
{'proficiency' in object && Array.isArray(object.proficiency) && (
<WeaponLabelIcon labelType={Proficiency[object.proficiency[0]]} />
)}
{'proficiency' in object && !Array.isArray(object.proficiency) && (
<WeaponLabelIcon labelType={Proficiency[object.proficiency]} />
)}
{'proficiency' in object &&
Array.isArray(object.proficiency) &&
object.proficiency.length > 1 && (
<WeaponLabelIcon labelType={Proficiency[object.proficiency[1]]} />
)}
</div>
<UncapIndicator
type={type}
ulb={object.uncap.ulb || false}
flb={object.uncap.flb || false}
transcendenceStage={
'transcendence_step' in gridObject
? gridObject.transcendence_step
: 0
}
special={'special' in object ? object.special : false}
/>
</div>
</header>
)
}
export default HovercardHeader

View file

@ -0,0 +1,56 @@
.languageSwitch {
$height: 24px;
background: $grey-60;
border-radius: calc($height / 2);
border: none;
position: relative;
width: 44px;
height: $height;
&:hover {
cursor: pointer;
}
.thumb {
$diameter: 18px;
background: $grey-100;
border-radius: calc($diameter / 2);
display: block;
height: $diameter;
width: $diameter;
position: absolute;
top: 3px;
left: 3px;
z-index: 3;
&:hover {
cursor: pointer;
}
&[data-state='checked'] {
background: $grey-100;
left: 23px;
}
}
.left,
.right {
color: $grey-100;
font-size: 10px;
font-weight: $bold;
position: absolute;
z-index: 2;
}
.left {
top: 6px;
left: 6px;
}
.right {
top: 6px;
right: 5px;
}
}

View file

@ -0,0 +1,51 @@
import React, { PropsWithChildren, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { setCookie } from 'cookies-next'
import { retrieveLocaleCookies } from '~utils/retrieveCookies'
import * as SwitchPrimitive from '@radix-ui/react-switch'
import styles from './index.module.scss'
interface Props extends SwitchPrimitive.SwitchProps {}
export const LanguageSwitch = React.forwardRef<HTMLButtonElement, Props>(
function LanguageSwitch(
{ children }: PropsWithChildren<Props>,
forwardedRef
) {
// Router and locale data
const router = useRouter()
const localeData = retrieveLocaleCookies()
// State
const [languageChecked, setLanguageChecked] = useState(false)
// Hooks
useEffect(() => {
setLanguageChecked(localeData === 'ja' ? true : false)
}, [localeData])
function changeLanguage(value: boolean) {
const language = value ? 'ja' : 'en'
const expiresAt = new Date()
expiresAt.setDate(expiresAt.getDate() + 120)
setCookie('NEXT_LOCALE', language, { path: '/', expires: expiresAt })
router.push(router.asPath, undefined, { locale: language })
}
return (
<SwitchPrimitive.Root
className={styles.languageSwitch}
onCheckedChange={changeLanguage}
checked={languageChecked}
ref={forwardedRef}
>
<SwitchPrimitive.Thumb className={styles.thumb} />
<span className={styles.left}>JP</span>
<span className={styles.right}>EN</span>
</SwitchPrimitive.Root>
)
}
)
export default LanguageSwitch

View file

@ -1,15 +0,0 @@
.ToastViewport {
position: fixed;
bottom: 0px;
right: 0px;
display: flex;
flex-direction: column;
width: 340px;
max-width: 100vw;
z-index: 2147483647;
padding: 25px;
gap: 10px;
margin: 0px;
list-style: none;
outline: none;
}

View file

@ -8,8 +8,6 @@ import { appState } from '~utils/appState'
import TopHeader from '~components/Header'
import UpdateToast from '~components/toasts/UpdateToast'
import './index.scss'
interface Props {}
const Layout = ({ children }: PropsWithChildren<Props>) => {

View file

@ -1,22 +0,0 @@
.Raid.Select {
min-width: 420px;
.Top {
display: flex;
flex-direction: column;
gap: $unit;
padding: $unit 0;
.SegmentedControl {
width: 100%;
}
.Input.Bound {
background-color: var(--select-contained-bg);
&:hover {
background-color: var(--select-contained-bg-hover);
}
}
}
}

View file

@ -1,170 +0,0 @@
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import * as RadixSelect from '@radix-ui/react-select'
import classNames from 'classnames'
import Overlay from '~components/common/Overlay'
import ChevronIcon from '~public/icons/Chevron.svg'
import './index.scss'
import SegmentedControl from '~components/common/SegmentedControl'
import Segment from '~components/common/Segment'
import Input from '~components/common/Input'
// Props
interface Props
extends React.DetailedHTMLProps<
React.SelectHTMLAttributes<HTMLSelectElement>,
HTMLSelectElement
> {
altText?: string
currentSegment: number
iconSrc?: string
open: boolean
trigger?: React.ReactNode
children?: React.ReactNode
onOpenChange?: () => void
onValueChange?: (value: string) => void
onSegmentClick: (segment: number) => void
onClose?: () => void
triggerClass?: string
overlayVisible?: boolean
}
const RaidSelect = React.forwardRef<HTMLButtonElement, Props>(function Select(
props: Props,
forwardedRef
) {
// Import translations
const { t } = useTranslation('common')
const searchInput = React.createRef<HTMLInputElement>()
const [open, setOpen] = useState(false)
const [value, setValue] = useState('')
const [query, setQuery] = useState('')
const triggerClasses = classNames(
{
SelectTrigger: true,
Disabled: props.disabled,
},
props.triggerClass
)
useEffect(() => {
setOpen(props.open)
}, [props.open])
useEffect(() => {
if (props.value && props.value !== '') setValue(`${props.value}`)
else setValue('')
}, [props.value])
function onValueChange(newValue: string) {
setValue(`${newValue}`)
if (props.onValueChange) props.onValueChange(newValue)
}
function onCloseAutoFocus() {
setOpen(false)
if (props.onClose) props.onClose()
}
function onEscapeKeyDown() {
setOpen(false)
if (props.onClose) props.onClose()
}
function onPointerDownOutside() {
setOpen(false)
if (props.onClose) props.onClose()
}
return (
<RadixSelect.Root
open={open}
value={value !== '' ? value : undefined}
onValueChange={onValueChange}
onOpenChange={props.onOpenChange}
>
<RadixSelect.Trigger
className={triggerClasses}
placeholder={props.placeholder}
ref={forwardedRef}
>
{props.iconSrc ? <img alt={props.altText} src={props.iconSrc} /> : ''}
<RadixSelect.Value placeholder={props.placeholder} />
{!props.disabled ? (
<RadixSelect.Icon className="SelectIcon">
<ChevronIcon />
</RadixSelect.Icon>
) : (
''
)}
</RadixSelect.Trigger>
<RadixSelect.Portal className="Select">
<>
<Overlay
open={open}
visible={props.overlayVisible != null ? props.overlayVisible : true}
/>
<RadixSelect.Content
className="Raid Select"
onCloseAutoFocus={onCloseAutoFocus}
onEscapeKeyDown={onEscapeKeyDown}
onPointerDownOutside={onPointerDownOutside}
>
<div className="Top">
<Input
autoComplete="off"
className="Search Bound"
name="query"
placeholder={t('search.placeholders.raid')}
ref={searchInput}
value={query}
onChange={() => {}}
/>
<SegmentedControl blended={true}>
<Segment
groupName="raid_section"
name="events"
selected={props.currentSegment === 1}
onClick={() => props.onSegmentClick(1)}
>
{t('raids.sections.events')}
</Segment>
<Segment
groupName="raid_section"
name="raids"
selected={props.currentSegment === 0}
onClick={() => props.onSegmentClick(0)}
>
{t('raids.sections.raids')}
</Segment>
<Segment
groupName="raid_section"
name="solo"
selected={props.currentSegment === 2}
onClick={() => props.onSegmentClick(2)}
>
{t('raids.sections.solo')}
</Segment>
</SegmentedControl>
</div>
<RadixSelect.Viewport>{props.children}</RadixSelect.Viewport>
</RadixSelect.Content>
</>
</RadixSelect.Portal>
</RadixSelect.Root>
)
})
RaidSelect.defaultProps = {
overlayVisible: true,
}
export default RaidSelect

View file

@ -1,4 +1,4 @@
.About.PageContent {
.about {
$width: 520px;
padding-bottom: $unit-12x;
@ -9,7 +9,7 @@
gap: $unit-2x;
z-index: 5;
.Hero {
.hero {
position: absolute;
width: 40vw;
height: 80vh;
@ -55,22 +55,10 @@
z-index: 2;
}
}
.Links {
.links {
display: grid;
gap: $unit;
margin: $unit-2x 0;
}
div.LinkItem {
margin-top: $unit-2x;
}
.LinkItem {
max-width: calc($width / 3 * 2);
@include breakpoint(phone) {
max-width: inherit;
width: 100%;
}
}
}

View file

@ -1,12 +1,13 @@
import React from 'react'
import Link from 'next/link'
import { Trans, useTranslation } from 'next-i18next'
import classNames from 'classnames'
import LinkItem from '../LinkItem'
import ShareIcon from '~public/icons/Share.svg'
import DiscordIcon from '~public/icons/discord.svg'
import GithubIcon from '~public/icons/github.svg'
import './index.scss'
import styles from './index.module.scss'
interface Props {}
@ -14,8 +15,10 @@ const AboutPage: React.FC<Props> = (props: Props) => {
const { t: common } = useTranslation('common')
const { t: about } = useTranslation('about')
const classes = classNames(styles.about, 'PageContent')
return (
<div className="About PageContent">
<div className={classes}>
<h1>{common('about.segmented_control.about')}</h1>
<section>
<h2>
@ -33,28 +36,19 @@ const AboutPage: React.FC<Props> = (props: Props) => {
</h2>
<p>{about('about.explanation.0')}</p>
<p>{about('about.explanation.1')}</p>
<div className="Hero" />
<div className={styles.hero} />
</section>
<section>
<h2>{about('about.feedback.title')}</h2>
<p>{about('about.feedback.explanation')}</p>
<p>{about('about.feedback.solicit')}</p>
<div className="Discord LinkItem">
<Link href="https://discord.gg/qyZ5hGdPC8">
<a
href="https://discord.gg/qyZ5hGdPC8"
target="_blank"
rel="noreferrer"
>
<div className="Left">
<DiscordIcon />
<h3>granblue-tools</h3>
</div>
<ShareIcon className="ShareIcon" />
</a>
</Link>
</div>
<LinkItem
className="discord constrained"
title="granblue-tools"
link="https://discord.gg/qyZ5hGdPC8"
icon={<DiscordIcon />}
/>
</section>
<section>
@ -114,38 +108,20 @@ const AboutPage: React.FC<Props> = (props: Props) => {
<h2>{about('about.contributing.title')}</h2>
<p>{about('about.contributing.explanation')}</p>
<ul className="Links">
<li className="Github LinkItem">
<Link href="https://github.com/jedmund/hensei-api">
<a
href="https://github.com/jedmund/hensei-api"
target="_blank"
rel="noreferrer"
>
<div className="Left">
<GithubIcon />
<h3>jedmund/hensei-api</h3>
<div className={styles.links}>
<LinkItem
className="github constrained"
title="jedmund/hensei-api"
link="https://github.com/jedmund/hensei-api"
icon={<GithubIcon />}
/>
<LinkItem
className="github constrained"
title="jedmund/hensei-web"
link="https://github.com/jedmund/hensei-web"
icon={<GithubIcon />}
/>
</div>
<ShareIcon className="ShareIcon" />
</a>
</Link>
</li>
<li className="Github LinkItem">
<Link href="https://github.com/jedmund/hensei-web">
<a
href="https://github.com/jedmund/hensei-web"
target="_blank"
rel="noreferrer"
>
<div className="Left">
<GithubIcon />
<h3>jedmund/hensei-web</h3>
</div>
<ShareIcon className="ShareIcon" />
</a>
</Link>
</li>
</ul>
</section>
<section>
<h2>{about('about.license.title')}</h2>

View file

@ -1,4 +1,4 @@
.ChangelogUnit {
.unit {
display: flex;
flex-direction: column;
gap: $unit;

View file

@ -2,7 +2,7 @@ import { useRouter } from 'next/router'
import React, { useEffect, useState } from 'react'
import api from '~utils/api'
import './index.scss'
import styles from './index.module.scss'
interface Props {
id: string
@ -82,7 +82,7 @@ const ChangelogUnit = ({ id, type, image }: Props) => {
}
return (
<div className="ChangelogUnit" key={id}>
<div className={styles.unit} key={id}>
<img alt={item ? item.name[locale] : ''} src={imageUrl()} />
<h4>{item ? item.name[locale] : ''}</h4>
</div>

View file

@ -0,0 +1,71 @@
.content.version {
display: flex;
flex-direction: column;
gap: $unit-2x;
.header {
align-items: baseline;
display: flex;
gap: $unit-half;
margin-bottom: $unit-2x;
h3 {
color: var(--accent-yellow);
font-weight: $medium;
font-size: $font-large;
}
time {
color: var(--text-secondary);
font-size: $font-small;
font-weight: $medium;
}
}
.contents {
margin-bottom: $unit-3x;
display: grid;
grid-template-columns: 1fr;
gap: $unit-4x;
.characters,
.weapons,
.summons {
display: grid;
grid-template-rows: auto 1fr;
gap: $unit;
& > h4 {
font-weight: $medium;
font-size: $font-regular;
}
.items {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: $unit-4x;
}
}
}
.notes {
h4 {
font-weight: $medium;
font-size: $font-regular;
margin-bottom: $unit-2x;
}
.list {
display: flex;
flex-direction: column;
color: var(--text-primary);
list-style-type: disc;
list-style-position: inside;
gap: $unit-half;
li {
font-size: 14px;
}
}
}
}

View file

@ -1,11 +0,0 @@
.Content.Version {
.Contents {
margin-bottom: $unit-3x;
}
.Notes h4 {
font-weight: $medium;
font-size: $font-regular;
margin-bottom: $unit-2x;
}
}

View file

@ -1,8 +1,9 @@
import React from 'react'
import { useTranslation } from 'next-i18next'
import ChangelogUnit from '~components/about/ChangelogUnit'
import classNames from 'classnames'
import './index.scss'
import ChangelogUnit from '~components/about/ChangelogUnit'
import styles from './index.module.scss'
interface UpdateObject {
character?: string[]
@ -51,9 +52,9 @@ const ContentUpdate = ({
const items = newItems[key]
section =
items && items.length > 0 ? (
<section className={`${key}s`}>
<section className={styles[`${key}s`]}>
<h4>{updates(`labels.${key}s`)}</h4>
<div className="items">{newItemElements(key)}</div>
<div className={styles.items}>{newItemElements(key)}</div>
</section>
) : (
''
@ -70,9 +71,9 @@ const ContentUpdate = ({
elements = items
? items.map((id) => {
return key === 'character' ? (
<ChangelogUnit id={id} type={key} image="03" />
<ChangelogUnit id={id} type={key} key={id} image="03" />
) : (
<ChangelogUnit id={id} type={key} />
<ChangelogUnit id={id} type={key} key={id} />
)
})
: []
@ -87,9 +88,9 @@ const ContentUpdate = ({
const items = uncappedItems[key]
section =
items && items.length > 0 ? (
<section className={`${key}s`}>
<section className={styles[`${key}s`]}>
<h4>{updates(`labels.uncaps.${key}s`)}</h4>
<div className="items">{uncapItemElements(key)}</div>
<div className={styles.items}>{uncapItemElements(key)}</div>
</section>
) : (
''
@ -100,15 +101,21 @@ const ContentUpdate = ({
}
return (
<section className="Content Version" data-version={version}>
<div className="Header">
<section
className={classNames({
[styles.content]: true,
[styles.version]: true,
})}
data-version={version}
>
<div className={styles.header}>
<h3>{`${updates('events.date', {
year: date.getFullYear(),
month: `${date.getMonth() + 1}`.padStart(2, '0'),
})} ${updates(event)}`}</h3>
<time>{dateString}</time>
</div>
<div className="Contents">
<div className={styles.contents}>
{newItemSection('character')}
{uncapItemSection('character')}
{newItemSection('weapon')}
@ -117,10 +124,10 @@ const ContentUpdate = ({
{uncapItemSection('summon')}
</div>
{numNotes > 0 ? (
<div className="Notes">
<div className={styles.notes}>
<section>
<h4>{updates('labels.updates')}</h4>
<ul className="Bare Contents">
<ul className={styles.list}>
{[...Array(numNotes)].map((e, i) => (
<li key={`${version}-${i}`}>
{updates(`versions.${version}.features.${i}`)}

View file

@ -0,0 +1,77 @@
.item {
$diameter: $unit-6x;
align-items: center;
background: var(--dialog-bg);
border: 1px solid var(--link-item-bg);
border-radius: $card-corner;
display: flex;
min-height: 82px;
transition: background $duration-zoom ease-in,
transform $duration-zoom ease-in;
&:hover {
background: var(--link-item-bg);
color: var(--text-primary);
.shareIcon {
fill: var(--text-primary);
transform: translate($unit-half, calc(($unit * -1) / 2));
}
}
&.constrained {
max-width: 520px;
@include breakpoint(phone) {
max-width: inherit;
width: 100%;
}
}
&.github:hover .left .icon svg {
fill: var(--text-primary);
}
&.discord:hover .left .icon svg {
fill: #5865f2;
}
a {
display: flex;
justify-content: space-between;
padding: $unit-2x;
width: 100%;
&:hover {
text-decoration: none;
}
.left {
align-items: center;
display: flex;
gap: $unit-2x;
flex-grow: 1;
h3 {
font-weight: 600;
max-width: 70%;
line-height: 1.3;
}
}
svg {
fill: var(--link-item-image-color);
width: $diameter;
height: auto;
transition: fill $duration-zoom ease-in;
&.shareIcon {
width: $unit-4x;
}
}
}
h3 {
font-weight: $bold;
}
}

View file

@ -0,0 +1,37 @@
import { ComponentProps } from 'react'
import Link from 'next/link'
import classNames from 'classnames'
import ShareIcon from '~public/icons/Share.svg'
import styles from './index.module.scss'
interface Props extends ComponentProps<'div'> {
title: string
link: string
icon: React.ReactNode
}
const LinkItem = ({ icon, title, link, className, ...props }: Props) => {
const classes = classNames(
{
[styles.item]: true,
},
className?.split(' ').map((c) => styles[c])
)
return (
<div className={classes}>
<Link href={link}>
<a href={link} target="_blank" rel="noreferrer">
<div className={styles.left}>
<i className={styles.icon}>{icon}</i>
<h3>{title}</h3>
</div>
<ShareIcon className={styles.shareIcon} />
</a>
</Link>
</div>
)
}
export default LinkItem

View file

@ -0,0 +1,61 @@
.roadmap {
padding-bottom: $unit-12x;
h3.priority {
font-weight: $medium;
font-size: $font-large;
margin-bottom: $unit-4x;
&.in_progress {
color: $yellow;
}
&.high {
color: $red;
}
&.mid {
color: $orange-10;
}
&.low {
color: $blue;
}
}
.notes {
display: flex;
flex-direction: column;
gap: $unit;
margin-bottom: $unit-2x;
p {
margin-bottom: $unit;
font-size: $font-medium;
}
}
ul {
color: var(--text-primary);
list-style-type: none;
display: grid;
grid-template-columns: 1fr 1fr;
gap: $unit-3x;
li {
display: flex;
flex-direction: column;
gap: $unit;
margin-bottom: $unit-2x;
h4 {
font-size: $font-medium;
font-weight: $bold;
}
p {
font-size: $font-regular;
}
}
}
}

View file

@ -1,107 +0,0 @@
.Roadmap.PageContent {
padding-bottom: $unit-12x;
h3.priority {
font-weight: $medium;
font-size: $font-large;
margin-bottom: $unit-4x;
&.in_progress {
color: $yellow;
}
&.high {
color: $red;
}
&.mid {
color: $orange-10;
}
&.low {
color: $blue;
}
}
.notes {
display: flex;
flex-direction: column;
gap: $unit;
margin-bottom: $unit-2x;
p {
margin-bottom: $unit;
font-size: $font-medium;
}
.LinkItem {
$diameter: $unit-6x;
background: var(--dialog-bg);
border: 1px solid var(--link-item-bg);
border-radius: $card-corner;
&:hover {
background-color: var(--link-item-bg);
svg {
fill: var(--link-item-image-color-hover);
}
}
a {
display: flex;
padding: $unit-2x;
&:hover {
text-decoration: none;
}
.Left {
align-items: center;
display: flex;
gap: $unit-2x;
flex-grow: 1;
}
svg {
fill: var(--link-item-image-color);
width: $diameter;
height: auto;
&.ShareIcon {
width: $unit-4x;
}
}
}
h3 {
font-weight: $bold;
max-width: 70%;
line-height: 1.3;
}
}
}
ul {
color: var(--text-primary);
list-style-type: none;
display: grid;
grid-template-columns: 1fr 1fr;
gap: $unit-3x;
li {
display: flex;
flex-direction: column;
gap: $unit;
margin-bottom: $unit-2x;
h4 {
font-size: $font-medium;
font-weight: $bold;
}
p {
font-size: $font-regular;
}
}
}
}

View file

@ -1,13 +1,12 @@
import React from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import classNames from 'classnames'
import ShareIcon from '~public/icons/Share.svg'
import LinkItem from '~components/about/LinkItem'
import GithubIcon from '~public/icons/github.svg'
import './index.scss'
import styles from './index.module.scss'
const ROADMAP_ITEMS = 6
@ -15,31 +14,31 @@ const RoadmapPage = () => {
const { t: common } = useTranslation('common')
const { t: about } = useTranslation('about')
const classes = classNames(styles.roadmap, 'PageContent')
return (
<div className="Roadmap PageContent">
<div className={classes}>
<h1>{common('about.segmented_control.roadmap')}</h1>
<section className="notes">
<section className={styles.notes}>
<p>{about('roadmap.blurb')}</p>
<p>{about('roadmap.link.intro')}</p>
<div className="Github LinkItem">
<Link href="https://github.com/users/jedmund/projects/1/views/3">
<a
href="https://github.com/users/jedmund/projects/1/views/3"
target="_blank"
rel="noreferrer"
>
<div className="Left">
<GithubIcon />
<h3>{about('roadmap.link.title')}</h3>
</div>
<ShareIcon className="ShareIcon" />
</a>
</Link>
</div>
<LinkItem
className="github"
title={about('roadmap.link.title')}
link="https://github.com/users/jedmund/projects/1/views/3"
icon={<GithubIcon />}
/>
</section>
<section className="features">
<h3 className="priority in_progress">{about('roadmap.subtitle')}</h3>
<section className={styles.features}>
<h3
className={classNames({
[styles.priority]: true,
[styles.in_progress]: true,
})}
>
{about('roadmap.subtitle')}
</h3>
<ul>
{[...Array(ROADMAP_ITEMS)].map((e, i) => (
<li key={`roadmap-${i}`}>

View file

@ -1,95 +1,23 @@
.Updates.PageContent {
.updates {
padding-bottom: $unit-12x;
.Version {
.version {
display: flex;
flex-direction: column;
gap: $unit-2x;
h3,
li,
p {
}
&.Content {
.Header h3 {
&.content {
.header h3 {
color: var(--accent-yellow);
}
.Contents {
display: grid;
grid-template-columns: 1fr;
gap: $unit-4x;
.characters,
.weapons,
.summons {
display: grid;
grid-template-rows: auto 1fr;
gap: $unit;
& > h4 {
font-weight: $medium;
font-size: $font-regular;
}
.items {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: $unit-4x;
}
}
}
}
.Header {
align-items: baseline;
display: flex;
gap: $unit-half;
margin-bottom: $unit-2x;
h3 {
color: var(--accent-blue);
font-weight: $medium;
font-size: $font-large;
}
time {
color: var(--text-secondary);
font-size: $font-small;
font-weight: $medium;
}
}
.Contents {
.contents {
display: flex;
flex-direction: column;
gap: $unit-4x;
&.Bare {
display: flex;
flex-direction: column;
color: var(--text-primary);
list-style-type: disc;
list-style-position: inside;
gap: $unit-half;
li {
font-size: 14px;
}
}
section {
display: flex;
flex-direction: column;
gap: $unit-2x;
h2 {
margin: 0;
}
}
.Notes {
.features {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: $unit-2x;
@ -122,6 +50,67 @@
}
}
}
}
.header {
align-items: baseline;
display: flex;
gap: $unit-half;
margin-bottom: $unit-2x;
h3 {
color: var(--accent-blue);
font-weight: $medium;
font-size: $font-large;
}
time {
color: var(--text-secondary);
font-size: $font-small;
font-weight: $medium;
}
}
.list {
display: flex;
flex-direction: column;
color: var(--text-primary);
list-style-type: disc;
list-style-position: inside;
gap: $unit-half;
li {
font-size: 14px;
}
}
.Contents {
display: flex;
flex-direction: column;
gap: $unit-4x;
&.Bare {
display: flex;
flex-direction: column;
color: var(--text-primary);
list-style-type: disc;
list-style-position: inside;
gap: $unit-half;
li {
font-size: 14px;
}
}
section {
display: flex;
flex-direction: column;
gap: $unit-2x;
h2 {
margin: 0;
}
}
.Bugs {
display: flex;

View file

@ -1,16 +1,17 @@
import React from 'react'
import { useTranslation } from 'next-i18next'
import classNames from 'classnames'
import ContentUpdate from '~components/about/ContentUpdate'
import ChangelogUnit from '~components/about/ChangelogUnit'
import './index.scss'
import styles from './index.module.scss'
const UpdatesPage = () => {
const { t: common } = useTranslation('common')
const { t: updates } = useTranslation('updates')
const classes = classNames(styles.updates, 'PageContent')
const versionUpdates = {
'1.0.0': 5,
'1.0.1': 4,
@ -54,8 +55,17 @@ const UpdatesPage = () => {
}
return (
<div className="Updates PageContent">
<div className={classes}>
<h1>{common('about.segmented_control.updates')}</h1>
<ContentUpdate
version="2023-06L"
dateString="2023/06/29"
event="events.legfest"
newItems={{
character: ['3040468000', '3040469000'],
weapon: ['1040421900', '1040712600', '1040516000', '1030305700'],
}}
/>
<ContentUpdate
version="2023-06F"
dateString="2023/06/19"
@ -241,15 +251,15 @@ const UpdatesPage = () => {
weapon: ['1040617100', '1040016100'],
}}
/>
<section className="Version" data-version="1.1">
<div className="Header">
<section className={styles.version} data-version="1.1">
<div className={styles.header}>
<h3>1.1.0</h3>
<time>2023/02/06</time>
</div>
<div className="Contents">
<div className={styles.contents}>
<section>
<h2>{updates('labels.features')}</h2>
<ul className="Notes">
<ul className={styles.features}>
{[...Array(versionUpdates['1.1.0'].updates)].map((e, i) => (
<li key={`1.1.0-update-${i}`}>
{image(
@ -266,7 +276,7 @@ const UpdatesPage = () => {
</section>
<section>
<h2>Bug fixes</h2>
<ul className="Bugs">
<ul className={styles.bugs}>
{[...Array(versionUpdates['1.1.0'].bugs)].map((e, i) => (
<li key={`1.1.0-bugfix-${i}`}>
{updates(`versions.1.1.0.bugs.${i}`)}
@ -276,113 +286,51 @@ const UpdatesPage = () => {
</section>
</div>
</section>
<section className="Content Version" data-version="2023-02U">
<div className="Header">
<h3>{`${updates('events.date', {
year: 2023,
month: 2,
})} ${updates('events.uncap')}`}</h3>
<time>2023/02/01</time>
</div>
<div className="Contents">
<section className="characters">
<h4>{updates('labels.uncaps.characters')}</h4>
<div className="items">
<ChangelogUnit id="3040136000" type="character" />
<ChangelogUnit id="3040219000" type="character" />
</div>
</section>
<section className="weapons">
<h4>{updates('labels.uncaps.weapons')}</h4>
<div className="items">
<ChangelogUnit id="1040511300" type="weapon" />
<ChangelogUnit id="1040412800" type="weapon" />
</div>
</section>
<section className="summons">
<h4>{updates('labels.uncaps.summons')}</h4>
<div className="items">
<ChangelogUnit id="2040234000" type="summon" />
<ChangelogUnit id="2040331000" type="summon" />
</div>
</section>
</div>
</section>
<section className="Content Version" data-version="2023-01F">
<div className="Header">
<h3>{`${updates('events.date', {
year: 2023,
month: 1,
})} ${updates('events.legfest')}`}</h3>
<time>2023/01/31</time>
</div>
<div className="Contents">
<section className="characters">
<h4>{updates('labels.characters')}</h4>
<div className="items">
<ChangelogUnit id="3040445000" type="character" />
<ChangelogUnit id="3040446000" type="character" />
</div>
</section>
<section className="weapons">
<h4>{updates('labels.weapons')}</h4>
<div className="items">
<ChangelogUnit id="1040116700" type="weapon" />
<ChangelogUnit id="1040421400" type="weapon" />
<ChangelogUnit id="1040316000" type="weapon" />
<ChangelogUnit id="1030208000" type="weapon" />
</div>
</section>
</div>
</section>
<section className="Content Version" data-version="2023-01F">
<div className="Header">
<h3>{`${updates('events.date', {
year: 2023,
month: 1,
})} ${updates('events.flash')}`}</h3>
<time>2023/01/19</time>
</div>
<div className="Contents">
<section className="characters">
<h4>{updates('labels.characters')}</h4>
<div className="items">
<ChangelogUnit id="3040444000" type="character" />
<ChangelogUnit id="3040443000" type="character" />
</div>
</section>
<section className="weapons">
<h4>{updates('labels.weapons')}</h4>
<div className="items">
<ChangelogUnit id="1040218300" type="weapon" />
<ChangelogUnit id="1040116600" type="weapon" />
</div>
</section>
</div>
</section>
<section className="Content Version" data-version="2023-01U">
<div className="Header">
<h3>{`${updates('events.date', {
year: 2023,
month: 1,
})} ${updates('events.uncap')}`}</h3>
<time>2023/01/06</time>
</div>
<div className="Contents">
<section className="characters">
<h4>{updates('labels.uncaps.characters')}</h4>
<div className="items">
<ChangelogUnit id="3040196000" type="character" image="03" />
</div>
</section>
</div>
</section>
<section className="Version" data-version="1.0">
<div className="Header">
<ContentUpdate
version="2023-02-U1"
dateString="2023/02/01"
event="events.uncap"
uncappedItems={{
character: ['3040136000', '3040219000'],
weapon: ['1040412800', '1040511300'],
summon: ['2040234000', '2040331000'],
}}
/>
<ContentUpdate
version="2023-01F"
dateString="2023/01/31"
event={'events.legfest'}
newItems={{
character: ['3040445000', '3040446000'],
weapon: ['1040116700', '1040421400', '1040316000', '1030208000'],
}}
numNotes={0}
/>
<ContentUpdate
version="2023-01F"
dateString="2023/01/19"
event="events.flash"
newItems={{
character: ['3040444000', '3040443000'],
weapon: ['1040218300', '1040116600'],
}}
numNotes={0}
/>
<ContentUpdate
version="2023-01U"
dateString="2023/01/06"
event="events.uncap"
uncappedItems={{
character: ['3040196000'],
}}
numNotes={0}
/>
<section className={styles.version} data-version="1.0">
<div className={styles.header}>
<h3>1.0.1</h3>
<time>2023/01/08</time>
</div>
<ul className="Bare Contents">
<ul className={styles.list}>
{[...Array(versionUpdates['1.0.1'])].map((e, i) => (
<li key={`1.0.1-update-${i}`}>
{updates(`versions.1.0.1.features.${i}`)}
@ -390,68 +338,33 @@ const UpdatesPage = () => {
))}
</ul>
</section>
<section className="Content Version" data-version="2022-12L">
<div className="Header">
<h3>{`${updates('events.date', { year: 2022, month: 12 })} ${updates(
'events.legfest'
)}`}</h3>
<time>2022/12/26</time>
</div>
<div className="Contents">
<section className="characters">
<h4>{updates('labels.characters')}</h4>
<div className="items">
<ChangelogUnit id="3040440000" type="character" />
<ChangelogUnit id="3040441000" type="character" />
<ChangelogUnit id="3040442000" type="character" />
</div>
</section>
<section className="weapons">
<h4>{updates('labels.weapons')}</h4>
<div className="items">
<ChangelogUnit id="1040315900" type="weapon" />
<ChangelogUnit id="1040914500" type="weapon" />
<ChangelogUnit id="1040218200" type="weapon" />
</div>
</section>
<section className="summons">
<h4>{updates('labels.summons')}</h4>
<div className="items">
<ChangelogUnit id="2040417000" type="summon" />
</div>
</section>
</div>
</section>
<section className="Content Version" data-version="2022-12F2">
<div className="Header">
<h3>{`${updates('events.date', { year: 2022, month: 12 })} ${updates(
'events.flash'
)}`}</h3>
<time>2022/12/26</time>
</div>
<div className="Contents">
<section className="characters">
<h4>{updates('labels.characters')}</h4>
<div className="items">
<ChangelogUnit id="3040438000" type="character" />
<ChangelogUnit id="3040439000" type="character" />
</div>
</section>
<section className="weapons">
<h4>{updates('labels.weapons')}</h4>
<div className="items">
<ChangelogUnit id="1040024200" type="weapon" />
<ChangelogUnit id="1040116500" type="weapon" />
</div>
</section>
</div>
</section>
<section className="Version" data-version="1.0">
<div className="Header">
<ContentUpdate
version="2022-12L"
dateString="2022/12/26"
event="events.legfest"
newItems={{
character: ['3040440000', '3040441000', '3040442000'],
weapon: ['1040315900', '1040914500', '1040218200'],
summon: ['2040417000'],
}}
numNotes={0}
/>
<ContentUpdate
version="2022-12F2"
dateString="2022/12/26"
event="events.flash"
newItems={{
character: ['3040438000', '3040439000'],
weapon: ['1040024200', '1040116500'],
}}
numNotes={0}
/>
<section className={styles.version} data-version="1.0">
<div className={styles.header}>
<h3>1.0.0</h3>
<time>2022/12/26</time>
</div>
<ul className="Bare Contents">
<ul className={styles.list}>
{[...Array(versionUpdates['1.0.0'])].map((e, i) => (
<li key={`1.0.0-update-${i}`}>
{updates(`versions.1.0.0.features.${i}`)}

View file

@ -1,9 +1,12 @@
.Account.DialogContent {
.fields {
display: flex;
flex-direction: column;
gap: $unit-2x;
width: $unit * 64;
overflow-y: hidden;
padding: 0 $unit-4x;
@include breakpoint(phone) {
gap: $unit-4x;
}
.DialogDescription {
font-size: $font-regular;

View file

@ -4,34 +4,20 @@ import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import { useTheme } from 'next-themes'
import {
Dialog,
DialogClose,
DialogTitle,
DialogTrigger,
} from '~components/common/Dialog'
import { Dialog } from '~components/common/Dialog'
import DialogHeader from '~components/common/DialogHeader'
import DialogFooter from '~components/common/DialogFooter'
import DialogContent from '~components/common/DialogContent'
import Button from '~components/common/Button'
import SelectItem from '~components/common/SelectItem'
import PictureSelectItem from '~components/common/PictureSelectItem'
import SelectTableField from '~components/common/SelectTableField'
// import * as Switch from '@radix-ui/react-switch'
import api from '~utils/api'
import changeLanguage from 'utils/changeLanguage'
import { accountState } from '~utils/accountState'
import { pictureData } from '~utils/pictureData'
import CrossIcon from '~public/icons/Cross.svg'
import './index.scss'
type StateVariables = {
[key: string]: boolean
picture: boolean
gender: boolean
language: boolean
theme: boolean
}
import styles from './index.module.scss'
interface Props {
open: boolean
@ -58,23 +44,8 @@ const AccountModal = React.forwardRef<HTMLDivElement, Props>(
const [mounted, setMounted] = useState(false)
const { theme: appTheme, setTheme: setAppTheme } = useTheme()
// Cookies
const accountCookie = getCookie('account')
const userCookie = getCookie('user')
const cookieData = {
account: accountCookie ? JSON.parse(accountCookie as string) : undefined,
user: userCookie ? JSON.parse(userCookie as string) : undefined,
}
// UI State
const [open, setOpen] = useState(false)
const [selectOpenState, setSelectOpenState] = useState<StateVariables>({
picture: false,
gender: false,
language: false,
theme: false,
})
// Values
const [username, setUsername] = useState(props.username || '')
@ -82,7 +53,6 @@ const AccountModal = React.forwardRef<HTMLDivElement, Props>(
const [language, setLanguage] = useState(props.language || '')
const [gender, setGender] = useState(props.gender || 0)
const [theme, setTheme] = useState(props.theme || 'system')
// const [privateProfile, setPrivateProfile] = useState(false)
// Setup
const [pictureOpen, setPictureOpen] = useState(false)
@ -148,7 +118,6 @@ const AccountModal = React.forwardRef<HTMLDivElement, Props>(
language: language,
gender: gender,
theme: theme,
// private: privateProfile,
},
}
@ -197,17 +166,20 @@ const AccountModal = React.forwardRef<HTMLDivElement, Props>(
.sort((a, b) => (a.name.en > b.name.en ? 1 : -1))
.map((item, i) => {
return (
<PictureSelectItem
<SelectItem
key={`picture-${i}`}
element={item.element}
src={[
icon={{
alt: item.name[locale],
src: [
`/profile/${item.filename}.png`,
`/profile/${item.filename}@2x.png 2x`,
]}
],
}}
value={item.filename}
>
{item.name[locale]}
</PictureSelectItem>
</SelectItem>
)
})
@ -215,15 +187,17 @@ const AccountModal = React.forwardRef<HTMLDivElement, Props>(
<SelectTableField
name="picture"
description={t('modals.settings.descriptions.picture')}
className="Image"
className="image"
label={t('modals.settings.labels.picture')}
image={{
className: pictureData.find((i) => i.filename === picture)?.element,
src: [`/profile/${picture}.png`, `/profile/${picture}@2x.png 2x`],
alt: pictureData.find((i) => i.filename === picture)?.name[locale],
}}
open={pictureOpen}
onOpenChange={() => openSelect('picture')}
onChange={handlePictureChange}
onClose={() => setPictureOpen(false)}
imageAlt={t('modals.settings.labels.image_alt')}
imageClass={pictureData.find((i) => i.filename === picture)?.element}
imageSrc={[`/profile/${picture}.png`, `/profile/${picture}@2x.png 2x`]}
value={picture}
>
{pictureOptions}
@ -308,36 +282,29 @@ const AccountModal = React.forwardRef<HTMLDivElement, Props>(
onOpenAutoFocus={(event: Event) => {}}
onEscapeKeyDown={onEscapeKeyDown}
>
<div className="DialogHeader" ref={headerRef}>
<div className="DialogTop">
<DialogTitle className="SubTitle">
{t('modals.settings.title')}
</DialogTitle>
<DialogTitle className="DialogTitle">@{username}</DialogTitle>
</div>
<DialogClose className="DialogClose" asChild>
<span>
<CrossIcon />
</span>
</DialogClose>
</div>
<DialogHeader
title={`@${username}`}
subtitle={t('modals.settings.title')}
/>
<form onSubmit={update}>
<div className="Fields">
<div className={styles.fields}>
{pictureField()}
{genderField()}
{languageField()}
{themeField()}
</div>
<div className="DialogFooter" ref={footerRef}>
<div className="Left"></div>
<div className="Right">
<DialogFooter
ref={footerRef}
rightElements={[
<Button
contained={true}
bound={true}
key="confirm"
text={t('modals.settings.buttons.confirm')}
/>,
]}
/>
</div>
</div>
</form>
</DialogContent>
</Dialog>

View file

@ -0,0 +1,6 @@
.fields {
display: flex;
flex-direction: column;
gap: $unit;
padding: $unit-fourth $unit-4x;
}

View file

@ -1,11 +0,0 @@
.Login.DialogContent {
gap: $unit;
// min-width: $unit * 52;
.Fields {
display: flex;
flex-direction: column;
gap: $unit;
padding: 0 $unit-4x;
}
}

View file

@ -5,17 +5,18 @@ import { useTranslation } from 'react-i18next'
import axios, { AxiosError, AxiosResponse } from 'axios'
import api from '~utils/api'
import { setHeaders } from '~utils/userToken'
import { accountState } from '~utils/accountState'
import changeLanguage from '~utils/changeLanguage'
import { setHeaders } from '~utils/userToken'
import Button from '~components/common/Button'
import Input from '~components/common/Input'
import { Dialog, DialogTrigger, DialogClose } from '~components/common/Dialog'
import { Dialog } from '~components/common/Dialog'
import DialogHeader from '~components/common/DialogHeader'
import DialogFooter from '~components/common/DialogFooter'
import DialogContent from '~components/common/DialogContent'
import changeLanguage from '~utils/changeLanguage'
import CrossIcon from '~public/icons/Cross.svg'
import './index.scss'
import styles from './index.module.scss'
interface ErrorMap {
[index: string]: string
@ -216,50 +217,48 @@ const LoginModal = (props: Props) => {
return (
<Dialog open={open} onOpenChange={openChange}>
<DialogContent
className="Login"
className="login"
footerref={footerRef}
onEscapeKeyDown={onEscapeKeyDown}
onOpenAutoFocus={onOpenAutoFocus}
>
<div className="DialogHeader">
<div className="DialogTitle">
<h1>{t('modals.login.title')}</h1>
</div>
<DialogClose className="DialogClose">
<CrossIcon />
</DialogClose>
</div>
<form className="form" onSubmit={login}>
<div className="Fields">
<DialogHeader title={t('modals.login.title')} />
<form onSubmit={login}>
<div className={styles.fields}>
<Input
className="Bound"
autoComplete="on"
bound={true}
hide1Password={false}
name="email"
placeholder={t('modals.login.placeholders.email')}
onChange={handleChange}
type="email"
error={errors.email}
ref={emailInput}
onChange={handleChange}
/>
<Input
className="Bound"
bound={true}
hide1Password={false}
name="password"
placeholder={t('modals.login.placeholders.password')}
type="password"
onChange={handleChange}
error={errors.password}
ref={passwordInput}
onChange={handleChange}
/>
</div>
<div className="DialogFooter" ref={footerRef}>
<div className="Buttons Span">
<DialogFooter
ref={footerRef}
rightElements={[
<Button
contained={true}
bound={true}
disabled={!formValid}
key="confirm"
text={t('modals.login.buttons.confirm')}
/>,
]}
/>
</div>
</div>
</form>
</DialogContent>
</Dialog>

View file

@ -0,0 +1,22 @@
.fields {
display: flex;
flex-direction: column;
gap: $unit;
padding: $unit-fourth $unit-4x;
}
.terms {
color: $grey-50;
font-size: $font-small;
line-height: 1.2;
margin-top: $unit;
text-align: center;
a {
color: $blue;
&:hover {
color: darken($blue, 30);
}
}
}

View file

@ -1,27 +0,0 @@
.Signup.DialogContent {
gap: $unit;
// min-width: $unit * 52;
.Fields {
display: flex;
flex-direction: column;
gap: calc($unit / 2);
padding: 0 $unit-4x;
.terms {
color: $grey-50;
font-size: $font-small;
line-height: 1.2;
margin-top: $unit;
text-align: center;
a {
color: $blue;
&:hover {
color: darken($blue, 30);
}
}
}
}
}

View file

@ -10,10 +10,12 @@ import { accountState } from '~utils/accountState'
import Button from '~components/common/Button'
import Input from '~components/common/Input'
import { Dialog, DialogTrigger, DialogClose } from '~components/common/Dialog'
import { Dialog } from '~components/common/Dialog'
import DialogHeader from '~components/common/DialogHeader'
import DialogFooter from '~components/common/DialogFooter'
import DialogContent from '~components/common/DialogContent'
import CrossIcon from '~public/icons/Cross.svg'
import './index.scss'
import styles from './index.module.scss'
interface Props {
open: boolean
@ -295,23 +297,16 @@ const SignupModal = (props: Props) => {
return (
<Dialog open={open} onOpenChange={openChange}>
<DialogContent
className="Signup"
className="signup"
footerref={footerRef}
onEscapeKeyDown={onEscapeKeyDown}
onOpenAutoFocus={onOpenAutoFocus}
>
<div className="DialogHeader">
<div className="DialogTitle">
<h1>{t('modals.signup.title')}</h1>
</div>
<DialogClose className="DialogClose">
<CrossIcon />
</DialogClose>
</div>
<form className="form" onSubmit={register}>
<div className="Fields">
<DialogHeader title={t('modals.signup.title')} />
<form onSubmit={register}>
<div className={styles.fields}>
<Input
bound={true}
className="Bound"
name="username"
placeholder={t('modals.signup.placeholders.username')}
@ -321,6 +316,7 @@ const SignupModal = (props: Props) => {
/>
<Input
bound={true}
className="Bound"
name="email"
placeholder={t('modals.signup.placeholders.email')}
@ -330,6 +326,7 @@ const SignupModal = (props: Props) => {
/>
<Input
bound={true}
className="Bound"
name="password"
placeholder={t('modals.signup.placeholders.password')}
@ -340,7 +337,7 @@ const SignupModal = (props: Props) => {
/>
<Input
className="Bound"
bound={true}
name="confirm_password"
placeholder={t('modals.signup.placeholders.password_confirm')}
type="password"
@ -350,15 +347,17 @@ const SignupModal = (props: Props) => {
/>
</div>
<div className="DialogFooter" ref={footerRef}>
<div className="Buttons Span">
<DialogFooter
ref={footerRef}
rightElements={[
<Button
contained={true}
bound={true}
disabled={!formValid}
key="confirm"
text={t('modals.signup.buttons.confirm')}
/>,
]}
/>
</div>
</div>
<p className="terms">
{/* <Trans i18nKey="modals.signup.agreement">

View file

@ -0,0 +1,69 @@
.content {
$weapon-diameter: 14rem;
display: flex;
flex-direction: column;
gap: $unit-4x;
padding: $unit-4x $unit-4x $unit-2x $unit-4x;
& > p {
font-size: $font-regular;
line-height: 1.4;
strong {
font-weight: $bold;
}
&:lang(ja) {
line-height: 1.4;
}
}
.diagram {
align-items: center;
display: grid;
grid-template-columns: 1fr auto 1fr;
ul {
align-items: center;
display: flex;
flex-direction: column;
gap: $unit-2x;
}
.character {
display: flex;
flex-direction: column;
gap: $unit;
text-align: center;
width: $weapon-diameter;
font-weight: $medium;
img {
border-radius: 1rem;
width: $weapon-diameter;
height: auto;
}
span {
line-height: 1.3;
}
}
.wrapper {
display: flex;
justify-content: center;
width: 100%;
}
.arrow {
align-items: center;
color: $grey-55;
display: flex;
font-size: 4rem;
text-align: center;
height: $weapon-diameter;
justify-content: center;
}
}
}

View file

@ -4,12 +4,13 @@ import { Trans, useTranslation } from 'next-i18next'
import { Dialog } from '~components/common/Dialog'
import DialogContent from '~components/common/DialogContent'
import DialogFooter from '~components/common/DialogFooter'
import Button from '~components/common/Button'
import Overlay from '~components/common/Overlay'
import { appState } from '~utils/appState'
import './index.scss'
import styles from './index.module.scss'
interface Props {
open: boolean
@ -75,19 +76,19 @@ const CharacterConflictModal = (props: Props) => {
return (
<Dialog open={open} onOpenChange={openChange}>
<DialogContent
className="Conflict"
className="conflict"
footerref={footerRef}
onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={close}
>
<div className="Content">
<div className={styles.content}>
<p>
<Trans i18nKey="modals.conflict.character"></Trans>
</p>
<div className="CharacterDiagram Diagram">
<div className={styles.diagram}>
<ul>
{props.conflictingCharacters?.map((character, i) => (
<li className="character" key={`conflict-${i}`}>
<li className={styles.character} key={`conflict-${i}`}>
<img
alt={character.object.name[locale]}
src={imageUrl(character.object, character.uncap_level)}
@ -96,9 +97,9 @@ const CharacterConflictModal = (props: Props) => {
</li>
))}
</ul>
<span className="arrow">&rarr;</span>
<div className="wrapper">
<div className="character">
<span className={styles.arrow}>&rarr;</span>
<div className={styles.wrapper}>
<div className={styles.character}>
<img
alt={props.incomingCharacter?.name[locale]}
src={imageUrl(props.incomingCharacter)}
@ -108,20 +109,22 @@ const CharacterConflictModal = (props: Props) => {
</div>
</div>
</div>
<div className="DialogFooter" ref={footerRef}>
<div className="Buttons Span">
<DialogFooter
rightElements={[
<Button
contained={true}
bound={true}
onClick={close}
key="cancel"
text={t('buttons.cancel')}
/>
/>,
<Button
contained={true}
bound={true}
onClick={props.resolveConflict}
key="confirm"
text={t('modals.conflict.buttons.confirm')}
/>,
]}
/>
</div>
</div>
</DialogContent>
<Overlay open={open} visible={true} />
</Dialog>

View file

@ -0,0 +1,39 @@
.grid {
display: flex;
flex-direction: column;
justify-content: center;
margin: auto;
max-width: $grid-width;
@include breakpoint(tablet) {
align-items: center;
}
.characters {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: $unit-3x;
margin: 0;
padding: 0;
max-width: $grid-width;
isolation: isolate;
@include breakpoint(tablet) {
gap: $unit-2x;
justify-content: space-between;
width: 100%;
}
// prettier-ignore
@media only screen
and (max-width: 500px)
and (max-height: 920px)
and (-webkit-min-device-pixel-ratio: 2) {
gap: $unit;
}
& > li:last-child {
margin: 0;
}
}
}

View file

@ -1,39 +0,0 @@
#CharacterGrid {
display: flex;
flex-direction: column;
justify-content: center;
margin: auto;
max-width: $grid-width;
@include breakpoint(tablet) {
align-items: center;
}
}
#Characters {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: $unit-3x;
margin: 0;
padding: 0;
max-width: $grid-width;
isolation: isolate;
@include breakpoint(tablet) {
gap: $unit-2x;
justify-content: space-between;
width: 100%;
}
// prettier-ignore
@media only screen
and (max-width: 500px)
and (max-height: 920px)
and (-webkit-min-device-pixel-ratio: 2) {
gap: $unit;
}
& > li:last-child {
margin: 0;
}
}

View file

@ -17,7 +17,7 @@ import type { DetailsObject, JobSkillObject, SearchableObject } from '~types'
import api from '~utils/api'
import { appState } from '~utils/appState'
import './index.scss'
import styles from './index.module.scss'
// Props
interface Props {
@ -180,6 +180,7 @@ const CharacterGrid = (props: Props) => {
setPosition(-1)
setConflicts([])
setIncoming(undefined)
setModalOpen(false)
}
async function removeCharacter(id: string) {
@ -515,7 +516,7 @@ const CharacterGrid = (props: Props) => {
cancelAction={cancelAlert}
cancelActionText={'Got it'}
/>
<div id="CharacterGrid">
<div className={styles.grid}>
<JobSection
job={job}
jobSkills={jobSkills}
@ -534,7 +535,7 @@ const CharacterGrid = (props: Props) => {
resolveConflict={resolveConflict}
resetConflict={resetConflict}
/>
<ul id="Characters">
<ul className={styles.characters}>
{Array.from(Array(numCharacters)).map((x, i) => {
return (
<li key={`grid_unit_${i}`}>

View file

@ -1,20 +1,5 @@
.Character.HovercardContent {
.title .Image {
position: relative;
.Perpetuity {
position: absolute;
background-image: url('/icons/perpetuity/filled.svg');
background-size: $unit-3x $unit-3x;
z-index: 20;
top: $unit-half * -1;
right: $unit-3x;
width: $unit-3x;
height: $unit-3x;
}
}
.Mastery {
.content {
.mastery {
display: flex;
flex-direction: column;
gap: $unit;
@ -24,7 +9,7 @@
flex-direction: column;
gap: $unit-half;
.ExtendedMastery {
.extendedMastery {
align-items: center;
display: flex;
gap: $unit-half;
@ -40,7 +25,7 @@
}
}
.Awakening {
.awakening {
display: flex;
flex-direction: column;
gap: $unit;
@ -59,10 +44,4 @@
}
}
}
// .Footer {
// position: sticky;
// bottom: 0;
// left: 0;
// }
}

View file

@ -18,7 +18,8 @@ import {
} from '~data/overMastery'
import { ExtendedMastery } from '~types'
import './index.scss'
import styles from './index.module.scss'
import HovercardHeader from '~components/HovercardHeader'
interface Props {
gridCharacter: GridCharacter
@ -33,20 +34,6 @@ const CharacterHovercard = (props: Props) => {
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const Proficiency = [
'none',
'sword',
'dagger',
'axe',
'spear',
'bow',
'staff',
'fist',
'harp',
'gun',
'katana',
]
const tintElement = Element[props.gridCharacter.object.element]
function goTo() {
@ -56,30 +43,6 @@ const CharacterHovercard = (props: Props) => {
window.open(url, '_blank')
}
const perpetuity = () => {
if (props.gridCharacter && props.gridCharacter.perpetuity) {
return <i className="Perpetuity" />
}
}
function characterImage() {
let imgSrc = ''
if (props.gridCharacter) {
const character = props.gridCharacter.object
// Change the image based on the uncap level
let suffix = '01'
if (props.gridCharacter.uncap_level == 6) suffix = '04'
else if (props.gridCharacter.uncap_level == 5) suffix = '03'
else if (props.gridCharacter.uncap_level > 2) suffix = '02'
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_${suffix}.jpg`
}
return imgSrc
}
function masteryElement(dictionary: ItemSkill[], mastery: ExtendedMastery) {
const canonicalMastery = dictionary.find(
(item) => item.id === mastery.modifier
@ -87,7 +50,7 @@ const CharacterHovercard = (props: Props) => {
if (canonicalMastery) {
return (
<li className="ExtendedMastery" key={canonicalMastery.id}>
<li className={styles.extendedMastery} key={canonicalMastery.id}>
<img
alt={canonicalMastery.name[locale]}
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/mastery/${canonicalMastery.slug}.png`}
@ -104,7 +67,7 @@ const CharacterHovercard = (props: Props) => {
const overMasterySection = () => {
if (props.gridCharacter && props.gridCharacter.over_mastery) {
return (
<section className="Mastery">
<section className={styles.mastery}>
<h5 className={tintElement}>
{t('modals.characters.subtitles.ring')}
</h5>
@ -136,7 +99,7 @@ const CharacterHovercard = (props: Props) => {
props.gridCharacter.aetherial_mastery.modifier > 0
) {
return (
<section className="Mastery">
<section className={styles.mastery}>
<h5 className={tintElement}>
{t('modals.characters.subtitles.earring')}
</h5>
@ -154,7 +117,7 @@ const CharacterHovercard = (props: Props) => {
const permanentMasterySection = () => {
if (props.gridCharacter && props.gridCharacter.perpetuity) {
return (
<section className="Mastery">
<section className={styles.mastery}>
<h5 className={tintElement}>
{t('modals.characters.subtitles.permanent')}
</h5>
@ -176,15 +139,17 @@ const CharacterHovercard = (props: Props) => {
if (gridAwakening) {
return (
<section className="Awakening">
<section className={styles.awakening}>
<h5 className={tintElement}>
{t('modals.characters.subtitles.awakening')}
</h5>
<div>
{gridAwakening.type.slug !== 'character-balanced' && (
<img
alt={gridAwakening.type.name[locale]}
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/awakening/${gridAwakening.type.slug}.jpg`}
/>
)}
<span>
<strong>{`${gridAwakening.type.name[locale]}`}</strong>&nbsp;
{`Lv${gridAwakening.level}`}
@ -200,7 +165,7 @@ const CharacterHovercard = (props: Props) => {
className={tintElement}
text={t('buttons.wiki')}
onClick={goTo}
contained={true}
bound={true}
/>
)
@ -209,51 +174,12 @@ const CharacterHovercard = (props: Props) => {
<HovercardTrigger asChild onClick={props.onTriggerClick}>
{props.children}
</HovercardTrigger>
<HovercardContent className="Character" side="top">
<div className="top">
<div className="title">
<h4>{props.gridCharacter.object.name[locale]}</h4>
<div className="Image">
{perpetuity()}
<img
alt={props.gridCharacter.object.name[locale]}
src={characterImage()}
/>
</div>
</div>
<div className="subInfo">
<div className="icons">
<WeaponLabelIcon
labelType={Element[props.gridCharacter.object.element]}
/>
<WeaponLabelIcon
labelType={
Proficiency[
props.gridCharacter.object.proficiency.proficiency1
]
}
/>
{props.gridCharacter.object.proficiency.proficiency2 ? (
<WeaponLabelIcon
labelType={
Proficiency[
props.gridCharacter.object.proficiency.proficiency2
]
}
/>
) : (
''
)}
</div>
<UncapIndicator
<HovercardContent className={styles.content} side="top">
<HovercardHeader
gridObject={props.gridCharacter}
object={props.gridCharacter.object}
type="character"
ulb={props.gridCharacter.object.uncap.ulb || false}
flb={props.gridCharacter.object.uncap.flb || false}
transcendenceStage={props.gridCharacter.transcendence_step}
special={props.gridCharacter.object.special}
/>
</div>
</div>
{wikiButton}
{awakeningSection()}
{overMasterySection()}

View file

@ -0,0 +1,32 @@
.mods {
display: flex;
flex-direction: column;
gap: $unit-4x;
padding: 0 $unit-4x $unit-2x;
section {
display: flex;
flex-direction: column;
gap: $unit-half;
&.inline {
align-items: center;
flex-direction: row;
justify-content: space-between;
h3 {
margin: 0;
}
}
h3 {
color: $grey-55;
font-size: $font-small;
margin-bottom: $unit;
}
select {
background-color: $grey-90;
}
}
}

View file

@ -1,78 +0,0 @@
.Character.DialogContent {
gap: $unit;
min-width: 480px;
@include breakpoint(phone) {
min-width: inherit;
}
.DialogHeader {
transition: 0.18s padding-top ease-in-out;
position: sticky;
top: 0;
&.Scrolled {
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
box-shadow: 0 1px 12px rgba(0, 0, 0, 0.34);
padding-top: $unit-2x;
}
img {
transition: 0.2s width ease-in-out;
width: $unit-6x !important;
}
.DialogTitle {
font-size: $font-large;
}
.SubTitle {
display: none;
}
}
.mods {
display: flex;
flex-direction: column;
gap: $unit-4x;
padding: 0 $unit-4x $unit-2x;
section {
display: flex;
flex-direction: column;
gap: $unit-half;
&.inline {
align-items: center;
flex-direction: row;
justify-content: space-between;
h3 {
margin: 0;
}
}
h3 {
color: $grey-55;
font-size: $font-small;
margin-bottom: $unit;
}
select {
background-color: $grey-90;
}
}
.Button {
font-size: $font-regular;
padding: ($unit * 1.5) ($unit-2x);
width: 100%;
&.btn-disabled {
background: $grey-90;
color: $grey-70;
cursor: not-allowed;
}
}
}
}

View file

@ -1,16 +1,12 @@
// Core dependencies
import React, { PropsWithChildren, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import classNames from 'classnames'
import { Trans, useTranslation } from 'next-i18next'
import isEqual from 'lodash/isEqual'
// UI dependencies
import {
Dialog,
DialogClose,
DialogTitle,
DialogTrigger,
} from '~components/common/Dialog'
import Alert from '~components/common/Alert'
import { Dialog, DialogTrigger } from '~components/common/Dialog'
import DialogContent from '~components/common/DialogContent'
import Button from '~components/common/Button'
import SelectWithInput from '~components/common/SelectWithInput'
@ -29,8 +25,7 @@ const emptyExtendedMastery: ExtendedMastery = {
const MAX_AWAKENING_LEVEL = 9
// Styles and icons
import CrossIcon from '~public/icons/Cross.svg'
import './index.scss'
import styles from './index.module.scss'
// Types
import {
@ -39,6 +34,8 @@ import {
GridCharacterObject,
} from '~types'
import AwakeningSelectWithInput from '~components/mastery/AwakeningSelectWithInput'
import DialogHeader from '~components/common/DialogHeader'
import DialogFooter from '~components/common/DialogFooter'
interface Props {
gridCharacter: GridCharacter
@ -54,52 +51,39 @@ const CharacterModal = ({
onOpenChange,
updateCharacter,
}: PropsWithChildren<Props>) => {
// Router and localization
const router = useRouter()
const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const { t } = useTranslation('common')
// UI state
// State: Component
const [open, setOpen] = useState(false)
const [alertOpen, setAlertOpen] = useState(false)
const [formValid, setFormValid] = useState(false)
// Refs
const headerRef = React.createRef<HTMLDivElement>()
const footerRef = React.createRef<HTMLDivElement>()
// Classes
const headerClasses = classNames({
DialogHeader: true,
Short: true,
})
// Callbacks and Hooks
useEffect(() => {
setOpen(modalOpen)
}, [modalOpen])
// Character properties: Perpetuity
// State: Data
const [perpetuity, setPerpetuity] = useState(false)
// Character properties: Ring
const [rings, setRings] = useState<CharacterOverMastery>({
1: { ...emptyExtendedMastery, modifier: 1 },
2: { ...emptyExtendedMastery, modifier: 2 },
3: emptyExtendedMastery,
4: emptyExtendedMastery,
})
// Character properties: Earrings
const [earring, setEarring] = useState<ExtendedMastery>(emptyExtendedMastery)
// Character properties: Awakening
const [awakening, setAwakening] = useState<Awakening>()
const [awakeningLevel, setAwakeningLevel] = useState(1)
// Character properties: Transcendence
const [transcendenceStep, setTranscendenceStep] = useState(0)
// Refs
const headerRef = React.createRef<HTMLDivElement>()
const footerRef = React.createRef<HTMLDivElement>()
// Hooks
useEffect(() => {
setOpen(modalOpen)
}, [modalOpen])
useEffect(() => {
if (gridCharacter.aetherial_mastery) {
setEarring({
@ -150,11 +134,81 @@ const CharacterModal = ({
return object
}
// Methods: Modification checking
function hasBeenModified() {
const rings = ringsChanged()
const aetherialMastery = aetherialMasteryChanged()
const awakening = awakeningChanged()
return (
rings ||
aetherialMastery ||
awakening ||
gridCharacter.perpetuity !== perpetuity
)
}
function ringsChanged() {
// Create an empty ExtendedMastery object
const emptyRingset: CharacterOverMastery = {
1: { ...emptyExtendedMastery, modifier: 1 },
2: { ...emptyExtendedMastery, modifier: 2 },
3: emptyExtendedMastery,
4: emptyExtendedMastery,
}
// Check if the current ringset is empty on the current GridCharacter and our local state
const isEmptyRingset =
gridCharacter.over_mastery === undefined && isEqual(emptyRingset, rings)
// Check if the ringset in local state is different from the one on the current GridCharacter
const ringsChanged = !isEqual(gridCharacter.over_mastery, rings)
// Return true if the ringset has been modified and is not empty
return ringsChanged && !isEmptyRingset
}
function aetherialMasteryChanged() {
// Create an empty ExtendedMastery object
const emptyAetherialMastery: ExtendedMastery = {
modifier: 0,
strength: 0,
}
// Check if the current earring is empty on the current GridCharacter and our local state
const isEmptyRingset =
gridCharacter.aetherial_mastery === undefined &&
isEqual(emptyAetherialMastery, earring)
// Check if the earring in local state is different from the one on the current GridCharacter
const aetherialMasteryChanged = !isEqual(
gridCharacter.aetherial_mastery,
earring
)
// Return true if the earring has been modified and is not empty
return aetherialMasteryChanged && !isEmptyRingset
}
function awakeningChanged() {
// Check if the awakening in local state is different from the one on the current GridCharacter
const awakeningChanged =
!isEqual(gridCharacter.awakening.type, awakening) ||
gridCharacter.awakening.level !== awakeningLevel
// Return true if the awakening has been modified and is not empty
return awakeningChanged
}
// Methods: UI state management
function handleOpenChange(open: boolean) {
if (hasBeenModified()) {
setAlertOpen(!open)
} else {
setOpen(open)
onOpenChange(open)
}
}
// Methods: Receive data from components
function receiveRingValues(overMastery: CharacterOverMastery) {
@ -167,21 +221,10 @@ const CharacterModal = ({
) {
setEarring({
modifier: earringModifier,
strength: earringStrength,
strength: earringModifier > 0 ? earringStrength : 0,
})
}
function handleCheckedChange(checked: boolean) {
setPerpetuity(checked)
}
async function handleUpdateCharacter() {
await updateCharacter(prepareObject())
setOpen(false)
if (onOpenChange) onOpenChange(false)
}
function receiveAwakeningValues(id: string, level: number) {
setAwakening(gridCharacter.object.awakenings.find((a) => a.id === id))
setAwakeningLevel(level)
@ -191,8 +234,61 @@ const CharacterModal = ({
setFormValid(isValid)
}
const ringSelect = () => {
return (
// Methods: Event handlers
function handleCheckedChange(checked: boolean) {
setPerpetuity(checked)
}
async function handleUpdateCharacter() {
await updateCharacter(prepareObject())
setOpen(false)
if (onOpenChange) onOpenChange(false)
}
function close() {
setEarring({
modifier: gridCharacter.aetherial_mastery
? gridCharacter.aetherial_mastery.modifier
: 0,
strength: gridCharacter.aetherial_mastery
? gridCharacter.aetherial_mastery.strength
: 0,
})
setRings(gridCharacter.over_mastery || emptyExtendedMastery)
setAwakening(gridCharacter.awakening.type)
setAwakeningLevel(gridCharacter.awakening.level)
setAlertOpen(false)
setOpen(false)
onOpenChange(false)
}
// Constants: Rendering
const confirmationAlert = (
<Alert
message={
<span>
<Trans i18nKey="alerts.unsaved_changes.object">
You will lose all changes to{' '}
<strong>{{ objectName: gridCharacter.object.name[locale] }}</strong>{' '}
if you continue.
<br />
<br />
Are you sure you want to continue without saving?
</Trans>
</span>
}
open={alertOpen}
primaryActionText="Close"
primaryAction={close}
cancelActionText="Nevermind"
cancelAction={() => setAlertOpen(false)}
/>
)
const ringSelect = (
<section>
<h3>{t('modals.characters.subtitles.ring')}</h3>
<RingSelect
@ -201,28 +297,30 @@ const CharacterModal = ({
/>
</section>
)
}
const earringSelect = () => {
const earringData = elementalizeAetherialMastery(gridCharacter)
return (
const earringSelect = (
<section>
<h3>{t('modals.characters.subtitles.earring')}</h3>
<SelectWithInput
object="earring"
dataSet={earringData}
selectValue={earring.modifier ? earring.modifier : 0}
inputValue={earring.strength ? earring.strength : 0}
dataSet={elementalizeAetherialMastery(gridCharacter)}
selectValue={
gridCharacter.aetherial_mastery
? gridCharacter.aetherial_mastery.modifier
: 0
}
inputValue={
gridCharacter.aetherial_mastery
? gridCharacter.aetherial_mastery.strength
: 0
}
sendValidity={receiveValidity}
sendValues={receiveEarringValues}
/>
</section>
)
}
const awakeningSelect = () => {
return (
const awakeningSelect = (
<section>
<h3>{t('modals.characters.subtitles.awakening')}</h3>
<AwakeningSelectWithInput
@ -240,64 +338,57 @@ const CharacterModal = ({
/>
</section>
)
}
const perpetuitySwitch = () => {
return (
<section className="inline">
const perpetuitySwitch = (
<section className={styles.inline}>
<h3>{t('modals.characters.subtitles.permanent')}</h3>
<Switch onCheckedChange={handleCheckedChange} checked={perpetuity} />
</section>
)
}
// Methods: Rendering
return (
<>
{confirmationAlert}
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent
className="Character"
className="character"
headerref={headerRef}
footerref={footerRef}
onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={() => {}}
>
<div className={headerClasses} ref={headerRef}>
<img
alt={gridCharacter.object.name[locale]}
className="DialogImage"
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-square/${gridCharacter.object.granblue_id}_01.jpg`}
<DialogHeader
ref={headerRef}
title={gridCharacter.object.name[locale]}
subtitle={t('modals.characters.title')}
image={{
src: `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-square/${gridCharacter.object.granblue_id}_01.jpg`,
alt: gridCharacter.object.name[locale],
}}
/>
<div className="DialogTop">
<DialogTitle className="SubTitle">
{t('modals.characters.title')}
</DialogTitle>
<DialogTitle className="DialogTitle">
{gridCharacter.object.name[locale]}
</DialogTitle>
</div>
<DialogClose className="DialogClose" asChild>
<span>
<CrossIcon />
</span>
</DialogClose>
</div>
<div className="mods">
{perpetuitySwitch()}
{ringSelect()}
{earringSelect()}
{awakeningSelect()}
</div>
<div className="DialogFooter" ref={footerRef}>
<section className={styles.mods}>
{perpetuitySwitch}
{ringSelect}
{earringSelect}
{awakeningSelect}
</section>
<DialogFooter
ref={footerRef}
rightElements={[
<Button
contained={true}
bound={true}
onClick={handleUpdateCharacter}
key="confirm"
disabled={!formValid}
text={t('modals.characters.buttons.confirm')}
/>,
]}
/>
</div>
</DialogContent>
</Dialog>
</>
)
}

View file

@ -1,4 +1,4 @@
.WeaponResult {
.result {
border-radius: 6px;
display: flex;
gap: $unit;
@ -8,7 +8,7 @@
background: var(--button-contained-bg);
cursor: pointer;
.Info h5 {
.info h5 {
color: var(--text-primary);
}
}
@ -21,7 +21,7 @@
width: 120px;
}
.Info {
.info {
display: flex;
flex-direction: column;
flex-grow: 1;

View file

@ -4,7 +4,7 @@ import { useRouter } from 'next/router'
import UncapIndicator from '~components/uncap/UncapIndicator'
import WeaponLabelIcon from '~components/weapon/WeaponLabelIcon'
import './index.scss'
import styles from './index.module.scss'
interface Props {
data: Character
@ -31,9 +31,9 @@ const CharacterResult = (props: Props) => {
}
return (
<li className="CharacterResult" onClick={props.onClick}>
<li className={styles.result} onClick={props.onClick}>
<img alt={character.name[locale]} src={characterUrl()} />
<div className="Info">
<div className={styles.info}>
<h5>{character.name[locale]}</h5>
<UncapIndicator
type="character"
@ -41,7 +41,7 @@ const CharacterResult = (props: Props) => {
ulb={character.uncap.ulb}
special={character.special}
/>
<div className="tags">
<div className={styles.tags}>
<WeaponLabelIcon labelType={Element[character.element]} />
</div>
</div>

View file

@ -0,0 +1,11 @@
.filterBar {
display: flex;
gap: $unit;
padding: $unit-half $unit-3x;
@include breakpoint(phone) {
display: grid;
gap: $unit;
grid-template-columns: 1fr 1fr;
}
}

View file

@ -1,14 +1,10 @@
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'next-i18next'
import cloneDeep from 'lodash.clonedeep'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import SearchFilter from '~components/search/SearchFilter'
import SearchFilterCheckboxItem from '~components/search/SearchFilterCheckboxItem'
import './index.scss'
import {
emptyElementState,
emptyProficiencyState,
@ -16,6 +12,8 @@ import {
} from '~utils/emptyStates'
import { elements, proficiencies, rarities } from '~utils/stateValues'
import styles from './index.module.scss'
interface Props {
sendFilters: (filters: { [key: string]: number[] }) => void
}
@ -144,16 +142,12 @@ const CharacterSearchFilterBar = (props: Props) => {
return (
<SearchFilter
label={`${t('filters.labels.proficiency')} ${proficiency}`}
display="grid"
numSelected={numSelected}
open={open}
onOpenChange={onOpenChange}
>
<DropdownMenu.Label className="Label">{`${t(
'filters.labels.proficiency'
)} ${proficiency}`}</DropdownMenu.Label>
<section>
<DropdownMenu.Group className="Group">
{Array.from(Array(proficiencies.length / 2)).map((x, i) => {
{Array.from(Array(proficiencies.length)).map((x, i) => {
const checked =
proficiency == 1
? proficiency1State[proficiencies[i]].checked
@ -170,43 +164,14 @@ const CharacterSearchFilterBar = (props: Props) => {
</SearchFilterCheckboxItem>
)
})}
</DropdownMenu.Group>
<DropdownMenu.Group className="Group">
{Array.from(Array(proficiencies.length / 2)).map((x, i) => {
const checked =
proficiency == 1
? proficiency1State[
proficiencies[i + proficiencies.length / 2]
].checked
: proficiency2State[
proficiencies[i + proficiencies.length / 2]
].checked
return (
<SearchFilterCheckboxItem
key={proficiencies[i + proficiencies.length / 2]}
onCheckedChange={onCheckedChange}
checked={checked}
valueKey={proficiencies[i + proficiencies.length / 2]}
>
{t(
`proficiencies.${
proficiencies[i + proficiencies.length / 2]
}`
)}
</SearchFilterCheckboxItem>
)
})}
</DropdownMenu.Group>
</section>
</SearchFilter>
)
}
return (
<div className="SearchFilterBar">
const rarityFilter = (
<SearchFilter
label={t('filters.labels.rarity')}
display="list"
numSelected={
Object.values(rarityState)
.map((x) => x.checked)
@ -215,9 +180,6 @@ const CharacterSearchFilterBar = (props: Props) => {
open={rarityMenu}
onOpenChange={rarityMenuOpened}
>
<DropdownMenu.Label className="Label">
{t('filters.labels.rarity')}
</DropdownMenu.Label>
{Array.from(Array(rarities.length)).map((x, i) => {
return (
<SearchFilterCheckboxItem
@ -231,9 +193,12 @@ const CharacterSearchFilterBar = (props: Props) => {
)
})}
</SearchFilter>
)
const elementFilter = (
<SearchFilter
label={t('filters.labels.element')}
display="list"
numSelected={
Object.values(elementState)
.map((x) => x.checked)
@ -242,9 +207,6 @@ const CharacterSearchFilterBar = (props: Props) => {
open={elementMenu}
onOpenChange={elementMenuOpened}
>
<DropdownMenu.Label className="Label">
{t('filters.labels.element')}
</DropdownMenu.Label>
{Array.from(Array(elements.length)).map((x, i) => {
return (
<SearchFilterCheckboxItem
@ -258,7 +220,12 @@ const CharacterSearchFilterBar = (props: Props) => {
)
})}
</SearchFilter>
)
return (
<div className={styles.filterBar}>
{rarityFilter}
{elementFilter}
{renderProficiencyFilter(1)}
{renderProficiencyFilter(2)}
</div>

View file

@ -1,4 +1,4 @@
.CharacterUnit {
.unit {
align-items: center;
display: flex;
flex-direction: column;
@ -8,7 +8,7 @@
position: relative;
margin-bottom: $unit * 4;
&.editable .CharacterImage:hover {
&.editable .image:hover {
border: $hover-stroke;
box-shadow: $hover-shadow;
cursor: pointer;
@ -60,7 +60,7 @@
z-index: 2;
}
.CharacterImage {
.image {
aspect-ratio: 131 / 273;
background: var(--card-bg);
border: 1px solid rgba(0, 0, 0, 0);
@ -92,17 +92,17 @@
}
}
.CharacterName {
.name {
@include breakpoint(phone) {
font-size: $font-tiny;
}
}
&:hover .Perpetuity.Empty {
&:hover .perpetuity.empty {
opacity: 1;
}
.Perpetuity {
.perpetuity {
position: absolute;
background-image: url('/icons/perpetuity/filled.svg');
background-size: $unit-4x $unit-4x;
@ -118,7 +118,7 @@
cursor: pointer;
}
&.Empty {
&.empty {
background-image: url('/icons/perpetuity/empty.svg');
opacity: 0;

View file

@ -1,9 +1,10 @@
import React, { MouseEvent, useEffect, useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio'
import { Trans, useTranslation } from 'next-i18next'
import { AxiosResponse } from 'axios'
import classNames from 'classnames'
import cloneDeep from 'lodash.clonedeep'
import Alert from '~components/common/Alert'
import Button from '~components/common/Button'
@ -26,12 +27,13 @@ import SettingsIcon from '~public/icons/Settings.svg'
// Types
import type {
CharacterOverMastery,
GridCharacterObject,
PerpetuityObject,
SearchableObject,
} from '~types'
import './index.scss'
import styles from './index.module.scss'
interface Props {
gridCharacter?: GridCharacter
@ -72,14 +74,10 @@ const CharacterUnit = ({
// Classes
const classes = classNames({
CharacterUnit: true,
editable: editable,
filled: gridCharacter !== undefined,
})
const buttonClasses = classNames({
Options: true,
Clicked: contextMenuOpen,
unit: true,
[styles.unit]: true,
[styles.editable]: editable,
[styles.filled]: gridCharacter !== undefined,
})
// Other
@ -147,7 +145,20 @@ const CharacterUnit = ({
// Save the server's response to state
function processResult(response: AxiosResponse) {
const gridCharacter: GridCharacter = response.data
appState.grid.characters[gridCharacter.position] = gridCharacter
let character = cloneDeep(gridCharacter)
if (character.over_mastery) {
const overMastery: CharacterOverMastery = {
1: gridCharacter.over_mastery[0],
2: gridCharacter.over_mastery[1],
3: gridCharacter.over_mastery[2],
4: gridCharacter.over_mastery[3],
}
character.over_mastery = overMastery
}
appState.grid.characters[gridCharacter.position] = character
}
function processError(error: any) {
@ -219,8 +230,10 @@ const CharacterUnit = ({
<ContextMenu onOpenChange={handleContextMenuOpenChange}>
<ContextMenuTrigger asChild>
<Button
active={contextMenuOpen}
floating={true}
leftAccessoryIcon={<SettingsIcon />}
className={buttonClasses}
className="options"
onClick={handleButtonClicked}
/>
</ContextMenuTrigger>
@ -278,8 +291,8 @@ const CharacterUnit = ({
const perpetuity = () => {
if (gridCharacter) {
const classes = classNames({
Perpetuity: true,
Empty: !gridCharacter.perpetuity,
[styles.perpetuity]: true,
[styles.empty]: !gridCharacter.perpetuity,
})
return <i className={classes} onClick={handlePerpetuityClick} />
@ -297,13 +310,13 @@ const CharacterUnit = ({
const content = (
<div
className="CharacterImage"
className={styles.image}
tabIndex={gridCharacter ? gridCharacter.position * 7 : 0}
onClick={openSearchModal}
>
{image}
{editable ? (
<span className="icon">
<span className={styles.icon}>
<PlusIcon />
</span>
) : (
@ -346,7 +359,7 @@ const CharacterUnit = ({
) : (
''
)}
<h3 className="CharacterName">{character?.name[locale]}</h3>
<h3 className={styles.name}>{character?.name[locale]}</h3>
</div>
{searchModal()}
</>

View file

@ -1,4 +1,4 @@
.AlertWrapper {
.wrapper {
align-items: center;
display: flex;
justify-content: center;
@ -7,10 +7,20 @@
width: 100vw;
top: 0;
left: 0;
z-index: 31;
z-index: 12;
}
.Alert {
.overlay {
isolation: isolate;
position: fixed;
z-index: 9;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.alert {
animation: $duration-modal-open cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal none
running openModalDesktop;
background: var(--dialog-bg);
@ -52,9 +62,21 @@
align-self: center;
width: 100%;
.Button {
& > * {
width: 100%;
}
}
}
@keyframes openModalDesktop {
0% {
opacity: 0;
transform: scale(0.96);
}
100% {
// opacity: 1;
transform: scale(1);
}
}
}

View file

@ -1,7 +1,7 @@
import React from 'react'
import * as AlertDialog from '@radix-ui/react-alert-dialog'
import './index.scss'
import styles from './index.module.scss'
import Button from '~components/common/Button'
import Overlay from '~components/common/Overlay'
@ -21,44 +21,44 @@ const Alert = (props: Props) => {
return (
<AlertDialog.Root open={props.open}>
<AlertDialog.Portal>
<AlertDialog.Overlay className="Overlay" onClick={props.cancelAction} />
<div className="AlertWrapper">
<Overlay
className="alert"
open={props.open}
visible={true}
onClick={props.cancelAction}
/>
<div className={styles.wrapper}>
<AlertDialog.Content
className="Alert"
className={styles.alert}
onEscapeKeyDown={props.cancelAction}
>
{props.title ? (
{props.title && (
<AlertDialog.Title>{props.title}</AlertDialog.Title>
) : (
''
)}
<AlertDialog.Description className="description">
<AlertDialog.Description className={styles.description}>
{props.message}
</AlertDialog.Description>
<div className="buttons">
<div className={styles.buttons}>
<AlertDialog.Cancel asChild>
<Button
contained={true}
bound={true}
onClick={props.cancelAction}
text={props.cancelActionText}
/>
</AlertDialog.Cancel>
{props.primaryAction ? (
{props.primaryAction && (
<AlertDialog.Action asChild>
<Button
className={props.primaryActionClassName}
contained={true}
bound={true}
onClick={props.primaryAction}
text={props.primaryActionText}
/>
</AlertDialog.Action>
) : (
''
)}
</div>
</AlertDialog.Content>
</div>
<Overlay open={props.open} visible={true} />
</AlertDialog.Portal>
</AlertDialog.Root>
)

View file

@ -1,4 +1,4 @@
.Button {
.button {
align-items: center;
background: var(--button-bg);
border: 2px solid transparent;
@ -7,18 +7,27 @@
display: inline-flex;
font-size: $font-button;
font-weight: $normal;
justify-content: center;
gap: 6px;
transition: 0.18s opacity ease-in-out;
user-select: none;
transition: background-color 0.18s ease-out, color 0.18s ease-out;
.text {
align-items: center;
color: inherit;
display: flex;
// width: 100%;
}
&:hover,
&.Blended:hover,
&.Blended.Active {
&.blended:hover,
&.blended.active {
background: var(--button-bg-hover);
cursor: pointer;
color: var(--button-text-hover);
.Accessory svg {
.accessory svg {
fill: var(--button-text-hover);
}
@ -28,41 +37,32 @@
}
}
&.Blended {
// Modifiers
&.full {
width: 100%;
}
&.grow {
flex-grow: 1;
}
&.blended {
background: transparent;
}
&.IconButton.medium {
height: inherit;
padding: $unit-half;
&:hover {
background: none;
}
.Text {
font-size: $font-small;
font-weight: $bold;
@include breakpoint(phone) {
display: none;
}
}
}
&.Contained {
&.bound {
background: var(--button-contained-bg);
&:hover {
background: var(--button-contained-bg-hover);
}
&.Save:hover .Accessory svg {
&.save:hover .Accessory svg {
fill: #ff4d4d;
stroke: #ff4d4d;
}
&.Save {
&.save {
color: #ff4d4d;
&.Active .Accessory svg {
@ -81,12 +81,184 @@
}
}
&.Options {
box-shadow: 0px 1px 3px rgb(0 0 0 / 14%);
&.bound.blended {
background: var(--dialog-bg);
&:hover {
background: var(--input-bound-bg);
}
}
&.floating {
pointer-events: none;
position: absolute;
opacity: 0;
z-index: 99;
}
&.jobAccessory.icon {
align-items: center;
border-radius: 99px;
justify-content: center;
position: relative;
padding: $unit * 1.5;
top: $unit;
left: $unit;
height: auto;
z-index: 10;
&:hover .accessory svg,
&.selected .accessory svg {
fill: var(--button-text-hover);
}
.accessory svg {
fill: var(--button-text);
width: $unit-3x;
height: auto;
}
}
&.remixed.small {
padding: $unit-half * 1.5;
&:disabled {
background-color: var(--button-bg-disabled);
.accessory svg {
fill: var(--button-text-disabled);
&:hover {
fill: var(--button-text-disabled);
}
}
}
.accessory svg {
height: 12px;
width: 12px;
}
.text {
font-size: $font-tiny;
}
@include breakpoint(phone) {
.text {
display: none;
}
}
}
// Sizes
&.icon {
aspect-ratio: 1 / 1;
.text {
display: none;
@include breakpoint(tablet) {
display: block;
}
@include breakpoint(phone) {
display: block;
}
}
}
&.small {
font-size: $font-small;
padding: $unit * 1.5;
}
&.medium {
height: $unit * 5.5;
padding: ($unit * 1.5) $unit-2x;
}
&.large {
font-size: $font-large;
padding: $unit-2x $unit-3x;
}
// Special variations
&.filter {
&.filtersActive .accessory svg {
fill: var(--accent-blue);
stroke: none;
}
&:hover {
background: var(--button-bg);
.accessory svg {
fill: var(--button-text);
}
}
.accessory svg {
fill: none;
stroke: var(--button-text);
}
}
&.save {
.accessory svg {
fill: none;
stroke: var(--button-text);
}
&.saved {
color: $save-red;
.accessory svg {
fill: $save-red;
stroke: $save-red;
}
&:hover {
color: $save-red;
.accessory svg {
fill: none;
stroke: $save-red;
}
}
}
&:hover {
color: $save-red;
.accessory svg {
fill: $save-red;
stroke: $save-red;
}
}
}
&.iconButton.medium {
height: inherit;
padding: $unit-half;
&:hover {
background: none;
}
.text {
font-size: $font-small;
font-weight: $bold;
@include breakpoint(phone) {
display: none;
}
}
}
&.options {
box-shadow: 0px 1px 3px rgb(0 0 0 / 14%);
left: 8px;
top: 8px;
z-index: 3;
}
&:disabled {
@ -100,15 +272,6 @@
}
}
&.medium {
height: $unit * 5.5;
padding: ($unit * 1.5) $unit-2x;
}
&.small {
padding: $unit * 1.5;
}
@include breakpoint(phone) {
&.destructive {
background: $error;
@ -124,36 +287,11 @@
background: $error;
color: $grey-100;
.Accessory svg {
.accessory svg {
fill: $grey-100;
}
}
&.Save {
.Accessory svg {
fill: none;
stroke: var(--button-text);
}
&.Saved {
color: #ff4d4d;
.Accessory svg {
fill: #ff4d4d;
stroke: none;
}
}
&:hover {
color: #ff4d4d;
.Accessory svg {
fill: none;
stroke: #ff4d4d;
}
}
}
&.modal:hover {
background: $grey-90;
}
@ -166,7 +304,7 @@
}
}
&.Destructive {
&.destructive {
background: $error;
color: white;
@ -175,15 +313,19 @@
}
}
.Accessory {
.accessory {
$dimension: $unit-2x;
display: flex;
&.Arrow {
&.arrow {
margin-top: $unit-half;
}
&.flipped {
transform: rotate(180deg);
}
svg {
fill: var(--button-text);
height: $dimension;
@ -255,73 +397,70 @@
}
}
&.null {
background: $grey-90;
color: $grey-55;
&:hover {
background: $grey-70;
color: $grey-15;
}
}
&.wind {
background: $wind-bg-20;
color: $wind-text-10;
background: var(--wind-bg);
color: var(--wind-text);
&:hover {
background: darken($wind-bg-20, 10);
background: var(--wind-bg-hover);
color: var(--wind-text-hover);
}
}
&.fire {
background: $fire-bg-20;
color: $fire-text-10;
background: var(--fire-bg);
color: var(--fire-text);
&:hover {
background: darken($fire-bg-20, 10);
background: var(--fire-bg-hover);
color: var(--fire-text-hover);
}
}
&.water {
background: $water-bg-20;
color: $water-text-10;
background: var(--water-bg);
color: var(--water-text);
&:hover {
background: darken($water-bg-20, 10);
background: var(--water-bg-hover);
color: var(--water-text-hover);
}
}
&.earth {
background: $earth-bg-20;
color: $earth-text-10;
background: var(--earth-bg);
color: var(--earth-text);
&:hover {
background: darken($earth-bg-20, 10);
background: var(--earth-bg-hover);
color: var(--earth-text-hover);
}
}
&.dark {
background: $dark-bg-10;
color: $dark-text-10;
background: var(--dark-bg);
color: var(--dark-text);
&:hover {
background: darken($dark-bg-10, 10);
background: var(--dark-bg-hover);
color: var(--dark-text-hover);
}
}
&.light {
background: $light-bg-20;
color: $light-text-10;
background: var(--light-bg);
color: var(--light-text);
&:hover {
background: darken($light-bg-20, 10);
background: var(--light-bg-hover);
color: var(--light-text-hover);
}
}
.Text {
color: inherit;
display: block;
width: 100%;
}
}
// CSS modules suck
:global(.unit:hover) .floating,
:global(.unit) .floating.active {
pointer-events: initial;
opacity: 1;
}

View file

@ -1,7 +1,7 @@
import React from 'react'
import classNames from 'classnames'
import './index.scss'
import styles from './index.module.scss'
interface Props
extends React.DetailedHTMLProps<
@ -14,16 +14,18 @@ interface Props
rightAccessoryClassName?: string
active?: boolean
blended?: boolean
contained?: boolean
buttonSize?: 'small' | 'medium' | 'large'
bound?: boolean
floating?: boolean
size?: 'icon' | 'small' | 'medium' | 'large'
text?: string
}
const defaultProps = {
active: false,
blended: false,
contained: false,
buttonSize: 'medium' as const,
bound: false,
floating: false,
size: 'medium' as const,
}
const Button = React.forwardRef<HTMLButtonElement, Props>(function button(
@ -34,29 +36,44 @@ const Button = React.forwardRef<HTMLButtonElement, Props>(function button(
rightAccessoryClassName,
active,
blended,
contained,
buttonSize,
floating,
bound,
size,
text,
...props
},
forwardedRef
) {
const classes = classNames(buttonSize, props.className, {
Button: true,
Active: active,
Blended: blended,
Contained: contained,
})
const classes = classNames(
{
[styles.button]: true,
[styles.active]: active,
[styles.bound]: bound,
[styles.blended]: blended,
[styles.floating]: floating,
[styles.icon]: size === 'icon',
[styles.small]: size === 'small',
[styles.medium]: size === 'medium' || !size,
[styles.large]: size === 'large',
},
props.className?.split(' ').map((className) => styles[className])
)
const leftAccessoryClasses = classNames(leftAccessoryClassName, {
Accessory: true,
Left: true,
})
const leftAccessoryClasses = classNames(
{
[styles.accessory]: true,
[styles.left]: true,
},
leftAccessoryClassName?.split(' ').map((className) => styles[className])
)
const rightAccessoryClasses = classNames(rightAccessoryClassName, {
Accessory: true,
Right: true,
})
const rightAccessoryClasses = classNames(
{
[styles.accessory]: true,
[styles.right]: true,
},
rightAccessoryClassName?.split(' ').map((className) => styles[className])
)
const hasLeftAccessory = () => {
if (leftAccessoryIcon)
@ -69,7 +86,7 @@ const Button = React.forwardRef<HTMLButtonElement, Props>(function button(
}
const hasText = () => {
if (text) return <span className="Text">{text}</span>
if (text) return <span className={styles.text}>{text}</span>
}
return (

View file

@ -1,3 +0,0 @@
.Joined .Input::placeholder {
color: var(--text-tertiary);
}

View file

@ -1,79 +0,0 @@
import React, {
ForwardRefRenderFunction,
forwardRef,
useEffect,
useState,
} from 'react'
import classNames from 'classnames'
import './index.scss'
interface Props extends React.HTMLProps<HTMLInputElement> {
fieldName: string
placeholder: string
value?: string
limit: number
error: string
onBlur?: (event: React.ChangeEvent<HTMLInputElement>) => void
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
}
const CharLimitedFieldset: ForwardRefRenderFunction<HTMLInputElement, Props> = (
{
fieldName,
placeholder,
value,
limit,
error,
onBlur,
onChange: onInputChange,
...props
},
ref
) => {
// States
const [currentCount, setCurrentCount] = useState(
() => limit - (value || '').length
)
// Hooks
useEffect(() => {
setCurrentCount(limit - (value || '').length)
}, [limit, value])
// Event handlers
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value: inputValue } = event.currentTarget
setCurrentCount(limit - inputValue.length)
if (onInputChange) {
onInputChange(event)
}
}
// Rendering methods
return (
<fieldset className="Fieldset">
<div className={classNames({ Joined: true }, props.className)}>
<input
{...props}
data-1p-ignore
autoComplete="off"
className="Input"
type={props.type}
name={fieldName}
placeholder={placeholder}
defaultValue={value || ''}
onBlur={onBlur}
onChange={handleInputChange}
maxLength={limit}
ref={ref}
formNoValidate
/>
<span className="Counter">{currentCount}</span>
</div>
{error.length > 0 && <p className="InputError">{error}</p>}
</fieldset>
)
}
export default forwardRef(CharLimitedFieldset)

View file

@ -5,7 +5,7 @@ import { Command as CommandPrimitive } from 'cmdk'
import { Dialog } from '../Dialog'
import { DialogContent, DialogProps } from '@radix-ui/react-dialog'
import './index.scss'
import styles from './index.module.scss'
const Command = forwardRef<
React.ElementRef<typeof CommandPrimitive>,

View file

@ -1,4 +1,4 @@
.ContextMenu {
.menu {
background: var(--menu-bg);
border-radius: $input-corner;
padding: $unit 0;

View file

@ -2,7 +2,7 @@ import React from 'react'
import classNames from 'classnames'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import './index.scss'
import styles from './index.module.scss'
interface Props
extends React.DetailedHTMLProps<
@ -16,7 +16,7 @@ export const ContextMenuContent = React.forwardRef<HTMLDivElement, Props>(
function ContextMenu({ children, ...props }, forwardedRef) {
const classes = classNames(
{
ContextMenu: true,
[styles.menu]: true,
},
props.className
)

View file

@ -1,4 +1,4 @@
.ContextItem {
.item {
color: var(--menu-text);
font-size: $font-regular;
padding: ($unit * 1.5) $unit-2x;

View file

@ -2,7 +2,7 @@ import React from 'react'
import classNames from 'classnames'
import { DropdownMenuItem } from '@radix-ui/react-dropdown-menu'
import './index.scss'
import styles from './index.module.scss'
interface Props {
className?: string
@ -14,7 +14,7 @@ const ContextMenuItem = React.forwardRef<HTMLDivElement, Props>(
function ContextMenu({ children, ...props }, forwardedRef) {
const classes = classNames(
{
ContextItem: true,
[styles.item]: true,
},
props.className
)

View file

@ -2,8 +2,6 @@ import React, { PropsWithChildren, useEffect, useState } from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { useLockedBody } from 'usehooks-ts'
import './index.scss'
interface Props extends DialogPrimitive.DialogProps {}
export const Dialog = ({ children, ...props }: PropsWithChildren<Props>) => {

View file

@ -1,4 +1,4 @@
.Dialog {
.dialog {
position: fixed;
box-sizing: border-box;
background: none;
@ -13,11 +13,11 @@
color: inherit;
z-index: 10;
.DialogContent {
.dialogContent {
$multiplier: 4;
// animation: $duration-modal-open cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal
// none running openModalDesktop;
animation: $duration-modal-open cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal
none running openModalDesktop;
background: var(--dialog-bg);
border-radius: $card-corner;
box-sizing: border-box;
@ -31,9 +31,10 @@
// min-height: $unit-12x;
overflow-y: auto;
// height: 80vh;
max-height: 80vh;
max-height: 60vh; // Having a max-height interferes with SearchModal scrolling
min-width: 580px;
max-width: 42vw;
width: 520px; // Using max/min-width messes with the Edit Party contenteditable div
// padding: $unit * $multiplier;
position: relative;
@ -42,11 +43,11 @@
}
@include breakpoint(phone) {
// animation: slideUp;
// animation-duration: 3s;
// animation-fill-mode: forwards;
// animation-play-state: running;
// animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
animation: slideUp;
animation-duration: 1s;
animation-fill-mode: forwards;
animation-play-state: running;
animation-timing-function: ease-out;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
min-width: inherit;
@ -59,144 +60,45 @@
width: 100%;
}
.Container {
overflow-y: hidden;
&.Scrollable {
overflow-y: auto;
}
}
.DialogHeader {
background: var(--dialog-bg);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0);
border-bottom: 1px solid rgba(0, 0, 0, 0);
display: flex;
align-items: center;
gap: $unit-2x;
justify-content: space-between;
padding: $unit-4x ($unit * $multiplier);
position: sticky;
top: 0;
z-index: 10;
&.Short {
padding-top: $unit-3x;
padding-bottom: $unit-3x;
}
.left {
&.search {
box-sizing: border-box;
display: flex;
flex-direction: column;
flex-grow: 1;
gap: $unit;
p {
font-size: $font-small;
line-height: 1.25;
}
}
.DialogImage {
border-radius: $input-corner;
width: $unit-10x;
}
}
.DialogClose {
background: transparent;
border: none;
&:hover {
cursor: pointer;
svg {
fill: $error;
}
}
svg {
fill: $grey-50;
float: right;
height: 24px;
width: 24px;
}
}
.DialogTitle {
color: var(--text-primary);
font-size: $font-xlarge;
h1 {
color: var(--text-primary);
font-size: $font-xlarge;
font-weight: $medium;
text-align: left;
}
}
.DialogTop {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: calc($unit / 2);
.SubTitle {
color: var(--text-secondary);
font-size: $font-small;
font-weight: $medium;
}
}
.DialogDescription {
color: var(--text-secondary);
flex-grow: 1;
}
.DialogFooter {
align-items: flex-end;
background: var(--dialog-bg);
bottom: 0;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.16);
border-top: 1px solid rgba(0, 0, 0, 0.24);
display: flex;
flex-direction: row;
justify-content: space-between;
padding: ($unit * 1.5) ($unit * $multiplier) $unit-3x;
position: sticky;
.Buttons {
display: flex;
gap: $unit;
&.Span {
width: 100%;
.Button {
width: 100%;
}
}
&.Spaced {
justify-content: space-between;
}
}
}
.actions {
display: flex;
justify-content: flex-end;
width: 100%;
}
.Fields {
display: flex;
flex-direction: column;
gap: $unit-2x;
padding: 0 $unit-4x;
min-height: 430px;
max-height: none;
padding: 0;
@include breakpoint(phone) {
gap: $unit-4x;
// animation: none;
min-width: inherit;
height: 90vh;
.container {
display: flex;
flex-direction: column;
}
}
}
&.conflict {
max-height: 80vh;
}
&.editParty {
min-height: 80vh;
.container {
display: flex;
flex-direction: column;
flex-grow: 1;
}
}
.container {
overflow-y: hidden;
&.scrollable {
overflow-y: auto;
}
}
@ -304,4 +206,26 @@
}
}
}
@keyframes openModalDesktop {
0% {
opacity: 0;
transform: scale(0.96);
}
100% {
// opacity: 1;
transform: scale(1);
}
}
@keyframes slideUp {
0% {
transform: translate(0%, 100%);
}
100% {
transform: translate(0, 0%);
}
}
}

View file

@ -4,7 +4,7 @@ import classNames from 'classnames'
import debounce from 'lodash.debounce'
import Overlay from '~components/common/Overlay'
import './index.scss'
import styles from './index.module.scss'
interface Props
extends React.DetailedHTMLProps<
@ -23,9 +23,12 @@ const DialogContent = React.forwardRef<HTMLDivElement, Props>(function Dialog(
forwardedRef
) {
// Classes
const classes = classNames(props.className, {
DialogContent: true,
})
const classes = classNames(
{
[styles.dialogContent]: true,
},
props.className?.split(' ').map((className) => styles[className])
)
// Handlers
function handleScroll(event: React.UIEvent<HTMLDivElement, UIEvent>) {
@ -89,7 +92,7 @@ const DialogContent = React.forwardRef<HTMLDivElement, Props>(function Dialog(
const calculateFooterShadow = debounce(() => {
const boxShadowBase = '0 -2px 8px'
const scrollable = document.querySelector('.Scrollable')
const scrollable = document.querySelector(`.${styles.scrollable}`)
const footer = props.footerref
if (footer && footer.current) {
@ -124,7 +127,7 @@ const DialogContent = React.forwardRef<HTMLDivElement, Props>(function Dialog(
return (
<DialogPrimitive.Portal>
<dialog className="Dialog">
<dialog className={styles.dialog}>
<DialogPrimitive.Content
{...props}
className={classes}
@ -134,8 +137,8 @@ const DialogContent = React.forwardRef<HTMLDivElement, Props>(function Dialog(
>
<div
className={classNames({
Container: true,
Scrollable: scrollable,
[styles.container]: true,
[styles.scrollable]: scrollable,
})}
onScroll={handleScroll}
>

View file

@ -0,0 +1,19 @@
.footer {
$multiplier: 4;
align-items: flex-end;
background: var(--dialog-bg);
bottom: -1px; // hack to fix content being visible 1px below
display: flex;
flex-direction: row;
justify-content: space-between;
padding: ($unit * 1.5) ($unit * $multiplier) $unit-3x;
position: sticky;
transition: box-shadow 0.1s ease-out, border-top 0.1s ease-out;
.left,
.right {
display: flex;
gap: $unit;
}
}

View file

@ -0,0 +1,39 @@
import React, { PropsWithChildren } from 'react'
import classNames from 'classnames'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import styles from './index.module.scss'
interface Props extends DialogPrimitive.DialogProps {
leftElements?: React.ReactNode[]
rightElements?: React.ReactNode[]
image?: {
alt: string
src: string
}
}
export const DialogFooter = React.forwardRef<HTMLDivElement, Props>(
function dialogFooter(
{
leftElements,
rightElements,
image,
children,
...props
}: PropsWithChildren<Props>,
forwardedRef
) {
const classes = classNames({
[styles.footer]: true,
})
return (
<footer {...props} className={classes} ref={forwardedRef}>
<div className={styles.left}>{leftElements}</div>
<div className={styles.right}>{rightElements}</div>
</footer>
)
}
)
export default DialogFooter

View file

@ -0,0 +1,87 @@
.header {
$multiplier: 4;
background: var(--dialog-bg);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0);
border-bottom: 1px solid rgba(0, 0, 0, 0);
display: flex;
align-items: center;
gap: $unit-2x;
justify-content: space-between;
padding: $unit-4x ($unit * $multiplier);
position: sticky;
top: 0;
z-index: 10;
&.short {
padding-top: $unit-3x;
padding-bottom: $unit-3x;
}
.top {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: calc($unit / 2);
}
.left {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: $unit;
p {
font-size: $font-small;
line-height: 1.25;
}
}
.title {
color: var(--text-primary);
font-size: $font-xlarge;
h1 {
color: var(--text-primary);
font-size: $font-xlarge;
font-weight: $medium;
text-align: left;
}
}
.subtitle {
color: var(--text-secondary);
font-size: $font-small;
font-weight: $medium;
}
.image {
border-radius: $input-corner;
width: $unit-10x;
}
.close {
background: transparent;
border: 2px solid transparent;
&:hover {
cursor: pointer;
svg {
fill: $error;
}
}
&:focus {
border-radius: $input-corner;
border: 2px solid $blue;
}
svg {
fill: $grey-50;
float: right;
height: 24px;
width: 24px;
}
}
}

View file

@ -0,0 +1,44 @@
import React, { PropsWithChildren } from 'react'
import classNames from 'classnames'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import CrossIcon from '~public/icons/Cross.svg'
import styles from './index.module.scss'
interface Props extends DialogPrimitive.DialogProps {
title: string
subtitle?: string
image?: {
alt: string
src: string
}
}
export const DialogHeader = React.forwardRef<HTMLDivElement, Props>(
function dialogHeader(
{ title, subtitle, image, children, ...props }: PropsWithChildren<Props>,
forwardedRef
) {
const classes = classNames({
[styles.header]: true,
})
return (
<header {...props} className={classes} ref={forwardedRef}>
{image && (
<img alt={image.alt} className={styles.image} src={image.src} />
)}
<div className={styles.top}>
{subtitle && <div className={styles.subtitle}>{subtitle}</div>}
<div className={styles.title}>{title}</div>
</div>
<DialogPrimitive.Close className={styles.close} tabIndex={0}>
<CrossIcon />
</DialogPrimitive.Close>
</header>
)
}
)
export default DialogHeader

View file

@ -0,0 +1,16 @@
.menu {
transform-origin: --radix-dropdown-menu-content-transform-origin;
background: var(--menu-bg);
border-radius: 6px;
box-shadow: 0 1px 4px rgb(0 0 0 / 8%);
box-sizing: border-box;
overflow: visible;
width: 30vw;
max-width: 180px;
margin: 0 $unit-2x;
z-index: 15;
@include breakpoint(phone) {
min-width: 50vw;
}
}

View file

@ -1,202 +0,0 @@
.Menu {
transform-origin: --radix-dropdown-menu-content-transform-origin;
background: var(--menu-bg);
border-radius: 6px;
box-shadow: 0 1px 4px rgb(0 0 0 / 8%);
box-sizing: border-box;
overflow: auto;
width: 30vw;
max-width: 180px;
margin: 0 $unit-2x;
z-index: 15;
@include breakpoint(phone) {
min-width: 50vw;
}
}
.MenuLabel {
color: var(--text-tertiary);
padding: $unit * 1.5 $unit * 1.5;
font-size: $font-small;
font-weight: $medium;
}
.MenuItem {
color: var(--text-tertiary);
font-weight: $normal;
@include breakpoint(phone) {
cursor: pointer;
}
&:hover:not(.disabled) {
background: var(--menu-bg-item-hover);
color: var(--text-primary);
cursor: pointer;
a {
color: var(--text-primary);
&:hover {
text-decoration: none;
}
&:visited {
color: var(--text-primary);
}
}
@include breakpoint(phone) {
background: inherit;
color: inherit;
cursor: default;
a {
color: inherit;
}
}
}
&.profile > div {
padding: 6px 12px;
}
&.language {
align-items: center;
display: flex;
flex-direction: row;
gap: $unit;
padding-right: $unit;
span {
flex-grow: 1;
}
.Switch {
$height: 24px;
background: $grey-60;
border-radius: calc($height / 2);
border: none;
position: relative;
width: 44px;
height: $height;
&:hover {
cursor: pointer;
}
.Thumb {
$diameter: 18px;
background: $grey-100;
border-radius: calc($diameter / 2);
display: block;
height: $diameter;
width: $diameter;
position: absolute;
top: 3px;
left: 3px;
z-index: 3;
&:hover {
cursor: pointer;
}
&[data-state='checked'] {
background: $grey-100;
left: 23px;
}
}
.left,
.right {
color: $grey-100;
font-size: 10px;
font-weight: $bold;
position: absolute;
z-index: 2;
}
.left {
top: 6px;
left: 6px;
}
.right {
top: 6px;
right: 5px;
}
}
}
& .destructive {
color: $error;
&:hover {
background: $error;
color: #fff;
}
}
a {
color: $grey-50;
&:hover {
text-decoration: none;
}
&:visited {
color: $grey-50;
}
}
& > a,
& > span {
display: block;
padding: 12px 12px;
}
& > div {
align-items: center;
display: flex;
flex-direction: row;
padding: 10px 12px;
&:hover {
i.tag {
background: var(--tag-bg);
color: var(--tag-text);
}
}
span {
flex-grow: 1;
}
img {
$diameter: 32px;
border-radius: calc($diameter / 2);
height: $diameter;
width: $diameter;
}
}
}
.MenuGroup {
border-bottom: 1px solid var(--menu-separator);
&:first-child .MenuItem:first-child {
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
&:last-child .MenuItem:last-child {
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
}
&:last-child {
border-bottom: none;
}
}

View file

@ -1,16 +1,14 @@
import React, { PropsWithChildren } from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import classNames from 'classnames'
import './index.scss'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import styles from './index.module.scss'
interface Props extends DropdownMenuPrimitive.DropdownMenuContentProps {}
export const DropdownMenu = DropdownMenuPrimitive.Root
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
export const DropdownMenuLabel = DropdownMenuPrimitive.Label
export const DropdownMenuItem = DropdownMenuPrimitive.Item
export const DropdownMenuGroup = DropdownMenuPrimitive.Group
export const DropdownMenuSeparator = DropdownMenuPrimitive.Separator
export const DropdownMenuContent = React.forwardRef<HTMLDivElement, Props>(
@ -19,7 +17,7 @@ export const DropdownMenuContent = React.forwardRef<HTMLDivElement, Props>(
forwardedRef
) {
const classes = classNames(props.className, {
Menu: true,
[styles.menu]: true,
})
return (
<DropdownMenuPrimitive.Portal>

View file

@ -0,0 +1,17 @@
.menuGroup {
border-bottom: 1px solid var(--menu-separator);
&:first-child > *:first-child {
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
&:last-child > *:last-child {
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
}
&:last-child {
border-bottom: none;
}
}

View file

@ -0,0 +1,23 @@
import React, { PropsWithChildren } from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import styles from './index.module.scss'
interface Props extends DropdownMenuPrimitive.DropdownMenuContentProps {}
export const DropdownMenuGroup = React.forwardRef<HTMLDivElement, Props>(
function dropdownMenuGroup(
{ children }: PropsWithChildren<Props>,
forwardedRef
) {
return (
<DropdownMenuPrimitive.Group
className={styles.menuGroup}
ref={forwardedRef}
>
{children}
</DropdownMenuPrimitive.Group>
)
}
)
export default DropdownMenuGroup

View file

@ -0,0 +1,113 @@
.menuItem {
color: var(--text-tertiary);
font-weight: $normal;
@include breakpoint(phone) {
cursor: pointer;
}
&:hover:not(.disabled) {
background: var(--menu-bg-item-hover);
color: var(--text-primary);
cursor: pointer;
a {
color: var(--text-primary);
&:hover {
text-decoration: none;
}
&:visited {
color: var(--text-primary);
}
}
@include breakpoint(phone) {
background: inherit;
color: inherit;
cursor: default;
a {
color: inherit;
}
}
}
&.profile > div {
padding: 6px 12px;
}
&.language {
align-items: center;
display: flex;
flex-direction: row;
gap: $unit;
padding-right: $unit;
span {
flex-grow: 1;
}
}
&.destructive {
color: $error;
&:hover {
background: $error;
color: #fff;
}
}
a {
color: $grey-50;
&:hover {
text-decoration: none;
}
&:visited {
color: $grey-50;
}
}
& > a,
& > span {
display: block;
padding: 12px 12px;
}
& > div {
align-items: center;
display: flex;
flex-direction: row;
padding: 10px 12px;
&:hover {
i.tag {
background: var(--tag-bg);
color: var(--tag-text);
}
}
span {
flex-grow: 1;
}
img {
$diameter: 32px;
border-radius: calc($diameter / 2);
height: $diameter;
width: $diameter;
}
}
& i {
background: var(--tag-bg);
color: var(--tag-text);
border-radius: calc($unit / 2);
font-size: 10px;
font-weight: $bold;
padding: 4px 6px;
text-transform: uppercase;
}
}

View file

@ -0,0 +1,39 @@
import React, { PropsWithChildren } from 'react'
import classNames from 'classnames'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import styles from './index.module.scss'
interface Props extends DropdownMenuPrimitive.DropdownMenuItemProps {
destructive?: boolean
}
const defaultProps = {
destructive: false,
}
export const DropdownMenuItem = React.forwardRef<HTMLDivElement, Props>(
function dropdownMenuItem(
{ children, destructive, ...props }: PropsWithChildren<Props>,
forwardedRef
) {
const classes = classNames(props.className, {
[styles.menuItem]: true,
[styles.language]: props.className?.includes('language'),
[styles.destructive]: destructive,
})
return (
<DropdownMenuPrimitive.Item
{...props}
className={classes}
ref={forwardedRef}
>
{children}
</DropdownMenuPrimitive.Item>
)
}
)
DropdownMenuItem.defaultProps = defaultProps
export default DropdownMenuItem

View file

@ -0,0 +1,6 @@
.menuLabel {
color: var(--text-tertiary);
padding: $unit * 1.5 $unit * 1.5;
font-size: $font-small;
font-weight: $medium;
}

View file

@ -0,0 +1,24 @@
import React, { PropsWithChildren } from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import styles from './index.module.scss'
interface Props extends DropdownMenuPrimitive.DropdownMenuLabelProps {}
export const DropdownMenuLabel = React.forwardRef<HTMLDivElement, Props>(
function dropdownMenuItem(
{ children, ...props }: PropsWithChildren<Props>,
forwardedRef
) {
return (
<DropdownMenuPrimitive.Label
{...props}
className={styles.menuLabel}
ref={forwardedRef}
>
{children}
</DropdownMenuPrimitive.Label>
)
}
)
export default DropdownMenuLabel

View file

@ -1,4 +1,4 @@
.Duration {
.duration {
align-items: center;
background: var(--input-bound-bg);
border: 2px solid transparent;
@ -14,22 +14,4 @@
border: 2px solid $blue;
outline: none;
}
.Input {
background: transparent;
border: none;
padding: 0;
width: initial;
height: 100%;
padding: calc($unit-2x - 2px) 0;
&:hover {
background: transparent;
}
&:focus,
&:focus-visible {
border: none;
}
}
}

View file

@ -1,21 +1,20 @@
import React, { useState, ChangeEvent, KeyboardEvent } from 'react'
import classNames from 'classnames'
import Input from '~components/common/Input'
import './index.scss'
import styles from './index.module.scss'
interface Props
extends React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> {
bound: boolean
value: number
onValueChange: (value: number) => void
}
const DurationInput = React.forwardRef<HTMLInputElement, Props>(
function DurationInput(
{ className, value, onValueChange, ...props },
{ bound, className, value, onValueChange, ...props },
forwardedRef
) {
// State
@ -178,16 +177,11 @@ const DurationInput = React.forwardRef<HTMLInputElement, Props>(
}
return (
<div className={classNames(className, { Duration: true })}>
<div className={styles.duration}>
<Input
ref={minutesRef}
type="text"
className={classNames(
{
AlignRight: true,
},
className
)}
className="alignRight duration"
value={getMinutes()}
onChange={handleMinutesChange}
onKeyUp={handleKeyUp}
@ -199,12 +193,7 @@ const DurationInput = React.forwardRef<HTMLInputElement, Props>(
<Input
ref={secondsRef}
type="text"
className={classNames(
{
AlignRight: true,
},
className
)}
className="alignRight duration"
value={getSeconds() > 0 ? `${getSeconds()}`.padStart(2, '0') : ''}
onChange={handleSecondsChange}
onKeyUp={handleKeyUp}

View file

@ -0,0 +1,66 @@
.hovercard {
animation: scaleIn $duration-zoom ease-out;
transform-origin: var(--radix-hover-card-content-transform-origin);
background: var(--dialog-bg);
border-radius: $card-corner;
border: 1px solid rgba(0, 0, 0, 0.1);
color: var(--text-primary);
display: flex;
flex-direction: column;
gap: $unit-2x;
max-height: 30vh;
overflow-y: auto;
padding: $unit-2x;
width: 300px;
section {
@keyframes scaleIn {
0% {
opacity: 0;
transform: scale(0);
}
20% {
opacity: 0.2;
transform: scale(0.4);
}
40% {
opacity: 0.4;
transform: scale(0.8);
}
60% {
opacity: 0.6;
transform: scale(1);
}
65% {
opacity: 0.65;
transform: scale(1.1);
}
70% {
opacity: 0.7;
transform: scale(1);
}
75% {
opacity: 0.75;
transform: scale(0.98);
}
80% {
opacity: 0.8;
transform: scale(1.02);
}
90% {
opacity: 0.9;
transform: scale(0.96);
}
100% {
opacity: 1;
transform: scale(1);
}
}
}
a.Button {
display: block;
padding: $unit * 1.5;
text-align: center;
}
}

View file

@ -1,99 +0,0 @@
div[data-radix-popper-content-wrapper] {
z-index: 10 !important;
}
.HovercardContent {
animation: scaleIn $duration-zoom ease-out;
transform-origin: var(--radix-hover-card-content-transform-origin);
background: var(--dialog-bg);
border-radius: $card-corner;
color: var(--text-primary);
display: flex;
flex-direction: column;
gap: $unit-2x;
max-height: 30vh;
overflow-y: auto;
padding: $unit-2x;
width: 300px;
.top {
display: flex;
flex-direction: column;
gap: calc($unit / 2);
.title {
align-items: center;
display: flex;
flex-direction: row;
gap: $unit * 2;
h4 {
flex-grow: 1;
font-size: $font-medium;
line-height: 1.2;
min-width: 140px;
}
img {
height: auto;
width: 100px;
}
}
.subInfo {
align-items: center;
display: flex;
flex-direction: row;
gap: $unit * 2;
.icons {
display: flex;
flex-direction: row;
flex-grow: 1;
gap: $unit;
}
.UncapIndicator {
min-width: 100px;
}
}
}
section {
h5 {
font-size: $font-small;
font-weight: $medium;
opacity: 0.7;
&.wind {
color: $wind-bg-20;
}
&.fire {
color: $fire-bg-20;
}
&.water {
color: $water-bg-20;
}
&.earth {
color: $earth-bg-20;
}
&.dark {
color: $dark-bg-10;
}
&.light {
color: $light-bg-20;
}
}
}
a.Button {
display: block;
padding: $unit * 1.5;
text-align: center;
}
}

View file

@ -2,7 +2,7 @@ import React, { PropsWithChildren } from 'react'
import classNames from 'classnames'
import * as HoverCardPrimitive from '@radix-ui/react-hover-card'
import './index.scss'
import styles from './index.module.scss'
interface Props extends HoverCardPrimitive.HoverCardContentProps {}
@ -14,7 +14,7 @@ export const HovercardContent = ({
...props
}: PropsWithChildren<Props>) => {
const classes = classNames(props.className, {
HovercardContent: true,
[styles.hovercard]: true,
})
return (
<HoverCardPrimitive.Portal>

View file

@ -0,0 +1,137 @@
.fieldset {
display: flex;
flex-direction: column;
gap: $unit-half;
&:last-child .error {
margin-bottom: 0;
}
&.hidden {
display: none;
}
&.full {
width: 100%;
}
.error {
color: $error;
font-size: $font-small;
padding: calc($unit / 2) ($unit * 2);
min-width: 100%;
margin-bottom: $unit;
width: 0;
}
.fullHeight {
height: 100%;
}
.input {
-webkit-font-smoothing: antialiased;
background-color: var(--input-bg);
border-radius: $input-corner;
border: none;
box-sizing: border-box;
color: var(--text-primary);
display: block;
font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial,
sans-serif;
font-size: $font-regular;
width: 100%;
&:not(.wrapper) {
padding: ($unit * 1.5) $unit-2x;
}
&.accessory {
$offset: 2px;
align-items: center;
background: var(--input-bg);
border-radius: $input-corner;
box-sizing: border-box;
position: relative;
.counter {
color: var(--text-tertiary);
display: block;
font-weight: $bold;
line-height: 48px;
position: absolute;
right: $unit-2x;
top: 0;
}
input {
background: transparent;
border-radius: $input-corner;
border: 2px solid transparent;
box-sizing: border-box;
color: var(--text-primary);
padding: ($unit * 1.75) $unit-2x;
width: 100%;
&:focus {
border: 2px solid $blue;
outline: none;
}
}
}
&[type='number']::-webkit-inner-spin-button {
-webkit-appearance: none;
}
&.bound {
background-color: var(--input-bound-bg);
&:hover {
background-color: var(--input-bound-bg-hover);
}
}
&.duration {
background: transparent;
border: none;
padding: 0;
width: initial;
height: 100%;
padding: calc($unit-2x - 2px) 0;
&:hover {
background: transparent;
}
&:focus,
&:focus-visible {
border: none;
}
}
&.number {
text-align: right;
width: $unit-8x;
}
&.range {
text-align: right;
width: $unit-12x;
}
&.alignRight {
text-align: right;
}
}
.counter {
display: none;
}
}
.input::placeholder,
.input > input::placeholder {
color: var(--text-tertiary);
opacity: 1;
}

View file

@ -1,55 +0,0 @@
.Input {
-webkit-font-smoothing: antialiased;
background-color: var(--input-bg);
border: 2px solid transparent;
border-radius: $input-corner;
box-sizing: border-box;
display: block;
padding: calc($unit-2x - 2px);
width: 100%;
&[type='number']::-webkit-inner-spin-button {
-webkit-appearance: none;
}
&:focus {
border: 2px solid $blue;
outline: none;
}
&.Bound {
background-color: var(--input-bound-bg);
&:hover {
background-color: var(--input-bound-bg-hover);
}
&::placeholder {
/* Chrome, Firefox, Opera, Safari 10.1+ */
color: var(--text-tertiary) !important;
}
}
&.AlignRight {
text-align: right;
}
&.Hidden {
display: none;
}
}
.InputError {
color: $error;
font-size: $font-tiny;
margin: $unit 0;
padding: calc($unit / 2) ($unit * 2);
min-width: 100%;
width: 0;
}
.Input::placeholder {
/* Chrome, Firefox, Opera, Safari 10.1+ */
color: var(--text-secondary);
opacity: 1; /* Firefox */
}

View file

@ -1,59 +1,117 @@
import React, { useEffect, useState } from 'react'
import classNames from 'classnames'
import styles from './index.module.scss'
import './index.scss'
interface Props
extends React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> {
visible?: string
interface Props extends React.ComponentProps<'input'> {
bound?: boolean
error?: string
label?: string
fieldsetClassName?: String
wrapperClassName?: string
hide1Password?: boolean
showCounter?: boolean
}
const defaultProps = {
visible: 'true',
bound: false,
hide1Password: true,
showCounter: false,
}
const Input = React.forwardRef<HTMLInputElement, Props>(function Input(
props: Props,
{
value: initialValue,
bound,
label,
error,
showCounter,
fieldsetClassName,
wrapperClassName,
...props
}: Props,
forwardedRef
) {
// States
const [inputValue, setInputValue] = useState('')
const [value, setValue] = useState(initialValue)
const [currentCount, setCurrentCount] = useState(() =>
props.maxLength ? props.maxLength - (`${value}` || '').length : 0
)
useEffect(() => {
setValue(initialValue)
}, [initialValue])
// Classes
const classes = classNames({ Input: true }, props.className)
const { defaultValue, ...inputProps } = props
const fieldsetClasses = classNames(
{
[styles.fieldset]: true,
},
fieldsetClassName?.split(' ').map((className) => styles[className])
)
// Change value when prop updates
const inputWrapperClasses = classNames(
{
[styles.wrapper]: true,
[styles.accessory]: showCounter,
[styles.input]: showCounter,
[styles.bound]: showCounter && bound,
},
wrapperClassName?.split(' ').map((className) => styles[className])
)
const inputClasses = classNames(
{
[styles.input]: !showCounter,
[styles.bound]: !showCounter && bound,
},
!showCounter &&
props.className?.split(' ').map((className) => styles[className])
)
const { defaultValue, hide1Password, ...inputProps } = props
// Hooks
useEffect(() => {
if (props.value) setInputValue(`${props.value}`)
}, [props.value])
if (props.maxLength)
setCurrentCount(props.maxLength - (`${value}` || '').length)
}, [props.maxLength, value])
// Event handlers
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
setInputValue(event.target.value)
setValue(event.target.value)
if (props.onChange) props.onChange(event)
}
return (
<React.Fragment>
// Rendering
const input = (
<div className={inputWrapperClasses}>
<input
{...inputProps}
autoComplete="off"
className={classes}
value={inputValue}
ref={forwardedRef}
data-1p-ignore={props.hide1Password}
autoComplete={props.autoComplete}
className={inputClasses}
type={props.type}
name={props.name}
placeholder={props.placeholder}
value={value || ''}
onBlur={props.onBlur}
onChange={handleChange}
maxLength={props.maxLength}
ref={forwardedRef}
formNoValidate
/>
{props.error && props.error.length > 0 && (
<p className="InputError">{props.error}</p>
)}
</React.Fragment>
<span className={styles.counter}>{currentCount}</span>
</div>
)
const fieldset = (
<fieldset className={fieldsetClasses}>
{label && <legend className={styles.legend}>{label}</legend>}
{input}
{error && <span className={styles.error}>{error}</span>}
</fieldset>
)
return fieldset
})
Input.defaultProps = defaultProps

View file

@ -2,8 +2,7 @@ import { useEffect, useState } from 'react'
import Input from '~components/common/Input'
import TableField from '~components/common/TableField'
import './index.scss'
import classNames from 'classnames'
import styles from './index.module.scss'
interface Props
extends React.DetailedHTMLProps<
@ -44,18 +43,21 @@ const InputTableField = ({
<TableField
{...props}
name={props.name || ''}
className={classNames({ InputField: true }, props.className)}
imageAlt={imageAlt}
imageClass={imageClass}
imageSrc={imageSrc}
className={styles.nameField}
image={{
alt: imageAlt,
className: imageClass,
src: imageSrc ? imageSrc : [],
}}
label={label}
>
<Input
className="Bound"
className={props.className}
placeholder={props.placeholder}
value={inputValue ? `${inputValue}` : ''}
step={1}
tabIndex={props.tabIndex}
bound={true}
type={props.type}
onChange={onInputChange}
/>

View file

@ -1,5 +0,0 @@
.Label {
box-sizing: border-box;
display: grid;
width: 100%;
}

View file

@ -1,68 +0,0 @@
import React, { useEffect, useState } from 'react'
import classNames from 'classnames'
import './index.scss'
interface Props
extends React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> {
visible?: boolean
error?: string
label?: string
}
const defaultProps = {
visible: true,
}
const LabelledInput = React.forwardRef<HTMLInputElement, Props>(function Input(
props: Props,
forwardedRef
) {
// States
const [inputValue, setInputValue] = useState('')
// Classes
const classes = classNames({ Input: true }, props.className)
const { defaultValue, visible, ...inputProps } = props
// Change value when prop updates
useEffect(() => {
if (props.value) setInputValue(`${props.value}`)
}, [props.value])
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
setInputValue(event.target.value)
if (props.onChange) props.onChange(event)
}
return (
<label
className={classNames({
Label: true,
Visible: props.visible,
})}
htmlFor={props.name}
>
<input
{...inputProps}
autoComplete="off"
className={classes}
value={inputValue}
ref={forwardedRef}
onChange={handleChange}
formNoValidate
/>
{props.label}
{props.error && props.error.length > 0 && (
<p className="InputError">{props.error}</p>
)}
</label>
)
})
LabelledInput.defaultProps = defaultProps
export default LabelledInput

View file

@ -1,4 +1,5 @@
.Overlay {
.overlay {
pointer-events: auto;
isolation: isolate;
position: fixed;
z-index: 9;
@ -7,15 +8,29 @@
bottom: 0;
left: 0;
&.Job {
&.alert {
z-index: 11;
}
&.job {
animation: none;
backdrop-filter: blur(5px) saturate(100%) brightness(80%) opacity(1);
}
&.Visible {
&.visible {
animation: 0.24s ease-in fadeInFilter;
animation-fill-mode: forwards;
backdrop-filter: blur(5px) saturate(100%) brightness(80%) opacity(0);
background: rgba(0, 0, 0, 0.6);
}
@keyframes fadeInFilter {
from {
backdrop-filter: blur(5px) saturate(100%) brightness(80%) opacity(0);
}
to {
backdrop-filter: blur(5px) saturate(100%) brightness(80%) opacity(1);
}
}
}

Some files were not shown because too many files have changed in this diff Show more