Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
kangju2000 committed Sep 1, 2024
1 parent 68da31e commit a55b8b6
Show file tree
Hide file tree
Showing 23 changed files with 404 additions and 27 deletions.
8 changes: 2 additions & 6 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module.exports = {
'prettier/prettier': ['error', { endOfLine: 'auto' }],
'no-unused-vars': 'off',
'no-empty-pattern': 'off',
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
Expand All @@ -34,12 +35,7 @@ module.exports = {
'warn',
{
'newlines-between': 'always',
groups: [
['builtin', 'external'],
['internal', 'parent', 'sibling', 'index'],
['object'],
['type'],
],
groups: [['builtin', 'external'], ['internal', 'parent', 'sibling', 'index'], ['object'], ['type']],
pathGroups: [
{
pattern: '@/**',
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"cheerio": "^1.0.0-rc.12",
"clsx": "^2.1.1",
"date-fns": "^2.30.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hotkeys-hook": "^4.4.1",
"react-shadow": "^20.4.0"
"react-shadow": "^20.4.0",
"tailwind-merge": "^2.5.2"
},
"devDependencies": {
"@chakra-ui/cli": "^2.4.1",
Expand Down Expand Up @@ -64,6 +66,6 @@
"packageManager": "pnpm@8.15.0",
"engines": {
"node": "20.x",
"pnpm": ">=8.15.0"
"pnpm": "8.15.0"
}
}
15 changes: 15 additions & 0 deletions pnpm-lock.yaml

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

43 changes: 43 additions & 0 deletions src/components/AnimatedRefreshButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// src/components/AnimatedRefreshButton.tsx
import { motion, useAnimation } from 'framer-motion'
import { useState } from 'react'

type AnimatedRefreshButtonProps = {
onClick: () => void
}

export function AnimatedRefreshButton({ onClick }: AnimatedRefreshButtonProps) {
const controls = useAnimation()
const [isAnimating, setIsAnimating] = useState(false)

const handleClick = async () => {
if (!isAnimating) {
setIsAnimating(true)
onClick()
await controls.start({
rotate: 360,
transition: { duration: 1, ease: 'linear' },
})
await controls.set({ rotate: 0 })
setIsAnimating(false)
}
}

return (
<motion.button onClick={handleClick} className="d-btn d-btn-circle d-btn-ghost d-btn-sm" whileTap={{ scale: 0.9 }}>
<motion.svg
xmlns="http://www.w3.org/2000/svg"
className="h-20px w-20px"
viewBox="0 0 20 20"
fill="currentColor"
animate={controls}
>
<path
fillRule="evenodd"
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
clipRule="evenodd"
/>
</motion.svg>
</motion.button>
)
}
16 changes: 16 additions & 0 deletions src/components/Content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useState } from 'react'

import { Navigation } from './Navigation'
import { SettingsContent } from './SettingsContent'
import { TaskContent } from './TaskContent'

export function Content() {
const [activeTab, setActiveTab] = useState<'tasks' | 'settings'>('tasks')

return (
<div className="flex h-full flex-col">
{activeTab === 'tasks' ? <TaskContent /> : <SettingsContent />}
<Navigation activeTab={activeTab} setActiveTab={setActiveTab} />
</div>
)
}
21 changes: 21 additions & 0 deletions src/components/LoadingSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { motion } from 'framer-motion'
type LoadingSkeletonProps = {
progress: number
}

export function LoadingSkeleton({ progress }: LoadingSkeletonProps) {
return (
<div className="space-y-16px">
<div className="mb-16px h-4px w-full rounded-full bg-gray-200">
<motion.div
className="h-4px rounded-full bg-blue-600"
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
/>
</div>
{[...Array(5)].map((_, index) => (
<div key={index} className="d-skeleton h-80px w-full"></div>
))}
</div>
)
}
56 changes: 56 additions & 0 deletions src/components/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { cn } from '@/utils/cn'

type NavigationProps = {
activeTab: 'tasks' | 'settings'
setActiveTab: (tab: 'tasks' | 'settings') => void
}

export function Navigation({ activeTab, setActiveTab }: NavigationProps) {
return (
<div className="flex justify-around border-t border-gray-200 bg-white bg-opacity-80">
<button
className={cn('flex flex-1 flex-col items-center justify-center px-16px py-12px', {
'text-blue-500': activeTab === 'tasks',
'text-gray-500': activeTab !== 'tasks',
})}
onClick={() => setActiveTab('tasks')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="mb-4px h-24px w-24px"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z" />
<path
fillRule="evenodd"
d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z"
clipRule="evenodd"
/>
</svg>
<span className="text-12px">과제</span>
</button>
<button
className={cn('flex flex-1 flex-col items-center justify-center px-16px py-12px', {
'text-blue-500': activeTab === 'settings',
'text-gray-500': activeTab !== 'settings',
})}
onClick={() => setActiveTab('settings')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="mb-4px h-24px w-24px"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z"
clipRule="evenodd"
/>
</svg>
<span className="text-12px">설정</span>
</button>
</div>
)
}
17 changes: 17 additions & 0 deletions src/components/SettingItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
type SettingItemProps = {
title: string
description: string
children: React.ReactNode
}

export function SettingItem({ title, description, children }: SettingItemProps) {
return (
<div className="flex items-center justify-between rounded-12px bg-white p-10px">
<div>
<h3 className="text-14px font-semibold text-gray-800">{title}</h3>
<p className="text-11px text-gray-600">{description}</p>
</div>
{children}
</div>
)
}
21 changes: 21 additions & 0 deletions src/components/SettingsContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { SettingItem } from './SettingItem'

export function SettingsContent() {
return (
<div className="flex-1 space-y-12px overflow-y-auto px-10px py-20px">
<SettingItem title="알림" description="과제 마감 알림 받기">
<input type="checkbox" className="d-toggle d-toggle-primary" />
</SettingItem>
<SettingItem title="자동 새로고침" description="10분마다 과제 목록 갱신">
<input type="checkbox" className="d-toggle d-toggle-primary" />
</SettingItem>
<SettingItem title="언어" description="앱 표시 언어 선택">
<select className="d-select d-select-bordered d-select-sm">
<option>한국어</option>
<option>English</option>
<option>日本語</option>
</select>
</SettingItem>
</div>
)
}
31 changes: 31 additions & 0 deletions src/components/TabNavigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { cn } from '@/utils/cn'

type TabNavigationProps = {
activeTab: 'ongoing' | 'all'
setActiveTab: (tab: 'ongoing' | 'all') => void
}

export function TabNavigation({ activeTab, setActiveTab }: TabNavigationProps) {
return (
<div className="flex space-x-8px">
<button
className={cn('rounded-full px-10px py-4px text-12px transition-colors', {
'bg-blue-500 text-white': activeTab === 'ongoing',
'bg-gray-200 text-gray-700': activeTab !== 'ongoing',
})}
onClick={() => setActiveTab('ongoing')}
>
진행중
</button>
<button
className={cn('rounded-full px-10px py-4px text-12px transition-colors', {
'bg-blue-500 text-white': activeTab === 'all',
'bg-gray-200 text-gray-700': activeTab !== 'all',
})}
onClick={() => setActiveTab('all')}
>
전체
</button>
</div>
)
}
40 changes: 40 additions & 0 deletions src/components/TaskCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { ActivityType } from '@/types'
import { cn } from '@/utils/cn'
import { calculateDday } from '@/utils/dateUtils'

type TaskCardProps = {
task: ActivityType
}

export function TaskCard({ task }: TaskCardProps) {
const dday = calculateDday(task.endAt, task.startAt)

return (
<div className="rounded-12px bg-white p-12px shadow-sm">
<div className="mb-8px flex items-center gap-8px">
<span
className={cn('rounded-8px px-6px py-4px text-11px', {
'bg-blue-100 text-blue-800': task.type === 'video',
'bg-green-100 text-green-800': task.type === 'assignment',
})}
>
{task.type === 'video' ? '영상' : '과제'}
</span>
<h3 className="flex-1 text-14px font-semibold text-gray-800">{task.title}</h3>
</div>
<p className="mb-4px text-12px text-gray-600">{task.courseTitle}</p>
<div className="flex items-center justify-between">
<span className="text-11px font-bold text-gray-500">{dday}</span>
<span
className={cn('rounded-8px px-6px py-4px text-11px', {
'bg-green-100 text-green-800': task.hasSubmitted,
'bg-yellow-100 text-yellow-800': !task.hasSubmitted && !task.startAt,
'bg-red-100 text-red-800': !task.hasSubmitted && task.startAt,
})}
>
{task.hasSubmitted ? '제출완료' : task.startAt !== '' ? '미제출' : '예정됨'}
</span>
</div>
</div>
)
}
49 changes: 49 additions & 0 deletions src/components/TaskContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { formatDistanceToNow } from 'date-fns'
import { ko } from 'date-fns/locale'
import { useState, useMemo } from 'react'

import { AnimatedRefreshButton } from './AnimatedRefreshButton'
import { LoadingSkeleton } from './LoadingSkeleton'
import { TabNavigation } from './TabNavigation'
import { TaskList } from './TaskList'
import { contentsData } from '@/data/dummyData'
import useGetContents from '@/hooks/useGetContents'

const isDevelopment = process.env.NODE_ENV === 'development'

export function TaskContent() {
const [taskTab, setTaskTab] = useState<'ongoing' | 'all'>('ongoing')
const { data, pos, isLoading, refetch } = useGetContents({ enabled: !isDevelopment })

const contentData = isDevelopment ? contentsData : data

const filteredTasks = useMemo(() => {
return contentData.activityList.filter(task => (taskTab === 'ongoing' ? !task.hasSubmitted : true))
}, [taskTab, contentData.activityList])

const formattedUpdateTime = formatDistanceToNow(new Date(contentData.updateAt), { addSuffix: true, locale: ko })

return (
<>
<div className="bg-white bg-opacity-50 px-16px py-12px">
<div className="mb-12px flex items-center justify-between">
<h2 className="text-16px font-bold">과제 목록</h2>
<div className="group relative">
<AnimatedRefreshButton onClick={refetch} />
<div className="absolute right-0 mt-4px whitespace-nowrap rounded-2px bg-gray-800 px-6px py-2px text-10px text-white opacity-0 shadow-lg transition-opacity duration-300 group-hover:opacity-100">
{formattedUpdateTime} 갱신됨
</div>
</div>
</div>
<TabNavigation activeTab={taskTab} setActiveTab={setTaskTab} />
</div>
<div className="relative flex-1 overflow-hidden">
<div className="absolute inset-x-0 top-0 z-10 h-16px bg-gradient-to-b from-slate-100 to-transparent"></div>
<div className="absolute inset-x-0 bottom-0 z-10 h-16px bg-gradient-to-t from-slate-100 to-transparent"></div>
<div className="h-full overflow-y-auto px-16px py-20px">
{isLoading ? <LoadingSkeleton progress={pos} /> : <TaskList tasks={filteredTasks} />}
</div>
</div>
</>
)
}
Loading

0 comments on commit a55b8b6

Please sign in to comment.