Skip to content

Commit 52782e2

Browse files
committedOct 19, 2024
add filter, enhanced fetching, enhanced mobile experience
1 parent fc260cf commit 52782e2

File tree

9 files changed

+264
-61
lines changed

9 files changed

+264
-61
lines changed
 

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"qrcode": "^1.5.4",
2323
"react": "^18",
2424
"react-dom": "^18",
25+
"react-select": "^5.8.1",
2526
"sweetalert2": "^11.14.2",
2627
"sweetalert2-react-content": "^5.0.7"
2728
},

‎src/components/Header.tsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,27 @@ import Search from './todo/popup/Search';
44
import AddTodoPopup from './todo/popup/AddTodo';
55
import { useRouter } from 'next/router';
66
import ProfilePopup from './todo/popup/ProfilePopup';
7+
import Filter from './todo/popup/Filter';
78

8-
const Header = () => {
9+
interface HeaderProps {
10+
setFilterStatus: (status: string) => void;
11+
filterStatus: string;
12+
}
13+
14+
const Header = ({ setFilterStatus, filterStatus }: HeaderProps) => {
915
const [showDropdown, setShowDropdown] = useState(false);
1016
const [showSearch, setShowSearch] = useState(false);
1117
const [showAddTodo, setShowAddTodo] = useState(false);
1218
const [showProfile, setShowProfile] = useState(false);
19+
const [showFilter, setShowFilter] = useState(false);
1320
const router = useRouter();
1421
const currentLocation = router.pathname;
1522

1623
const toggleDropdown = () => setShowDropdown(!showDropdown);
1724
const toggleSearch = () => setShowSearch(!showSearch);
1825
const toggleAddTodo = () => setShowAddTodo(!showAddTodo);
1926
const toggleProfile = () => setShowProfile(!showProfile);
27+
const toggleFilter = () => setShowFilter(!showFilter);
2028

2129
return (
2230
<div className="h-16 w-full dark:bg-[#1a1a1a] bg-slate-100 rounded-lg items-center flex p-4 sticky top-0 z-50">
@@ -28,6 +36,7 @@ const Header = () => {
2836
<div className="flex items-center">
2937
<Icon icon="mi:add" className="ml-3 lg:hidden text-2xl cursor-pointer" onClick={toggleAddTodo} />
3038
<Icon icon="material-symbols:search" className="ml-3 text-2xl cursor-pointer" onClick={toggleSearch} />
39+
<Icon icon={!filterStatus ? 'heroicons-outline:filter' : 'heroicons-solid:filter'} className="ml-3 text-2xl font-black cursor-pointer stroke-2" onClick={toggleFilter} />
3140
</div>
3241
)}
3342
<Icon icon="solar:user-bold" className="ml-3 text-2xl cursor-pointer" onClick={toggleDropdown} />
@@ -40,9 +49,11 @@ const Header = () => {
4049
</ul>
4150
</div>
4251
)}
52+
4353
<Search show={showSearch} onClose={toggleSearch} />
4454
<AddTodoPopup show={showAddTodo} onClose={toggleAddTodo} />
4555
<ProfilePopup show={showProfile} onClose={toggleProfile} />
56+
<Filter show={showFilter} onClose={toggleFilter} setFilterStatus={setFilterStatus} filterStatus={filterStatus} />
4657
</div>
4758
);
4859
};

‎src/components/calendar/Calendar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const Calendar: React.FC<CalendarProps> = ({ todoData, onDateClick }) => {
5555
};
5656

5757
return (
58-
<div className={`p-4 h-1/2 rounded-3xl shadow-lg w-full ${pinkMode ? 'bg-pink-200' : 'bg-blue-200'}`}>
58+
<div className={`p-4 h-full rounded-3xl shadow-lg w-full ${pinkMode ? 'bg-pink-200' : 'bg-blue-200'}`}>
5959
<div className="flex justify-between items-center mb-4">
6060
<h2
6161
className="text-xl font-semibold text-indigo-800 cursor-pointer"

‎src/components/todo/ActivityDesktopTodo.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const ActivityDesktopTodo: React.FC<ActivityDesktopTodoProps> = ({ todoData, sel
4646
});
4747

4848
return (
49-
<div className="max-h-[95vh] md:min-h-[96vh] xl:min-h-[90.5vh] flex flex-col w-full max-w-md rounded-xl bg-slate-50 dark:bg-gray-800 p-6 overflow-hidden">
49+
<div className="max-h-[45vh] h-[35dvh] min-h-[35dvh] lg:min-h-full flex flex-col w-full max-w-lg rounded-xl bg-slate-50 dark:bg-gray-800 p-6 overflow-hidden">
5050
<div className="flex items-center justify-between mb-4">
5151
<h1 className="text-2xl font-poppins font-bold">To-do</h1>
5252
<p className="text-sm text-gray-500 dark:text-gray-400">bro.. you have {todoData.length} tasks</p>

‎src/components/todo/ActivityTodo.tsx

+38-8
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,9 @@ const ActivityTodo: React.FC<ActivityTodoProps> = ({ todoData, selectedDate }) =
5252
const endDate = parseISO(task.end);
5353

5454
const addToGroup = (date: Date, label: string) => {
55-
let key = label;
56-
if (!grouped[key]) grouped[key] = [];
57-
if (!grouped[key].some(t => t.id === task.id)) {
58-
grouped[key].push(task);
55+
if (!grouped[label]) grouped[label] = [];
56+
if (!grouped[label].some(t => t.id === task.id)) {
57+
grouped[label].push(task);
5958
}
6059
};
6160

@@ -69,15 +68,13 @@ const ActivityTodo: React.FC<ActivityTodoProps> = ({ todoData, selectedDate }) =
6968
) {
7069
addToGroup(currentDate, 'Today');
7170
}
72-
7371
if (
7472
(isBefore(startDate, startOfTomorrow) && isAfter(endDate, startOfTomorrow)) ||
7573
(isEqual(startDate, startOfTomorrow) || isEqual(endDate, startOfTomorrow)) ||
7674
(isAfter(startDate, startOfTomorrow) && isBefore(startDate, endOfTomorrow))
7775
) {
7876
addToGroup(addDays(currentDate, 1), 'Tomorrow');
7977
}
80-
8178
let currentCheckDate = isAfter(startDate, startOfToday) ? startDate : startOfToday;
8279
while (isBefore(currentCheckDate, endDate) || isEqual(currentCheckDate, endDate)) {
8380
const label = isToday(currentCheckDate)
@@ -93,13 +90,46 @@ const ActivityTodo: React.FC<ActivityTodoProps> = ({ todoData, selectedDate }) =
9390
}
9491
});
9592

96-
return grouped;
93+
const sortedKeys = Object.keys(grouped).sort((a, b) => {
94+
const order = ['Today', 'Tomorrow', 'Past'];
95+
const aIndex = order.indexOf(a);
96+
const bIndex = order.indexOf(b);
97+
98+
// sooooo, 1 is like go down, -1 go up (in the list). phm kan? xixixi (for ur future reference ig)
99+
const getStatusOrder = (key: string) => {
100+
const tasks = grouped[key];
101+
if (tasks.some(task => task.status === 'Belum Selesai')) return -1;
102+
if (tasks.some(task => task.status === 'Dalam Progress')) return -1;
103+
if (tasks.some(task => task.status === 'Selesai')) return 1;
104+
return 0;
105+
};
106+
107+
if (a === 'Past' && getStatusOrder(a) === -1) return -1;
108+
if (b === 'Past' && getStatusOrder(b) === -1) return 1;
109+
110+
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
111+
if (aIndex !== -1) return -1;
112+
if (bIndex !== -1) return 1;
113+
114+
const statusOrderA = getStatusOrder(a);
115+
const statusOrderB = getStatusOrder(b);
116+
if (statusOrderA !== statusOrderB) return statusOrderA - statusOrderB;
117+
118+
return a.localeCompare(b);
119+
});
120+
121+
const sortedGrouped: { [key: string]: Task[] } = {};
122+
sortedKeys.forEach(key => {
123+
sortedGrouped[key] = grouped[key];
124+
});
125+
126+
return sortedGrouped;
97127
};
98128

99129
const groupedTasks = groupTasksByDate();
100130

101131
return (
102-
<div className="dark:bg-gray-900 bg-[#f6f6f6] text-white p-6 rounded-3xl shadow-lg min-w-full mx-auto h-1/2 min-h-[50vh] overflow-y-auto sigma-uwu-scrollbar">
132+
<div className="dark:bg-gray-900 bg-[#f6f6f6] text-white p-6 rounded-3xl shadow-lg min-w-full mx-auto min-h-[45dvh] max-h-[45dvh] lg:min-h-full overflow-y-auto sigma-uwu-scrollbar">
103133
<ViewTodo todo_id={todoId} show={showTodoDetailPopup} onClose={toggleTodoDetailPopup} />
104134
{Object.entries(groupedTasks).length === 0 ? (
105135
<div className="flex justify-center items-center h-full">

‎src/components/todo/WordsOfAffirmation.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const AffirmationDisplay = () => {
3434

3535
return (
3636
<motion.div
37-
className="h-72 min-h-1/2 md:h-1/2 flex items-center justify-center p-8 cursor-pointer rounded-lg shadow-lg"
37+
className="h-full min-h-[35dvh] lg:min-h-full md:h-1/2 flex items-center justify-center p-8 cursor-pointer rounded-lg shadow-lg"
3838
style={{
3939
background: `linear-gradient(135deg, ${gradientColors[0]}, ${gradientColors[1]})`,
4040
}}

‎src/components/todo/popup/Filter.tsx

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import React, { useState } from 'react';
2+
import Select from 'react-select';
3+
4+
interface FilterProps {
5+
setFilterStatus: (status: string) => void;
6+
filterStatus: string;
7+
show: boolean;
8+
onClose: () => void;
9+
}
10+
11+
interface Option {
12+
value: string;
13+
label: string;
14+
}
15+
16+
const Filter: React.FC<FilterProps> = ({ setFilterStatus, filterStatus, show, onClose }) => {
17+
const [selectedOption, setSelectedOption] = useState<Option | null>(
18+
filterStatus ? { value: filterStatus, label: filterStatus } : null
19+
);
20+
21+
if (!show) return null;
22+
23+
const options: Option[] = [
24+
{ value: 'Selesai', label: 'Selesai' },
25+
{ value: 'Belum Selesai', label: 'Belum Selesai' },
26+
{ value: 'Dalam Progress', label: 'Dalam Progress' }
27+
];
28+
29+
const handleChange = (selected: Option | null) => {
30+
setSelectedOption(selected);
31+
setFilterStatus(selected ? selected.value : '');
32+
};
33+
34+
const handleClear = () => {
35+
setSelectedOption(null);
36+
setFilterStatus('');
37+
};
38+
39+
return (
40+
<div className="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
41+
<div className="fixed inset-0 bg-black opacity-50" onClick={onClose}></div>
42+
<div className="flex items-center justify-center min-h-screen p-4">
43+
<div className="bg-white dark:bg-gray-900 w-full max-w-sm rounded-lg shadow-xl overflow-hidden relative z-10" style={{ minHeight: '35dvh', height: '35dvh' }}>
44+
<div className="p-4 h-full flex flex-col justify-between">
45+
<div>
46+
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">Filter to-do</h3>
47+
<Select
48+
options={options}
49+
value={selectedOption}
50+
onChange={handleChange}
51+
placeholder="Select status"
52+
className="w-full mb-4"
53+
classNamePrefix="react-select"
54+
theme={(theme) => ({
55+
...theme,
56+
colors: {
57+
...theme.colors,
58+
primary: '#3b82f6',
59+
primary75: '#60a5fa',
60+
primary50: '#93c5fd',
61+
primary25: '#bfdbfe',
62+
neutral0: 'var(--color-bg)',
63+
neutral80: 'var(--color-text)',
64+
neutral20: 'var(--color-border)',
65+
},
66+
})}
67+
styles={{
68+
control: (provided, state) => ({
69+
...provided,
70+
backgroundColor: 'var(--color-bg)',
71+
borderColor: state.isFocused ? '#3b82f6' : 'var(--color-border)',
72+
'&:hover': {
73+
borderColor: '#3b82f6',
74+
},
75+
color: 'var(--color-text)',
76+
}),
77+
option: (provided, state) => ({
78+
...provided,
79+
backgroundColor: state.isSelected
80+
? '#3b82f6'
81+
: state.isFocused
82+
? '#bfdbfe'
83+
: 'var(--color-bg)',
84+
color: state.isSelected ? 'white' : 'var(--color-text)',
85+
}),
86+
singleValue: (provided) => ({
87+
...provided,
88+
color: 'var(--color-text)',
89+
}),
90+
menu: (provided) => ({
91+
...provided,
92+
backgroundColor: 'var(--color-bg)',
93+
}),
94+
}}
95+
/>
96+
{selectedOption && (
97+
<button
98+
onClick={handleClear}
99+
className="text-sm text-blue-600 hover:text-blue-800 mb-4 block"
100+
>
101+
Clear selection
102+
</button>
103+
)}
104+
</div>
105+
<div className="mt-auto">
106+
<button
107+
type="button"
108+
className="w-full px-4 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
109+
onClick={onClose}
110+
>
111+
Close
112+
</button>
113+
</div>
114+
</div>
115+
</div>
116+
</div>
117+
</div>
118+
);
119+
};
120+
121+
export default Filter;

‎src/pages/api/todo/index.ts

+19-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { useLocalStorage } from "@/hooks/useLocalStorage";
22
import pgdb from "@/services/db";
3+
import { param } from "framer-motion/client";
34
import { NextApiRequest, NextApiResponse } from "next";
45

56
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
67
if (req.method === 'GET') {
78
const sessionId = req.headers['session-id'] as string;
89
const date = req.query.date as string;
10+
const status = req.query.status as string;
11+
12+
let params = [];
913

1014
if (!sessionId) {
1115
return res.status(401).json({ message: 'Unauthorized' });
@@ -16,14 +20,24 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
1620
try {
1721
let sql;
1822
sql = 'SELECT * FROM todo WHERE session_id = (SELECT session_id FROM session WHERE session = $1)';
19-
23+
params.push(sessionId);
2024
if (date) {
21-
sql += ' AND DATE(start) <= $2 AND DATE("end") >= $2';
25+
params.push(date);
26+
sql += ' AND DATE(start) <= $' + params.length + ' AND DATE("end") >= $' + params.length;
27+
}
28+
29+
if (status) {
30+
31+
const validStatuses = ['Selesai', 'Belum Selesai', 'Dalam Progress'];
32+
if (!validStatuses.includes(status)) {
33+
return res.status(400).json({ message: 'Invalid status' });
34+
}
35+
params.push(status);
36+
37+
sql += ' AND status = $' + params.length;
2238
}
2339

24-
const result = date
25-
? await client.query(sql, [sessionId, date])
26-
: await client.query(sql, [sessionId]);
40+
const result = await client.query(sql, params);
2741

2842
return res.status(200).json(result.rows);
2943
} finally {

‎src/pages/index.tsx

+70-44
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import React, { useEffect, useMemo, useState } from 'react';
2-
import TodoList from '@/components/todo/TodoList';
32
import Header from '@/components/Header';
43
import Calendar from '@/components/calendar/Calendar';
54
import ActivityTodo from '@/components/todo/ActivityTodo';
65
import { useLocalStorage } from '@/hooks/useLocalStorage';
7-
import { data } from 'framer-motion/client';
86
import useFetch from '@/hooks/useFetch';
97
import ActivityDesktopTodo from '@/components/todo/ActivityDesktopTodo';
108
import { motion } from 'framer-motion';
@@ -13,10 +11,22 @@ import AffirmationDisplay from '@/components/todo/WordsOfAffirmation';
1311
import { useRouter } from 'next/router';
1412
import Swal from 'sweetalert2';
1513

14+
// ok, for reference future me, this is to delay the funct call to avoid multiple crazy fetches and to save on network usage (i think? it works, so uh dont touch pls)
15+
function debounce(fn: Function, delay: number) {
16+
let timeoutId: NodeJS.Timeout;
17+
return (...args: any[]) => {
18+
if (timeoutId) {
19+
clearTimeout(timeoutId);
20+
}
21+
timeoutId = setTimeout(() => fn(...args), delay);
22+
};
23+
}
24+
1625
const IndexPage: React.FC = () => {
1726
const [sessionToken, setSessionToken] = useLocalStorage('session_token', '');
1827
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
1928
const [todoData, setTodoData] = useState<any[]>([]);
29+
const [filterStatus, setFilterStatus] = useState<string>('');
2030
const router = useRouter();
2131
const importId = router.query.import as string;
2232
const connectId = router.query.connectcode as string;
@@ -25,6 +35,8 @@ const IndexPage: React.FC = () => {
2535
'session-id': sessionToken,
2636
}), [sessionToken]);
2737

38+
const { data, loading, error, refetch } = useFetch(`/api/todo?status=${filterStatus}`, headers);
39+
2840
useEffect(() => {
2941
if (connectId) {
3042
const fetchData = async () => {
@@ -71,20 +83,31 @@ const IndexPage: React.FC = () => {
7183
}
7284
}, [connectId]);
7385

74-
const { data, loading, error } = useFetch('/api/todo/', headers);
86+
const debouncedSetFilterStatus = debounce((status: string) => {
87+
refetch(`/api/todo?status=${status}`, headers);
88+
}, 300);
89+
90+
const handleSetFilterStatus = (status: string) => {
91+
setFilterStatus(status);
92+
debouncedSetFilterStatus(status);
93+
};
7594

7695
const handleCalendarDateClick = async (date: Date) => {
7796
setSelectedDate(date);
7897

7998
let url = '/api/todo';
8099

100+
if (filterStatus) {
101+
url += `?status=${filterStatus}`;
102+
}
103+
81104
if (date.getTime() === 0) {
82105
setSelectedDate(null);
83106
} else {
84107
const nextDay = new Date(date);
85108
nextDay.setDate(date.getDate() + 1);
86109
nextDay.setHours(0, 0, 0, 0);
87-
url += `?date=${nextDay.toISOString()}`;
110+
filterStatus ? url += `&date=${nextDay.toISOString()}` : url += `?date=${nextDay.toISOString()}`;
88111
}
89112

90113
try {
@@ -96,7 +119,6 @@ const IndexPage: React.FC = () => {
96119
}
97120
const todoItems = await response.json();
98121
setTodoData(todoItems);
99-
console.log('To-do data for selected date:', todoItems);
100122
} catch (fetchError) {
101123
console.error('Error fetching to-do data:', fetchError);
102124
}
@@ -111,6 +133,8 @@ const IndexPage: React.FC = () => {
111133
if (data) {
112134
setTodoData(data);
113135
console.log('Session is valid');
136+
} else {
137+
setTodoData([]);
114138
}
115139
}, [error, loading, data]);
116140

@@ -122,7 +146,7 @@ const IndexPage: React.FC = () => {
122146
});
123147

124148
if (response.ok) {
125-
Swal.fire({
149+
Swal.fire({
126150
title: 'Import successful',
127151
text: 'Your task have been imported successfully',
128152
icon: 'success',
@@ -131,11 +155,11 @@ const IndexPage: React.FC = () => {
131155
timer: 3000,
132156
timerProgressBar: true,
133157
showConfirmButton: false,
134-
});
158+
});
135159

136-
router.push('/');
160+
router.push('/');
137161

138-
setTodoData([...todoData, await response.json()]);
162+
setTodoData([...todoData, await response.json()]);
139163
} else {
140164
Swal.fire({
141165
title: 'Import failed',
@@ -156,46 +180,48 @@ const IndexPage: React.FC = () => {
156180

157181
return (
158182
<>
159-
{loading && (
160-
<div className='dark:bg-[#121212] dark:text-white bg-white text-black'>
161-
<div className='w-full h-screen flex justify-center items-center'>
162-
<div className='text-2xl font-bold'>Loading...</div>
163-
</div>
164-
</div>
165-
)}
166-
{!loading && (
167-
<div className='dark:bg-[#121212] dark:text-white bg-white text-black min-h-screen'>
168-
<Header />
169-
<div className='w-full lg:flex'>
170-
<div className='p-4 w-full lg:w-1/3 h-screen justify-center'>
171-
<div className='mb-4'>
172-
<motion.div
173-
initial={{ opacity: 0, y: -20 }}
174-
animate={{ opacity: 1, y: 0 }}
175-
transition={{ duration: 0.5 }}
176-
>
177-
<Calendar todoData={data} onDateClick={handleCalendarDateClick} />
178-
</motion.div>
179-
</div>
180-
<ActivityTodo todoData={todoData} selectedDate={selectedDate} />
183+
{loading && (
184+
<div className='dark:bg-[#121212] dark:text-white bg-white text-black'>
185+
<div className='w-full h-screen flex justify-center items-center'>
186+
<div className='text-2xl font-bold'>Loading...</div>
181187
</div>
182-
<div className='p-4 hidden w-full lg:w-1/3 lg:flex justify-center'>
183-
<div className='w-full h-full overflow-y-auto scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-gray-400 dark:scrollbar-thumb-gray-600'>
184-
<ActivityDesktopTodo todoData={todoData} selectedDate={selectedDate} />
185-
</div>
186-
</div>
187-
<div className='p-4 w-full lg:w-1/3 lg:flex justify-center'>
188-
<div className='w-full h-full overflow-y-auto overflow-x-hidden scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-gray-400 dark:scrollbar-thumb-gray-600'>
189-
<div className='hidden lg:flex'>
190-
<AddTodo />
188+
</div>
189+
)}
190+
{!loading && (
191+
<div className="dark:bg-[#121212] dark:text-white bg-white text-black min-h-screen flex flex-col">
192+
<Header setFilterStatus={handleSetFilterStatus} filterStatus={filterStatus} />
193+
<div className="flex-grow flex flex-col lg:flex-row overflow-hidden">
194+
<div className="flex flex-col lg:flex-row flex-grow overflow-y-auto">
195+
<div className="w-full lg:w-1/3 flex flex-col p-4">
196+
<motion.div
197+
initial={{ opacity: 0, y: -20 }}
198+
animate={{ opacity: 1, y: 0 }}
199+
transition={{ duration: 0.5 }}
200+
className="mb-4"
201+
>
202+
<Calendar todoData={data} onDateClick={handleCalendarDateClick} />
203+
</motion.div>
204+
<div className="flex-grow">
205+
<ActivityTodo todoData={todoData} selectedDate={selectedDate} />
206+
</div>
207+
</div>
208+
209+
<div className="hidden lg:flex w-full lg:w-1/3 p-4">
210+
<ActivityDesktopTodo todoData={todoData} selectedDate={selectedDate} />
211+
</div>
212+
213+
<div className="w-full lg:w-1/3 flex flex-col p-4">
214+
<div className="hidden lg:block mb-4">
215+
<AddTodo />
216+
</div>
217+
<div className="flex-grow">
218+
<AffirmationDisplay />
219+
</div>
191220
</div>
192-
<AffirmationDisplay />
193221
</div>
194222
</div>
195223
</div>
196-
</div>
197-
)
198-
}
224+
)}
199225
</>
200226
);
201227
};

0 commit comments

Comments
 (0)
Please sign in to comment.