Skip to content

Commit

Permalink
Merge pull request #47 from softeerbootcamp4th/TASK-168
Browse files Browse the repository at this point in the history
[Feature][Task-168] 캐스퍼 레이싱 대시보드 구현 & 레이싱 컨트롤 버튼 기능 보완
  • Loading branch information
nim-od authored Aug 12, 2024
2 parents 7e0e8ca + 012c758 commit d16c6a0
Show file tree
Hide file tree
Showing 42 changed files with 335 additions and 146 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/user/public/images/racing/front/pet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions packages/user/src/assets/icons/car-marker.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 9 additions & 2 deletions packages/user/src/components/common/GradientBorderWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { PropsWithChildren } from 'react';

export default function GradientBorderWrapper({ children }: PropsWithChildren) {
interface GradientBorderWrapperProps {
className?: string | undefined;
}

export default function GradientBorderWrapper({
children,
className,
}: PropsWithChildren<GradientBorderWrapperProps>) {
return (
<div className="gradient-border cursor-pointer rounded-[11px]">
<div className={`gradient-border cursor-pointer rounded-[11px] ${className}`}>
<div className="bg-background rounded-[9px]">{children}</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/user/src/components/event/chatting/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ChatInputArea from './inputArea/index.tsx';
/** 실시간 기대평 섹션 */
export default function RealTimeChatting() {
return (
<section className="container flex max-w-[1200px] flex-col items-center pb-[115px] pt-[95px]">
<section className="container flex max-w-[1200px] snap-start flex-col items-center pb-[115px] pt-[150px]">
<h6 className="text-heading-10 mb-[25px] font-medium">기대평을 남겨보세요!</h6>
<ChatInputArea />
<div className="h-[1000px] w-full overflow-y-auto rounded-[10px] bg-neutral-800 py-10">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,39 @@
import { Category } from '@softeer/common/types';
import { useEffect, useRef, useState } from 'react';
import Lightning from 'src/assets/icons/lighting.svg?react';
import { useToast } from 'src/hooks/useToast.ts';
import type { Rank } from 'src/types/rank.d.ts';
import Gauge from './Gauge.tsx';
import GaugeButton from './GaugeButton.tsx';
import TeamButton from './TeamButton.tsx';

const ranks = [1, 2, 3, 4] as const;
export type Rank = typeof ranks[number];

interface TeamGaugeButtonProps {
interface ControlButtonProps {
type: Category;
rank: Rank;
percentage: number;
onScale: () => void;
}

const MAX_CLICK = 30;
const MAX_CLICK = 10;
const MIN_PERCENT = 2;
const RESET_SECOND = 1000;
const RESET_SECOND = 10000;

export default function TeamGaugeButton({
export default function ControlButton({
type,
rank,
percentage: originPercentage,
}: TeamGaugeButtonProps) {
const { progress, clickCount, handleClick } = useGaugeProgress(originPercentage);
onScale,
}: ControlButtonProps) {
const { progress, clickCount, handleClick } = useGaugeProgress(originPercentage, onScale);

return (
<div className="flex flex-col gap-3">
<div
className={`absolute flex transform flex-col gap-3 transition-all duration-500 ease-in-out ${styles[rank]}`}
>
<div className="flex items-center gap-2">
<Lightning />
<Gauge percent={progress} />
</div>
<GaugeButton
<TeamButton
onClick={handleClick}
disabled={clickCount === MAX_CLICK}
rank={rank}
Expand All @@ -41,7 +44,16 @@ export default function TeamGaugeButton({
);
}

function useGaugeProgress(originPercentage: number) {
const styles: Record<Rank, string> = {
1: 'left-[40px] z-40',
2: 'left-[310px] z-30',
3: 'left-[580px] z-20',
4: 'left-[850px] z-10',
};

function useGaugeProgress(originPercentage: number, onClick: () => void) {
const { toast } = useToast();

const [progress, setProgress] = useState(0);
const [clickCount, setClickCount] = useState(0);
const initPercentageRef = useRef(originPercentage);
Expand All @@ -61,12 +73,18 @@ function useGaugeProgress(originPercentage: number) {
}

if (clickCount === MAX_CLICK) {
toast({ description: '배터리가 떨어질 때까지 기다려주세요!' });
const resetTimer = setTimeout(resetToInitProgress, RESET_SECOND);
return () => clearTimeout(resetTimer);
}
}, [clickCount, originPercentage]);

const handleClick = () => clickCount < MAX_CLICK && setClickCount((count) => count + 1);
const handleClick = () => {
if (clickCount < MAX_CLICK) {
setClickCount((count) => count + 1);
onClick();
}
};

const updateProgress = (count: number) => {
const newProgress = MIN_PERCENT + (100 - MIN_PERCENT) * (count / MAX_CLICK);
Expand All @@ -78,5 +96,5 @@ function useGaugeProgress(originPercentage: number) {
setProgress(initPercentageRef.current);
};

return { progress, clickCount, handleClick };
return { progress, clickCount, handleClick };
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ export default function Gauge({ percent }: GaugeProps) {
const backgroundColor = useMemo(() => {
if (percent < 22) return 'bg-gradient-gauge1';
if (percent < 57) return 'bg-gradient-gauge2';
if (percent < 88) return 'bg-gradient-gauge3';
return 'bg-gradient-gauge4';
if (percent < 88) return 'bg-gradient-gauge3';
return 'bg-gradient-gauge4';
}, [percent]);

return (
Expand Down
69 changes: 69 additions & 0 deletions packages/user/src/components/event/racing/controls/TeamButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Category } from '@softeer/common/types';
import { ButtonHTMLAttributes } from 'react';
import GradientBorderWrapper from 'src/components/common/GradientBorderWrapper.tsx';
import { TEAM_DESCRIPTIONS } from 'src/constants/teamDescriptions.ts';
import type { Rank } from 'src/types/rank.d.ts';

const imageUrls: Record<Category, string> = {
travel: '/images/racing/side/travel.png',
leisure: '/images/racing/side/leisure.png',
place: '/images/racing/side/place.png',
pet: '/images/racing/side/pet.png',
};

interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {
type: Category;
rank: Rank;
percent: number;
}

export default function TeamButton({
type,
rank,
percent,
disabled = false,
...props
}: ButtonProps) {
const { shortTitle, title } = TEAM_DESCRIPTIONS[type];
const imageUrl = imageUrls[type];

const displayTitle = shortTitle ?? title;
const formattedPercent = percent.toFixed(2);

const styles = getStyles({ type, isActive: !disabled });

return (
<button type="button" disabled={disabled} className={styles.button} {...props}>
<GradientBorderWrapper className={styles.borderWrapper}>
<div className={styles.innerborderWrapper}>
<h2 className="pt-2">{rank}</h2>
<div className="flex flex-col items-center">
<p className={styles.percent}>{formattedPercent}%</p>
<h6>{displayTitle}</h6>
</div>
</div>
</GradientBorderWrapper>
<img src={imageUrl} alt={`${title} 팀 캐스퍼 실물`} className={styles.image} />
</button>
);
}

function getStyles({ type, isActive }: { type: Category; isActive: boolean }) {
const { bgStyles, fontStyles } = styles[type];
const imageBaseStyles = 'absolute -bottom-[25px] -right-[18px] z-10 w-[100px] object-contain';

return {
percent: `text-body-3 font-medium ${fontStyles}`,
image: `${imageBaseStyles} ${isActive ? 'transition-transform duration-300 ease-out group-active:scale-125' : ''}`,
button: 'relative overflow-visible group disabled:opacity-50',
borderWrapper: isActive ? 'group-active:animate-rotate' : '',
innerborderWrapper: `flex h-[84px] w-[240px] gap-7 rounded-[inherit] px-[10px] py-[10px] ${bgStyles}`,
};
}

const styles: Record<Category, { bgStyles: string; fontStyles: string }> = {
travel: { bgStyles: 'bg-gradient-cards1', fontStyles: 'text-orange-500' },
leisure: { bgStyles: 'bg-gradient-cards2', fontStyles: 'text-yellow-500' },
place: { bgStyles: 'bg-gradient-cards3', fontStyles: 'text-neutral-200' },
pet: { bgStyles: 'bg-gradient-cards4', fontStyles: 'text-yellow-500' },
};
25 changes: 25 additions & 0 deletions packages/user/src/components/event/racing/controls/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { CATEGORIES } from '@softeer/common/constants';
import { Category } from '@softeer/common/types';
import { CategoryRankMap } from 'src/types/rank.js';
import ControlButton from './ControlButton.tsx';

interface RacingControlsProps {
ranks: CategoryRankMap;
setScaledType: (type: Category) => void;
}

export default function RacingControls({ ranks, setScaledType }: RacingControlsProps) {
return (
<div className="relative mt-[50px] h-[300px] w-full">
{CATEGORIES.map((type) => (
<ControlButton
key={type}
type={type}
rank={ranks[type]}
percentage={25}
onScale={() => setScaledType(type)}
/>
))}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default function Background() {
return (
<img
className="absolute -z-10 h-full w-full object-contain"
alt="레이싱 배경"
src="/images/racing/background.png"
/>
);
}
41 changes: 41 additions & 0 deletions packages/user/src/components/event/racing/dashboard/Casper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Category } from '@softeer/common/types';
import MarkerIcon from 'src/assets/icons/car-marker.svg?react';
import useAuth from 'src/hooks/useAuth.tsx';
import type { Rank } from 'src/types/rank.d.ts';

interface CasperProps {
type:Category
rank: Rank;
className: string;
}
export default function Casper({ type, rank, className }: CasperProps) {
const { user } = useAuth();
const isMyCasper = user?.type === type;

return (
<div className={`flex flex-col items-center gap-8 absolute ${className} ${rankStyles[rank]} ${transitionStyles}`}>
<div className="h-[10px]">{isMyCasper && <MarkerIcon /> }</div>
<img
src={imageUrls[type]}
alt={`${rank}등 차`}
className="object-contain"
/>
</div>
);
}

const transitionStyles = 'transform transition-all duration-700 ease-in-out';

const rankStyles: Record<Rank, string> = {
1: 'w-[335px] left-[378px] top-[295px] z-40 rotate-0',
2: 'w-[260px] left-[170px] top-[345px] z-30 -rotate-[4deg]',
3: 'w-[230px] left-[850px] top-[360px] z-20 rotate-6',
4: 'w-[120px] left-[690px] top-[410px] z-10 rotate-[5deg]',
};

const imageUrls: Record<Category, string> = {
travel: '/images/racing/front/travel.png',
leisure: '/images/racing/front/leisure.png',
place: '/images/racing/front/place.png',
pet: '/images/racing/front/pet.png',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default function RacingTitle() {
return (
<>
<h3>
버튼을 연타해 승리를 <strong>CHARGE</strong>하세요!
</h3>
<div className="mb-5 flex items-center gap-3">
<p className="text-body-2 text-foreground/60">
1등에 가까워질 수 있도록 배터리를 가득 충전
<strong className="inline-block align-bottom leading-6">🔋</strong>해주세요!
</p>
</div>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface RacingTeamCardProps {
}
export default function RacingTeamCard({ user }: RacingTeamCardProps) {
return (
<TeamCard type={user.category} size="racing">
<TeamCard type={user.type} size="racing">
<button
type="button"
onClick={() => copyLink(user.shareUrl as string)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import TriggerButtonWrapper from 'src/components/common/TriggerButtonWrapper.tsx';
import useAuth from 'src/hooks/useAuth.tsx';
import TeamSelectModal from '../teamSelectModal/index.tsx';
import TeamSelectModal from './teamSelectModal/index.tsx';
import RacingTeamCard from './RacingTeamCard.tsx';
import UnassignedCard from './UnassignedCard.tsx';

Expand Down
59 changes: 59 additions & 0 deletions packages/user/src/components/event/racing/dashboard/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { CATEGORIES } from '@softeer/common/constants';
import { Category } from '@softeer/common/types';
import { Suspense } from 'react';
import type { CategoryRankMap } from 'src/types/rank.d.ts';
import Background from './Background.tsx';
import RacingCard from './card/index.tsx';
import Casper from './Casper.tsx';
import RacingTitle from './RacingTitle.tsx';
import EventTimer from './timer/index.tsx';

interface RacingDashboardProps {
ranks: CategoryRankMap;
scaledType: Category | null;
}

export default function RacingDashboard({ ranks, scaledType }: RacingDashboardProps) {
return (
<div className="relative h-[685px] w-full">
<HeaderSection />
<RacingCardSection />
<CaspersSection ranks={ranks} scaledType={scaledType} />
<Background />
</div>
);
}

function HeaderSection() {
return (
<div className="absolute -top-[5px] flex w-full flex-col items-center">
<RacingTitle />
<Suspense>
<EventTimer />
</Suspense>
</div>
);
}

function RacingCardSection() {
return (
<div className="absolute left-[27px] top-[95px]">
<RacingCard />
</div>
);
}

function CaspersSection({ ranks, scaledType }: RacingDashboardProps) {
return (
<>
{CATEGORIES.map((type) => (
<Casper
key={type}
type={type}
rank={ranks[type]}
className={scaledType === type ? 'scale-110' : ''}
/>
))}
</>
);
}
Loading

0 comments on commit d16c6a0

Please sign in to comment.