feat(space): goal complete를 중앙 모달로 재구성
This commit is contained in:
@@ -4,22 +4,7 @@ import type { FormEvent } from 'react';
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { copy } from '@/shared/i18n';
|
import { copy } from '@/shared/i18n';
|
||||||
import { cn } from '@/shared/lib/cn';
|
import { cn } from '@/shared/lib/cn';
|
||||||
import {
|
import { HUD_FIELD } from './overlayStyles';
|
||||||
HUD_FIELD,
|
|
||||||
HUD_OPTION_CHEVRON,
|
|
||||||
HUD_OPTION_ROW,
|
|
||||||
HUD_OPTION_ROW_BREAK,
|
|
||||||
HUD_OPTION_ROW_PRIMARY,
|
|
||||||
HUD_REVEAL_BASE,
|
|
||||||
HUD_REVEAL_COMPLETE,
|
|
||||||
HUD_REVEAL_HIDDEN,
|
|
||||||
HUD_TRAY_CONTENT,
|
|
||||||
HUD_TEXT_LINK,
|
|
||||||
HUD_TEXT_LINK_STRONG,
|
|
||||||
HUD_TRAY_HAIRLINE,
|
|
||||||
HUD_TRAY_LAYER,
|
|
||||||
HUD_TRAY_SHELL,
|
|
||||||
} from './overlayStyles';
|
|
||||||
|
|
||||||
interface GoalCompleteSheetProps {
|
interface GoalCompleteSheetProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -100,14 +85,14 @@ export const GoalCompleteSheet = ({
|
|||||||
isTimerCompleteMode
|
isTimerCompleteMode
|
||||||
? copy.space.goalComplete.timerTitle
|
? copy.space.goalComplete.timerTitle
|
||||||
: view === 'next'
|
: view === 'next'
|
||||||
? copy.space.goalComplete.nextTitle
|
? copy.space.goalComplete.nextTitle
|
||||||
: copy.space.goalComplete.title;
|
: copy.space.goalComplete.title;
|
||||||
const description =
|
const description =
|
||||||
isTimerCompleteMode
|
isTimerCompleteMode
|
||||||
? copy.space.goalComplete.timerDescription
|
? copy.space.goalComplete.timerDescription
|
||||||
: view === 'next'
|
: view === 'next'
|
||||||
? copy.space.goalComplete.nextDescription
|
? copy.space.goalComplete.nextDescription
|
||||||
: copy.space.goalComplete.description;
|
: copy.space.goalComplete.description;
|
||||||
|
|
||||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -165,197 +150,195 @@ export const GoalCompleteSheet = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const baseButtonClass =
|
||||||
|
'inline-flex min-h-[3.5rem] items-center justify-center rounded-[18px] border px-4 py-3 text-center text-[13px] font-medium tracking-[0.01em] transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/12 disabled:cursor-not-allowed disabled:opacity-40';
|
||||||
|
const secondaryButtonClass =
|
||||||
|
'border-white/10 bg-white/[0.04] text-white/72 hover:border-white/16 hover:bg-white/[0.07] hover:text-white';
|
||||||
|
const primaryButtonClass =
|
||||||
|
'border-white/14 bg-white/[0.12] text-white hover:border-white/22 hover:bg-white/[0.17]';
|
||||||
|
const breakButtonClass =
|
||||||
|
'border-emerald-200/14 bg-[rgba(16,38,31,0.44)] text-emerald-50 hover:border-emerald-200/20 hover:bg-[rgba(16,38,31,0.58)]';
|
||||||
|
|
||||||
|
const renderGoalCard = () => {
|
||||||
|
if (!trimmedCurrentGoal) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-[24px] border border-white/8 bg-black/14 px-4 py-4 text-left shadow-[inset_0_1px_0_rgba(255,255,255,0.05)]">
|
||||||
|
<p className="text-[10px] font-medium uppercase tracking-[0.22em] text-white/34">
|
||||||
|
{copy.space.goalComplete.currentGoalLabel}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-[15px] font-medium leading-[1.45] tracking-[-0.01em] text-white/88">
|
||||||
|
{trimmedCurrentGoal}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
HUD_REVEAL_BASE,
|
'pointer-events-none fixed inset-0 z-50 flex items-center justify-center px-4 transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)]',
|
||||||
open ? HUD_REVEAL_COMPLETE : HUD_REVEAL_HIDDEN,
|
open ? 'opacity-100' : 'opacity-0',
|
||||||
)}
|
)}
|
||||||
aria-hidden={!open}
|
aria-hidden={!open}
|
||||||
>
|
>
|
||||||
<section className={HUD_TRAY_SHELL}>
|
<div
|
||||||
<div aria-hidden className={HUD_TRAY_LAYER} />
|
className={cn(
|
||||||
<div aria-hidden className={HUD_TRAY_HAIRLINE} />
|
'absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(5,7,11,0.18)_0%,rgba(2,6,23,0.58)_45%,rgba(2,6,23,0.78)_100%)] backdrop-blur-[10px] transition-opacity duration-300',
|
||||||
|
open ? 'opacity-100' : 'opacity-0',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={HUD_TRAY_CONTENT}>
|
<section
|
||||||
<header className="flex items-start justify-between gap-2">
|
className={cn(
|
||||||
<div>
|
'pointer-events-auto relative w-full max-w-[42rem] overflow-hidden rounded-[30px] border border-white/12 bg-[linear-gradient(180deg,rgba(18,22,30,0.94)_0%,rgba(9,12,18,0.92)_100%)] text-white shadow-[0_28px_90px_rgba(2,6,23,0.48)] transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)]',
|
||||||
<p className="text-[11px] font-medium tracking-[0.08em] text-white/38">목표 완료</p>
|
open ? 'translate-y-0 scale-100' : 'translate-y-4 scale-[0.975]',
|
||||||
<h3 className="mt-1 max-w-[22rem] text-[1rem] font-medium tracking-tight text-white/94">{title}</h3>
|
)}
|
||||||
<p className="mt-1 max-w-[21rem] text-[12px] leading-[1.55] text-white/56">{description}</p>
|
role="dialog"
|
||||||
</div>
|
aria-modal="true"
|
||||||
|
aria-labelledby="goal-complete-title"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute inset-0 bg-[radial-gradient(110%_90%_at_50%_0%,rgba(255,255,255,0.12)_0%,rgba(255,255,255,0.02)_42%,rgba(255,255,255,0)_100%)]"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute inset-x-0 top-0 h-px bg-white/18"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative px-7 py-7 md:px-9 md:py-8">
|
||||||
|
<header className="relative text-center">
|
||||||
{isTimerCompleteMode ? null : (
|
{isTimerCompleteMode ? null : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className="inline-flex h-8 w-8 items-center justify-center rounded-full border border-white/10 bg-black/14 text-[11px] text-white/72 backdrop-blur-md transition-all hover:bg-black/20 hover:text-white disabled:cursor-not-allowed disabled:border-white/6 disabled:bg-black/10 disabled:text-white/26"
|
className="absolute right-0 top-0 inline-flex h-10 w-10 items-center justify-center rounded-full border border-white/10 bg-white/[0.04] text-[12px] text-white/64 transition-all hover:border-white/16 hover:bg-white/[0.08] hover:text-white disabled:cursor-not-allowed disabled:opacity-30"
|
||||||
aria-label={copy.space.goalComplete.closeAriaLabel}
|
aria-label={copy.space.goalComplete.closeAriaLabel}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-[18px] border border-white/10 bg-white/[0.06] shadow-[inset_0_1px_0_rgba(255,255,255,0.08)]">
|
||||||
|
<span className="text-[20px] text-white/88">{isTimerCompleteMode ? '⌛' : '✦'}</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-5 text-[11px] font-medium tracking-[0.14em] text-white/34">GOAL COMPLETE</p>
|
||||||
|
<h3
|
||||||
|
id="goal-complete-title"
|
||||||
|
className="mx-auto mt-2 max-w-[28rem] text-[1.65rem] font-light leading-[1.16] tracking-[-0.03em] text-white/96 md:text-[1.9rem]"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p className="mx-auto mt-3 max-w-[28rem] text-[14px] leading-[1.7] text-white/56">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{isTimerCompleteMode ? (
|
<div className="mt-6 space-y-4">
|
||||||
<div className="mt-3 space-y-3">
|
{renderGoalCard()}
|
||||||
{trimmedCurrentGoal ? (
|
|
||||||
<div className="rounded-[18px] border border-white/8 bg-black/10 px-3.5 py-3">
|
|
||||||
<p className="text-[11px] font-medium uppercase tracking-[0.16em] text-white/36">
|
|
||||||
{copy.space.goalComplete.currentGoalLabel}
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 truncate text-[14px] text-white/86">{trimmedCurrentGoal}</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<footer className="mt-4 space-y-2">
|
{isTimerCompleteMode ? (
|
||||||
<button
|
<footer className="grid grid-cols-2 gap-3 pt-2">
|
||||||
type="button"
|
|
||||||
onClick={handleFinish}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className={cn(HUD_OPTION_ROW, HUD_OPTION_ROW_PRIMARY)}
|
|
||||||
>
|
|
||||||
<div className="max-w-[20.5rem]">
|
|
||||||
<p className="text-[13px] font-semibold tracking-[0.01em] text-white/90">
|
|
||||||
{submissionMode === 'finish'
|
|
||||||
? copy.space.goalComplete.timerFinishPending
|
|
||||||
: copy.space.goalComplete.timerFinishButton}
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 text-[12px] leading-[1.55] text-white/48">
|
|
||||||
{copy.space.goalComplete.timerFinishDescription}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span aria-hidden className={HUD_OPTION_CHEVRON}>→</span>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleExtend}
|
onClick={handleExtend}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className={HUD_OPTION_ROW}
|
className={cn(baseButtonClass, secondaryButtonClass)}
|
||||||
>
|
>
|
||||||
<div className="max-w-[20.5rem]">
|
{submissionMode === 'extend'
|
||||||
<p className="text-[13px] font-medium tracking-[0.01em] text-white/78">
|
? copy.space.goalComplete.extendPending
|
||||||
{submissionMode === 'extend'
|
: copy.space.goalComplete.extendButton}
|
||||||
? copy.space.goalComplete.extendPending
|
|
||||||
: copy.space.goalComplete.extendButton}
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 text-[12px] leading-[1.55] text-white/44">
|
|
||||||
{copy.space.goalComplete.extendDescription}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span aria-hidden className={HUD_OPTION_CHEVRON}>→</span>
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
) : view === 'choice' ? (
|
|
||||||
<div className="mt-3 space-y-3">
|
|
||||||
{trimmedCurrentGoal ? (
|
|
||||||
<div className="rounded-[18px] border border-white/8 bg-black/10 px-3.5 py-3">
|
|
||||||
<p className="text-[11px] font-medium uppercase tracking-[0.16em] text-white/36">
|
|
||||||
{copy.space.goalComplete.currentGoalLabel}
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 truncate text-[14px] text-white/86">{trimmedCurrentGoal}</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<footer className="mt-4 space-y-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setView('next')}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className={cn(HUD_OPTION_ROW, HUD_OPTION_ROW_PRIMARY)}
|
|
||||||
>
|
|
||||||
<div className="max-w-[20.5rem]">
|
|
||||||
<p className="text-[13px] font-semibold tracking-[0.01em] text-white/90">
|
|
||||||
{copy.space.goalComplete.chooseNextButton}
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 text-[12px] leading-[1.55] text-white/48">
|
|
||||||
{copy.space.goalComplete.chooseNextDescription}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span aria-hidden className={HUD_OPTION_CHEVRON}>→</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onRest}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className={cn(HUD_OPTION_ROW, HUD_OPTION_ROW_BREAK)}
|
|
||||||
>
|
|
||||||
<div className="max-w-[20.5rem]">
|
|
||||||
<p className="text-[13px] font-medium tracking-[0.01em] text-white/78">
|
|
||||||
{copy.space.goalComplete.restButton}
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 text-[12px] leading-[1.55] text-emerald-50/56">
|
|
||||||
{copy.space.goalComplete.restDescription}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span aria-hidden className={cn(HUD_OPTION_CHEVRON, 'text-emerald-100/34 group-hover:text-emerald-100/58')}>→</span>
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleFinish}
|
onClick={handleFinish}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className={HUD_OPTION_ROW}
|
className={cn(baseButtonClass, primaryButtonClass)}
|
||||||
>
|
>
|
||||||
<div className="max-w-[20.5rem]">
|
{submissionMode === 'finish'
|
||||||
<p className="text-[13px] font-medium tracking-[0.01em] text-white/78">
|
? copy.space.goalComplete.timerFinishPending
|
||||||
{submissionMode === 'finish'
|
: copy.space.goalComplete.timerFinishButton}
|
||||||
? copy.space.goalComplete.finishPending
|
|
||||||
: copy.space.goalComplete.finishButton}
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 text-[12px] leading-[1.55] text-white/44">
|
|
||||||
{copy.space.goalComplete.finishDescription}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span aria-hidden className={HUD_OPTION_CHEVRON}>→</span>
|
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
) : view === 'choice' ? (
|
||||||
) : (
|
<footer className="grid grid-cols-3 gap-3 pt-2">
|
||||||
<form className="mt-3 space-y-3" onSubmit={handleSubmit}>
|
|
||||||
{trimmedCurrentGoal ? (
|
|
||||||
<div className="rounded-[18px] border border-white/8 bg-black/10 px-3.5 py-3">
|
|
||||||
<p className="text-[11px] font-medium uppercase tracking-[0.16em] text-white/36">
|
|
||||||
{copy.space.goalComplete.currentGoalLabel}
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 truncate text-[14px] text-white/86">{trimmedCurrentGoal}</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<label className="block">
|
|
||||||
<span className="mb-2 block text-[11px] font-medium uppercase tracking-[0.16em] text-white/36">
|
|
||||||
{copy.space.goalComplete.nextGoalLabel}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
value={draft}
|
|
||||||
onChange={(event) => setDraft(event.target.value)}
|
|
||||||
placeholder={placeholder}
|
|
||||||
className={HUD_FIELD}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<footer className="mt-3 flex items-center justify-end gap-2">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setView('choice')}
|
onClick={onRest}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className={HUD_TEXT_LINK}
|
className={cn(baseButtonClass, breakButtonClass)}
|
||||||
>
|
>
|
||||||
{copy.space.goalComplete.backButton}
|
{copy.space.goalComplete.restButton}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="button"
|
||||||
disabled={!canConfirm || isSubmitting}
|
onClick={handleFinish}
|
||||||
className={HUD_TEXT_LINK_STRONG}
|
disabled={isSubmitting}
|
||||||
|
className={cn(baseButtonClass, secondaryButtonClass)}
|
||||||
>
|
>
|
||||||
{submissionMode === 'next'
|
{submissionMode === 'finish'
|
||||||
? copy.space.goalComplete.confirmPending
|
? copy.space.goalComplete.finishPending
|
||||||
: copy.space.goalComplete.confirmButton}
|
: copy.space.goalComplete.finishButton}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setView('next')}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className={cn(baseButtonClass, primaryButtonClass)}
|
||||||
|
>
|
||||||
|
{copy.space.goalComplete.chooseNextButton}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</form>
|
) : (
|
||||||
)}
|
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||||
|
<div className="text-left">
|
||||||
|
<label
|
||||||
|
htmlFor="goal-complete-next-goal"
|
||||||
|
className="text-[11px] font-medium tracking-[0.12em] text-white/36"
|
||||||
|
>
|
||||||
|
{copy.space.goalComplete.nextGoalLabel}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
id="goal-complete-next-goal"
|
||||||
|
value={draft}
|
||||||
|
onChange={(event) => setDraft(event.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className={cn(HUD_FIELD, 'mt-2 h-[3.25rem] rounded-[20px] bg-white/[0.05]')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer className="grid grid-cols-2 gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (isSubmitting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setView('choice');
|
||||||
|
}}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className={cn(baseButtonClass, secondaryButtonClass)}
|
||||||
|
>
|
||||||
|
{copy.space.goalComplete.backButton}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!canConfirm || isSubmitting}
|
||||||
|
className={cn(baseButtonClass, primaryButtonClass)}
|
||||||
|
>
|
||||||
|
{submissionMode === 'next'
|
||||||
|
? copy.space.goalComplete.confirmPending
|
||||||
|
: copy.space.goalComplete.confirmButton}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user