Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 희망 학점 입력 페이지 구현 #7

Merged
merged 4 commits into from
Feb 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"dependencies": {
"@stackflow/core": "^1.1.1",
"@stackflow/plugin-basic-ui": "^1.11.1",
"@stackflow/plugin-history-sync": "^1.7.1",
"@stackflow/plugin-renderer-basic": "^1.1.13",
"@stackflow/react": "^1.4.2",
"@tailwindcss/vite": "^4.0.0",
Expand Down
23 changes: 23 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions src/components/RollingNumber.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { HTMLMotionProps, motion, useSpring, useTransform } from 'motion/react';
import { useEffect } from 'react';

interface RollingNumberProps extends HTMLMotionProps<'span'> {
number: number;
}

const RollingNumber = ({ number, ...props }: RollingNumberProps) => {
const springValue = useSpring(number, {
mass: 0.8,
stiffness: 75,
damping: 15,
});

const displayNumber = useTransform(springValue, (latest) => Math.round(latest));

useEffect(() => {
springValue.set(number);
}, [number, springValue]);

return <motion.span {...props}>{displayNumber}</motion.span>;
};

export default RollingNumber;
140 changes: 140 additions & 0 deletions src/pages/DesiredCreditActivity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { AppScreen } from '@stackflow/plugin-basic-ui';
import { ActivityComponentType } from '@stackflow/react';

import { Check, ChevronDown } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import { useState } from 'react';
import AppBar from '../components/AppBar';
import RollingNumber from '../components/RollingNumber';
import { useDropdown } from '../hooks/useDropDown';

interface DesiredCreditParams {
majorRequired: number;
majorElective: number;
generalRequired: number;
}

const Classification = {
majorRequired: '전공필수',
majorElective: '전공선택',
generalRequired: '교양필수',
generalElective: '교양선택',
};

const MAX_CREDIT = 22;

const DesiredCreditActivity: ActivityComponentType<DesiredCreditParams> = ({ params: credit }) => {
const totalCredit = Object.values(credit).reduce((acc, credit) => acc + credit, 0);
const availableCredits = Array.from({ length: MAX_CREDIT - totalCredit + 1 }, (_, i) => i);

const [desiredCredit, setDesiredCredit] = useState(totalCredit);
const [generalElective, setGeneralElective] = useState(0);
const [showDropdown, setShowDropdown, dropDownRef] = useDropdown();

const handleCreditSelect = (credit: number) => {
setGeneralElective(credit);
setDesiredCredit(totalCredit + credit);
setShowDropdown(false);
};

return (
<AppScreen>
<div className="min-h-screen py-12">
<AppBar progress={100} />
<div className="mt-15 flex flex-col items-center">
<h2 className="text-center text-[28px] font-semibold">
사용자님의 이번학기 <br />
희망 학점은 <RollingNumber number={desiredCredit} className="text-primary" />
학점이군요!
</h2>
<span className="mt-1 font-light">희망 학점에 맞추어 교양선택과목을 추천해드릴게요.</span>
<div className="mt-15 grid grid-cols-2 gap-x-2.5 gap-y-6 px-12">
{Object.entries(credit).map(([key, value]) => (
<div key={key}>
<label className="mb-1.5 block text-sm">
{Classification[key as keyof typeof Classification]} 학점
</label>
<input
type="number"
disabled
value={value}
className="bg-basic-light text-primary w-full rounded-xl px-4 py-3 text-lg font-semibold"
/>
</div>
))}
<div>
<label className="mb-1.5 block text-sm">교양선택 학점</label>
<div className="relative" ref={dropDownRef}>
<div
className="bg-basic-light flex w-full items-center justify-between rounded-xl px-4 py-3"
onClick={() => setShowDropdown(!showDropdown)}
>
<button
type="button"
className={`text-lg font-semibold ${generalElective === 0 ? 'text-placeholder' : 'text-primary'}`}
>
{generalElective}
</button>
<ChevronDown className="size-4" />
</div>
<AnimatePresence>
{showDropdown && (
<motion.ul
className="bg-basic-light absolute z-10 mt-2 max-h-44 w-full overflow-y-auto rounded-xl border border-gray-200 shadow-sm"
initial={{ opacity: 0, y: -10 }}
animate={{
opacity: 1,
y: 0,
}}
exit={{
opacity: 0,
y: -10,
}}
transition={{
duration: 0.2,
}}
>
{availableCredits.map((availableCredit) => (
<li key={availableCredit}>
<button
type="button"
className="text-list flex w-full items-center justify-between rounded-xl px-4 py-2 text-lg font-semibold hover:bg-gray-100"
onClick={() => handleCreditSelect(availableCredit)}
>
{availableCredit}
{availableCredit === generalElective && (
<Check className="size-4 text-green-500" />
)}
</button>
</li>
))}
</motion.ul>
)}
</AnimatePresence>
</div>
</div>
</div>
{generalElective > 0 && (
<motion.button
type="button"
className="bg-primary mt-57 w-50 rounded-2xl py-3.5 font-semibold text-white"
initial={{
opacity: 0,
y: 20,
}}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.3,
ease: 'easeOut',
}}
>
네 맞아요
</motion.button>
)}
</div>
</div>
</AppScreen>
);
};

export default DesiredCreditActivity;
7 changes: 7 additions & 0 deletions src/pages/OnboardingActivity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ChapelInput from '../components/ChapelInput';
import DepartmentInput from '../components/DepartmentInput';
import GradeInput from '../components/GradeInput';
import studentMachine from '../machines/studentMachine';
import { useFlow } from '../stackflow';

const OnboardingActivity: ActivityComponentType = () => {
// localStorage에 저장된 state를 가져옴
Expand All @@ -22,6 +23,7 @@ const OnboardingActivity: ActivityComponentType = () => {
snapshot: restoredState,
});
const [progress, setProgress] = useState(0);
const { push } = useFlow();

useEffect(() => {
const stateProgressMap = {
Expand All @@ -40,6 +42,11 @@ const OnboardingActivity: ActivityComponentType = () => {
// localStorage에 state를 저장
localStorage.setItem('student', JSON.stringify(persistedState));
console.log(state.context);
push('DesiredCreditActivity', {
majorRequired: 6,
majorElective: 5,
generalRequired: 4,
});
};

return (
Expand Down
12 changes: 11 additions & 1 deletion src/stackflow.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { basicUIPlugin } from '@stackflow/plugin-basic-ui';
import { historySyncPlugin } from '@stackflow/plugin-history-sync';
import { basicRendererPlugin } from '@stackflow/plugin-renderer-basic';
import { stackflow } from '@stackflow/react';
import CourseSelectionActivity from './pages/CourseSelectionActivity';
import DesiredCreditActivity from './pages/DesiredCreditActivity';
import OnboardingActivity from './pages/OnboardingActivity';

export const { Stack, useFlow } = stackflow({
Expand All @@ -11,10 +13,18 @@ export const { Stack, useFlow } = stackflow({
basicUIPlugin({
theme: 'cupertino',
}),
historySyncPlugin({
routes: {
OnboardingActivity: '/',
CourseSelectionActivity: '/course-selection',
DesiredCreditActivity: '/desired-credit',
},
fallbackActivity: () => 'OnboardingActivity',
}),
],
activities: {
OnboardingActivity,
CourseSelectionActivity,
DesiredCreditActivity,
},
initialActivity: () => 'OnboardingActivity',
});