fix(app): premium entry 조정과 duration 입력 버그 수정
This commit is contained in:
@@ -57,13 +57,13 @@ export const AppAtmosphereEntryShell = ({
|
||||
{/* Main Focus Entry Ritual */}
|
||||
<div className="flex flex-1 flex-col items-center justify-center px-4 pb-10">
|
||||
{/* Inline Accessories (No overlap guarantee) */}
|
||||
<div className="mb-10 flex min-h-[5rem] flex-col items-center justify-end opacity-0 animate-fade-in-up delay-150">
|
||||
<div className="mb-12 flex min-h-[5rem] flex-col items-center justify-end opacity-0 animate-fade-in-up delay-150">
|
||||
{errorAccessory}
|
||||
{!errorAccessory && topAccessory}
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-4xl space-y-8 text-center opacity-0 animate-fade-in-up delay-150">
|
||||
<p className="text-sm font-medium uppercase tracking-[0.25em] text-white/50">
|
||||
<div className="w-full max-w-4xl text-center opacity-0 animate-fade-in-up delay-150 mb-16">
|
||||
<p className="text-xs font-bold uppercase tracking-[0.3em] text-white/40 drop-shadow-sm mb-6">
|
||||
What will you focus on?
|
||||
</p>
|
||||
<input
|
||||
@@ -77,62 +77,66 @@ export const AppAtmosphereEntryShell = ({
|
||||
}
|
||||
}}
|
||||
placeholder={goalPlaceholder}
|
||||
className="w-full bg-transparent text-center text-4xl font-light tracking-tight text-white outline-none placeholder:text-white/20 md:text-5xl lg:text-[4.5rem] lg:leading-[1.1]"
|
||||
className="w-full bg-transparent text-center text-4xl font-light tracking-tight text-white outline-none placeholder:text-white/20 md:text-5xl lg:text-[4.5rem] lg:leading-[1.1] transition-colors focus:placeholder:text-white/10"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col items-center space-y-8 opacity-0 animate-fade-in-up delay-150">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="flex items-center gap-4 rounded-[2rem] border border-white/10 bg-white/5 p-2 pr-6 shadow-2xl backdrop-blur-xl">
|
||||
<div className="flex gap-2">
|
||||
{durationSuggestions.map((minutes) => {
|
||||
const isSelected = durationDraft === String(minutes);
|
||||
return (
|
||||
<button
|
||||
key={minutes}
|
||||
type="button"
|
||||
onClick={() => onSelectDuration(minutes)}
|
||||
className={cn(
|
||||
'rounded-full px-5 py-2.5 text-sm font-medium transition-all duration-300',
|
||||
isSelected
|
||||
? 'bg-white text-black shadow-md'
|
||||
: 'text-white/70 hover:bg-white/10 hover:text-white',
|
||||
)}
|
||||
>
|
||||
{minutes}m
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="h-8 w-px bg-white/10" />
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
value={durationDraft}
|
||||
onChange={(event) => onDurationChange(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
onStartSession();
|
||||
}
|
||||
}}
|
||||
inputMode="numeric"
|
||||
placeholder="Custom"
|
||||
className="w-16 bg-transparent text-right text-lg font-medium text-white outline-none placeholder:text-white/30"
|
||||
/>
|
||||
<span className="text-sm text-white/50">min</span>
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-center space-y-10 opacity-0 animate-fade-in-up delay-150">
|
||||
|
||||
<div className="flex flex-col items-center">
|
||||
{/* Primary Action: Massive Custom Timer Input */}
|
||||
<div className="group relative flex items-baseline justify-center">
|
||||
<input
|
||||
value={durationDraft}
|
||||
onChange={(event) => onDurationChange(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
onStartSession();
|
||||
}
|
||||
}}
|
||||
inputMode="numeric"
|
||||
placeholder="0"
|
||||
className="w-24 bg-transparent text-center text-6xl font-light tracking-tighter text-white outline-none placeholder:text-white/10 transition-all focus:w-32 md:text-7xl"
|
||||
/>
|
||||
<span className="absolute -right-12 bottom-2 text-xl font-medium tracking-wide text-white/30 transition-colors group-focus-within:text-white/60 md:bottom-3 md:text-2xl">
|
||||
min
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-white/40">{durationHelper}</p>
|
||||
|
||||
{/* Secondary Action: Subtle Quick Select Pills */}
|
||||
<div className="mt-8 flex items-center gap-2 rounded-full border border-white/5 bg-[linear-gradient(145deg,rgba(255,255,255,0.04)_0%,rgba(255,255,255,0.01)_100%)] p-1.5 backdrop-blur-md">
|
||||
{durationSuggestions.map((minutes) => {
|
||||
const isSelected = durationDraft === String(minutes);
|
||||
return (
|
||||
<button
|
||||
key={minutes}
|
||||
type="button"
|
||||
onClick={() => onSelectDuration(minutes)}
|
||||
className={cn(
|
||||
'rounded-full px-4 py-2 text-[12px] font-medium tracking-wide transition-all duration-300',
|
||||
isSelected
|
||||
? 'bg-white/10 text-white shadow-sm ring-1 ring-white/20'
|
||||
: 'text-white/40 hover:text-white/80',
|
||||
)}
|
||||
>
|
||||
{minutes}m
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<p className="mt-6 text-[11px] font-medium tracking-wide text-white/30">{durationHelper}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onStartSession}
|
||||
disabled={!canStart}
|
||||
className="group relative flex h-16 items-center justify-center overflow-hidden rounded-full border border-white/20 bg-white/10 px-12 text-lg font-medium tracking-wide text-white shadow-2xl backdrop-blur-md transition-all duration-300 hover:bg-white/20 hover:scale-[1.02] active:scale-[0.98] disabled:pointer-events-none disabled:opacity-40"
|
||||
className="group relative flex h-14 items-center justify-center overflow-hidden rounded-full border border-white/20 bg-[linear-gradient(145deg,rgba(255,255,255,0.15)_0%,rgba(255,255,255,0.05)_100%)] px-12 text-[14px] font-medium tracking-widest text-white shadow-[0_0_40px_rgba(255,255,255,0.1)] backdrop-blur-2xl transition-all duration-500 hover:border-white/40 hover:bg-white/20 hover:shadow-[0_0_60px_rgba(255,255,255,0.2)] hover:scale-[1.03] active:scale-[0.98] disabled:pointer-events-none disabled:opacity-30 uppercase"
|
||||
>
|
||||
<span className="relative z-10">
|
||||
<span className="relative z-10 drop-shadow-md">
|
||||
{isStartingSession ? startButtonLoadingLabel : startButtonLabel}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -140,6 +140,15 @@ export const FocusDashboardWidget = () => {
|
||||
() => getAtmosphereOptionById(selectedAtmosphereId),
|
||||
[selectedAtmosphereId],
|
||||
);
|
||||
const rawDurationValue = useMemo(() => {
|
||||
const digitsOnly = durationDraft.replace(/[^\d]/g, '');
|
||||
if (!digitsOnly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number(digitsOnly);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}, [durationDraft]);
|
||||
const parsedDurationMinutes = parseDurationMinutes(durationDraft);
|
||||
const resolvedTimerPreset = useMemo(() => {
|
||||
const targetMinutes =
|
||||
@@ -178,9 +187,11 @@ export const FocusDashboardWidget = () => {
|
||||
isPro && reviewEntryPresetConfig ? `추천 ritual · ${reviewEntryPresetConfig.label}` : null;
|
||||
const reviewTeaserTitle = isPro ? entryCopy.reviewTitlePro : entryCopy.reviewTitle;
|
||||
const durationHelper =
|
||||
parsedDurationMinutes === null
|
||||
? 'Please enter the estimated duration in minutes.'
|
||||
: entryCopy.durationHelper;
|
||||
rawDurationValue !== null && rawDurationValue < 5
|
||||
? 'Please enter at least 5 minutes.'
|
||||
: parsedDurationMinutes === null
|
||||
? 'Please enter the estimated duration in minutes.'
|
||||
: entryCopy.durationHelper;
|
||||
const hasCurrentSession = Boolean(currentSession);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -301,13 +312,13 @@ export const FocusDashboardWidget = () => {
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 bg-cover bg-center transition-transform duration-[1.5s] ease-[cubic-bezier(0.22,1,0.36,1)]',
|
||||
isStartingSession ? 'scale-[1.08] blur-[2px] brightness-75' : 'scale-100 blur-0 brightness-100',
|
||||
isStartingSession ? 'scale-[1.08] blur-[2px] brightness-75' : 'scale-100 blur-[60px] brightness-50',
|
||||
)}
|
||||
style={getSceneStageBackgroundStyle(activeScene, sceneAssetMap?.[activeScene.id])}
|
||||
/>
|
||||
{/* Immersive Overlay Gradients */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.6)_100%)] mix-blend-multiply pointer-events-none" />
|
||||
<div className="absolute inset-0 bg-black/10 pointer-events-none" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.85)_100%)] mix-blend-multiply pointer-events-none" />
|
||||
<div className="absolute inset-0 bg-black/40 pointer-events-none" />
|
||||
|
||||
{/* Header */}
|
||||
<header className="absolute top-0 inset-x-0 z-50 flex items-center justify-between px-8 py-8 md:px-12 md:py-10">
|
||||
|
||||
Reference in New Issue
Block a user