* 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:
parent
8cbdb1838d
commit
9c3c36e81b
301 changed files with 8267 additions and 6976 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.GridRepCollection {
|
||||
.collection {
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
57
components/HovercardHeader/index.module.scss
Normal file
57
components/HovercardHeader/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
150
components/HovercardHeader/index.tsx
Normal file
150
components/HovercardHeader/index.tsx
Normal 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
|
||||
56
components/LanguageSwitch/index.module.scss
Normal file
56
components/LanguageSwitch/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
51
components/LanguageSwitch/index.tsx
Normal file
51
components/LanguageSwitch/index.tsx
Normal 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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.ChangelogUnit {
|
||||
.unit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
71
components/about/ContentUpdate/index.module.scss
Normal file
71
components/about/ContentUpdate/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
.Content.Version {
|
||||
.Contents {
|
||||
margin-bottom: $unit-3x;
|
||||
}
|
||||
|
||||
.Notes h4 {
|
||||
font-weight: $medium;
|
||||
font-size: $font-regular;
|
||||
margin-bottom: $unit-2x;
|
||||
}
|
||||
}
|
||||
|
|
@ -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}`)}
|
||||
|
|
|
|||
77
components/about/LinkItem/index.module.scss
Normal file
77
components/about/LinkItem/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
37
components/about/LinkItem/index.tsx
Normal file
37
components/about/LinkItem/index.tsx
Normal 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
|
||||
61
components/about/RoadmapPage/index.module.scss
Normal file
61
components/about/RoadmapPage/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}`}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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}`)}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
6
components/auth/LoginModal/index.module.scss
Normal file
6
components/auth/LoginModal/index.module.scss
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
padding: $unit-fourth $unit-4x;
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
.Login.DialogContent {
|
||||
gap: $unit;
|
||||
// min-width: $unit * 52;
|
||||
|
||||
.Fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
padding: 0 $unit-4x;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
22
components/auth/SignupModal/index.module.scss
Normal file
22
components/auth/SignupModal/index.module.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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">→</span>
|
||||
<div className="wrapper">
|
||||
<div className="character">
|
||||
<span className={styles.arrow}>→</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>
|
||||
|
|
|
|||
39
components/character/CharacterGrid/index.module.scss
Normal file
39
components/character/CharacterGrid/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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}`}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
// }
|
||||
}
|
||||
|
|
@ -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>
|
||||
{`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()}
|
||||
|
|
|
|||
32
components/character/CharacterModal/index.module.scss
Normal file
32
components/character/CharacterModal/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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()}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
.Joined .Input::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.ContextMenu {
|
||||
.menu {
|
||||
background: var(--menu-bg);
|
||||
border-radius: $input-corner;
|
||||
padding: $unit 0;
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.ContextItem {
|
||||
.item {
|
||||
color: var(--menu-text);
|
||||
font-size: $font-regular;
|
||||
padding: ($unit * 1.5) $unit-2x;
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>) => {
|
||||
|
|
|
|||
|
|
@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
19
components/common/DialogFooter/index.module.scss
Normal file
19
components/common/DialogFooter/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
39
components/common/DialogFooter/index.tsx
Normal file
39
components/common/DialogFooter/index.tsx
Normal 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
|
||||
87
components/common/DialogHeader/index.module.scss
Normal file
87
components/common/DialogHeader/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
components/common/DialogHeader/index.tsx
Normal file
44
components/common/DialogHeader/index.tsx
Normal 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
|
||||
16
components/common/DropdownMenuContent/index.module.scss
Normal file
16
components/common/DropdownMenuContent/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
17
components/common/DropdownMenuGroup/index.module.scss
Normal file
17
components/common/DropdownMenuGroup/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
23
components/common/DropdownMenuGroup/index.tsx
Normal file
23
components/common/DropdownMenuGroup/index.tsx
Normal 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
|
||||
113
components/common/DropdownMenuItem/index.module.scss
Normal file
113
components/common/DropdownMenuItem/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
39
components/common/DropdownMenuItem/index.tsx
Normal file
39
components/common/DropdownMenuItem/index.tsx
Normal 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
|
||||
6
components/common/DropdownMenuLabel/index.module.scss
Normal file
6
components/common/DropdownMenuLabel/index.module.scss
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.menuLabel {
|
||||
color: var(--text-tertiary);
|
||||
padding: $unit * 1.5 $unit * 1.5;
|
||||
font-size: $font-small;
|
||||
font-weight: $medium;
|
||||
}
|
||||
24
components/common/DropdownMenuLabel/index.tsx
Normal file
24
components/common/DropdownMenuLabel/index.tsx
Normal 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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
66
components/common/Hovercard/index.module.scss
Normal file
66
components/common/Hovercard/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
137
components/common/Input/index.module.scss
Normal file
137
components/common/Input/index.module.scss
Normal 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;
|
||||
}
|
||||
|
|
@ -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 */
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
.Label {
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
Loading…
Reference in a new issue