Time tracking dashboard

Leo
Leo
Cover Image for Time tracking dashboard
  1. 需求
  2. 实现步骤
  3. 总结
  4. 组件
  5. 状态
  6. 响应式布局
  7. map函数:
  8. 条件渲染:

今天我们来完成一个 React 的基础练习,参考的是frontendmentor 的设计文档和文字描述。

需求

这个练习要求实现一个时间跟踪的仪表盘,用户可以选择查看每日、每周或每月的数据。并且要适应不同屏幕尺寸下的布局。

实现步骤

我们将Dashboard分成三个组件:AccountInfo、TypeGrid和TimeGrid。AccountInfo组件显示用户的头像和名称。TypeGrid组件显示可用的时间跟踪类型,并允许用户选择一个类型。TimeGrid 组件显示所选类型的时间跟踪数据。

由于代码比较简单就不再逐一说明

总结

这个练习主要使用了以下技术:

组件

在 React 中,组件是构建用户界面的基本单元。一个组件是一个可复用的模块,它有自己的状态和行为。React 组件有两种类型:类组件和函数式组件。在本次练习中,所有组件都是函数式组件。

状态

React 组件的状态是组件的内部数据。状态管理是 React 的核心概念之一。在这个代码示例中,DashBoard 组件的状态被定义为 type,它表示当前显示的数据类型。useState 是 React 自带的一个钩子函数,用于创建组件的状态。使用 useState 创建的状态是可变的,可以在组件中动态更新。

响应式布局

响应式布局是一种布局方式,可以适应不同屏幕大小。在这个代码示例中,我们使用了响应式布局来适应不同大小的屏幕。在本次练习中,我们使用了 md 和其他类名前缀来区分不同大小的屏幕。

map函数:

在JavaScript中,map()是一个常用的方法,用于迭代数组,并返回新的数组。在本次练习中,通过map()方法迭代typeList和trackList数组,分别生成类型按钮和时间卡片。

条件渲染:

React中可以使用条件语句来决定渲染什么。在这个例子中,在TypeGrid组件中,根据当前的curType属性来确定是否为选中状态。

最后附上控件的渲染效果以及完整源码。你可以通过改变浏览器宽度来查看控件在不同宽度屏幕上的展示效果,也可以点击 数据类型来切换数据。

Repart for
Jeremy Roboson
Daily
Weekly
Monthly
Work
...
32hrs
last week - 36hrs
Play
...
10hrs
last week - 8hrs
Study
...
4hrs
last week - 7hrs
Exercise
...
4hrs
last week - 5hrs
Social
...
5hrs
last week - 10hrs
Self Care
...
2hrs
last week - 2hrs
time-tracking-dashboard.jsx
Copy

import { useState } from 'react';
import ellipsisIcon from './icons/icon-ellipsis.svg';
import exerciseIcon from './icons/icon-exercise.svg';
import playIcon from './icons/icon-play.svg';
import selfCareIcon from './icons/icon-self-care.svg';
import socialIcon from './icons/icon-social.svg';
import studyIcon from './icons/icon-study.svg';
import workIcon from './icons/icon-work.svg';
import avatar from './icons/image-jeremy.png';
const timeData = [
{
title: 'Work',
timeframes: {
daily: {
current: 5,
previous: 7,
},
weekly: {
current: 32,
previous: 36,
},
monthly: {
current: 103,
previous: 128,
},
},
},
{
title: 'Play',
timeframes: {
daily: {
current: 1,
previous: 2,
},
weekly: {
current: 10,
previous: 8,
},
monthly: {
current: 23,
previous: 29,
},
},
},
{
title: 'Study',
timeframes: {
daily: {
current: 0,
previous: 1,
},
weekly: {
current: 4,
previous: 7,
},
monthly: {
current: 13,
previous: 19,
},
},
},
{
title: 'Exercise',
timeframes: {
daily: {
current: 1,
previous: 1,
},
weekly: {
current: 4,
previous: 5,
},
monthly: {
current: 11,
previous: 18,
},
},
},
{
title: 'Social',
timeframes: {
daily: {
current: 1,
previous: 3,
},
weekly: {
current: 5,
previous: 10,
},
monthly: {
current: 21,
previous: 23,
},
},
},
{
title: 'Self Care',
timeframes: {
daily: {
current: 0,
previous: 1,
},
weekly: {
current: 2,
previous: 2,
},
monthly: {
current: 7,
previous: 11,
},
},
},
];
let icons = {
Ellipsis: ellipsisIcon,
Exercise: exerciseIcon,
Play: playIcon,
'Self Care': selfCareIcon,
Social: socialIcon,
Study: studyIcon,
Work: workIcon,
};
let bgColors = {
Ellipsis: 'bg-sky-400',
Exercise: 'bg-green-400',
Play: 'bg-blue-400',
'Self Care': 'bg-yellow-300',
Social: 'bg-violet-300',
Study: 'bg-red-300',
Work: 'bg-orange-300',
};
const dataType = ['Daily', 'Weekly', 'Monthly'];
function AccountInfo() {
return (
<div className="flex md:flex-col flex-row w-full bg-[#4b3fe1] px-4 pt-4 pb-4 md:pb-16 rounded-xl">
<img
src={avatar.src}
className="mr-2 rounded-full border-white border-2"
width={50}
height={50}
/>
<div>
<div className="md:mt-4 font-thin text-sm text-slate-300">
Repart for
</div>
<div className="font-thin text-lg md:text-3xl text-white">
Jeremy Roboson
</div>
</div>
</div>
);
}
function TypeGrid({ curType, typeList, handleClick }) {
return (
<div className="grid grid-cols-3 md:grid-cols-1 gap-1 md:mt-4 mt-2 ml-4 md:pb-4 pb-2">
{typeList.map((type) => (
<div
className={`text-sm font-thin ${
type === curType ? 'text-white' : 'text-slate-500'
}`}
onClick={() => handleClick(type)}
>
{type}
</div>
))}
</div>
);
}
function TimeCard({ title, timeFrames, type }) {
let lastWord = 'day';
if (type === 'Weekly') {
lastWord = 'week';
} else if (type === 'Monthly') {
lastWord = 'month';
}
const currenTime = timeFrames[type.toLowerCase()].current;
const previousTime = timeFrames[type.toLowerCase()].previous;
return (
<div
className={`flex relative w-[240px] h-[100px] md:w-[150px] md:h-[160px] ${bgColors[title]} rounded-lg overflow-hidden`}
>
<img
src={icons[title].src}
className="mr-2 absolute z-10 right-2"
width={40}
height={20}
/>
<div className="absolute w-full top-6 bottom-0 bg-[#1c1f4a] font-thin text-slate-200 rounded-t-lg z-20">
<div className="w-full px-4 py-4">
<div className="flex justify-between text-sm">
<div>{title}</div>
<div>...</div>
</div>
<div className="flex flex-row md:flex-col justify-between text-sm">
<div className="mt-2 font-thin text-xl md:text-3xl text-white">
{currenTime}hrs
</div>
<div className="mt-3 font-thin text-sm">
last {lastWord} - {previousTime}hrs
</div>
</div>
</div>
</div>
</div>
);
}
function TimeGrid({ type, trackList }) {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{trackList.map((track) => (
<TimeCard
key={track.title}
title={track.title}
timeFrames={track.timeframes}
type={type}
/>
))}
</div>
);
}
export default function DashBoard() {
const [type, setType] = useState('Weekly');
return (
<div className="flex w-full bg-[#0e131f] px-4 p-16">
<div className="mx-auto flex flex-col md:flex-row md:shadow-xl md:rounded-xl">
<div className="flex flex-col w-[240px] md:w-[150px] bg-[#1c1f4a] rounded-xl text-white">
<AccountInfo />
<TypeGrid
curType={type}
typeList={dataType}
handleClick={(type) => setType(type)}
/>
</div>
<div className="mt-4 md:ml-4 md:mt-0">
<TimeGrid type={type} trackList={timeData} />
</div>
</div>
</div>
);
}