Skip to content
Open
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
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,77 @@ The app will now be running at [http://localhost:5173/](http://localhost:5173/).
## Credits

Made with ❤️ by [maciekt07](https://github.com/maciekt07), licensed under [MIT](https://github.com/maciekt07/TodoApp/blob/main/LICENSE).

# 📋 Scaling and Expanding the Todo App

Based on the current codebase, here are some ways to develop and scale the Todo application when it reaches real-world usage.

---

## 🚀 When Scaling Up

### 🖥 Move to a Real Backend

- Currently, the app stores data in `localStorage`.
- Build a backend API using databases like **MongoDB** or **PostgreSQL**.
- Add user authentication (e.g., **OAuth**, **JWT**).

### ⚡️ Improve Performance

- Optimize bundle size with **code splitting**.
- Use **lazy loading** for rarely used components.
- Enhance caching strategies using **Workbox** or **service workers**.

### 🤝 Enable Collaboration

- Allow users to **share task lists** with others.
- Support **real-time task editing**.
- Send **notifications** on updates.

---

## 📉 When Scaling Down

### 📦 Reduce App Size

- Remove unused features.
- Optimize splash screens and images.
- Minimize dependencies on external libraries.

### 📱 Optimize for Older Devices

- Reduce heavy animations.
- Simplify the UI for performance.
- Improve initial load time.

---

## ✨ Add New Features

### 📆 Calendar and Reminders

- Integrate with **Google Calendar** or **Apple Calendar**.
- Add **push notifications** for upcoming tasks.
- Support **recurring tasks**.

### 📊 Analytics and Reports

- Track task completion statistics.
- Show **productivity charts** over time.
- Offer habit improvement suggestions.

### 🎨 Advanced Customization

- Add theming and interface personalization.
- Allow **custom layouts and views**.
- Integrate with other productivity tools (e.g., Notion, Slack).

---

## 💰 Business Model Ideas

- Launch a **Pro version** with premium features.
- Integrate **payment systems** like Stripe or PayPal.
- Offer a **freemium model** with task limits.

---
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.0.2",
"@mui/material": "^7.0.2",
"@mui/x-date-pickers": "^8.0.0",
"date-fns": "^4.1.0",
"emoji-picker-react": "^4.12.2",
"lz-string": "^1.5.0",
"ntc-ts": "^0.0.8",
Expand Down
57 changes: 57 additions & 0 deletions src/components/PrioritySelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { FormControl, FormLabel, Select, SelectChangeEvent, styled, MenuItem } from "@mui/material";
import { CSSProperties } from "react";
import { TaskPriority } from "../types/user";
import { ColorPalette } from "../theme/themeConfig";

interface Props {
width?: CSSProperties["width"];
onPriorityChange: (priority: TaskPriority) => void;
selectedPriority: TaskPriority;
fontColor?: CSSProperties["color"];
}

export const PrioritySelect: React.FC<Props> = ({
width,
onPriorityChange,
selectedPriority,
fontColor,
}) => {
const handlePriorityChange = (event: SelectChangeEvent<unknown>) => {
onPriorityChange(event.target.value as TaskPriority);
};

const priorityOptions: TaskPriority[] = ["Low", "Medium", "High"];

return (
<FormControl sx={{ width: width || "100%" }}>
<FormLabel
sx={{
color: fontColor ? `${fontColor}e8` : `${ColorPalette.fontLight}e8`,
marginLeft: "8px",
fontWeight: 500,
}}
>
Priority
</FormLabel>

<StyledSelect width={width} value={selectedPriority} onChange={handlePriorityChange}>
{priorityOptions.map((priority) => (
<MenuItem key={priority} value={priority}>
{priority}
</MenuItem>
))}
</StyledSelect>
</FormControl>
);
};

const StyledSelect = styled(Select)<{ width?: CSSProperties["width"] }>`
margin: 12px 0;
border-radius: 16px !important;
transition: 0.3s all;
width: ${({ width }) => width || "100%"};

/* background: #ffffff18; */
z-index: 999;
border: none !important;
`;
106 changes: 105 additions & 1 deletion src/components/tasks/EditTask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import { ColorPicker, CustomDialogTitle, CustomEmojiPicker } from "..";
import { DESCRIPTION_MAX_LENGTH, TASK_NAME_MAX_LENGTH } from "../../constants";
import { UserContext } from "../../contexts/UserContext";
import { DialogBtn } from "../../styles";
import { Category, Task } from "../../types/user";
import { Category, Task, TaskPriority } from "../../types/user";
import { showToast } from "../../utils";
import { useTheme } from "@emotion/react";
import { ColorPalette } from "../../theme/themeConfig";
import { CategorySelect } from "../CategorySelect";
import { PrioritySelect } from "../PrioritySelect";

interface EditTaskProps {
open: boolean;
Expand All @@ -33,6 +34,7 @@ export const EditTask = ({ open, task, onClose }: EditTaskProps) => {
const [editedTask, setEditedTask] = useState<Task | undefined>(task);
const [emoji, setEmoji] = useState<string | null>(null);
const [selectedCategories, setSelectedCategories] = useState<Category[]>([]);
const [selectedPriority, setSelectedPriority] = useState<TaskPriority>("Low");

const theme = useTheme();

Expand All @@ -58,11 +60,13 @@ export const EditTask = ({ open, task, onClose }: EditTaskProps) => {
useEffect(() => {
setEditedTask(task);
setSelectedCategories(task?.category as Category[]);
setSelectedPriority(task?.priority as TaskPriority);
}, [task]);

// Event handler for input changes in the form fields.
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
console.log(name, value);

// Update the editedTask state with the changed value.
setEditedTask((prevTask) => ({
Expand All @@ -85,6 +89,9 @@ export const EditTask = ({ open, task, onClose }: EditTaskProps) => {
deadline: editedTask.deadline || undefined,
category: editedTask.category || undefined,
lastSave: new Date(),
priority: editedTask.priority,
startDate: editedTask.startDate || undefined,
endDate: editedTask.endDate || undefined,
};
}
return task;
Expand Down Expand Up @@ -115,6 +122,13 @@ export const EditTask = ({ open, task, onClose }: EditTaskProps) => {
}));
}, [selectedCategories]);

useEffect(() => {
setEditedTask((prevTask) => ({
...(prevTask as Task),
priority: selectedPriority,
}));
}, [selectedPriority]);

useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (JSON.stringify(editedTask) !== JSON.stringify(task) && open) {
Expand Down Expand Up @@ -242,13 +256,103 @@ export const EditTask = ({ open, task, onClose }: EditTaskProps) => {
}}
/>

<StyledInput
label="Start Date"
name="startDate"
type="datetime-local"
value={
editedTask?.startDate
? new Date(editedTask.startDate).toLocaleString("sv").replace(" ", "T").slice(0, 16)
: ""
}
onChange={handleInputChange}
slotProps={{
inputLabel: {
shrink: true,
},
input: {
startAdornment: editedTask?.startDate ? (
<InputAdornment position="start">
<Tooltip title="Clear">
<IconButton
color="error"
onClick={() => {
setEditedTask((prevTask) => ({
...(prevTask as Task),
startDate: undefined,
}));
}}
>
<CancelRounded />
</IconButton>
</Tooltip>
</InputAdornment>
) : undefined,
},
}}
sx={{
colorScheme: theme.darkmode ? "dark" : "light",
" & .MuiInputBase-root": {
transition: ".3s all",
},
}}
/>

<StyledInput
label="End Date"
name="endDate"
type="datetime-local"
value={
editedTask?.endDate
? new Date(editedTask.endDate).toLocaleString("sv").replace(" ", "T").slice(0, 16)
: ""
}
onChange={handleInputChange}
slotProps={{
inputLabel: {
shrink: true,
},
input: {
startAdornment: editedTask?.endDate ? (
<InputAdornment position="start">
<Tooltip title="Clear">
<IconButton
color="error"
onClick={() => {
setEditedTask((prevTask) => ({
...(prevTask as Task),
endDate: undefined,
}));
}}
>
<CancelRounded />
</IconButton>
</Tooltip>
</InputAdornment>
) : undefined,
},
}}
sx={{
colorScheme: theme.darkmode ? "dark" : "light",
" & .MuiInputBase-root": {
transition: ".3s all",
},
}}
/>

{settings.enableCategories !== undefined && settings.enableCategories && (
<CategorySelect
fontColor={theme.darkmode ? ColorPalette.fontLight : ColorPalette.fontDark}
selectedCategories={selectedCategories}
onCategoryChange={(categories) => setSelectedCategories(categories)}
/>
)}

<PrioritySelect
selectedPriority={selectedPriority}
onPriorityChange={setSelectedPriority}
fontColor={theme.darkmode ? ColorPalette.fontLight : ColorPalette.fontDark}
/>
<div
style={{
display: "flex",
Expand Down
49 changes: 48 additions & 1 deletion src/components/tasks/TaskItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { memo, useContext } from "react";
import { Emoji } from "emoji-picker-react";
import { DoneRounded, PushPinRounded, Link } from "@mui/icons-material";
import { DoneRounded, PushPinRounded, Link, FlagRounded } from "@mui/icons-material";
import { Tooltip } from "@mui/material";
import type { Task, UUID } from "../../types/user";
import {
Expand All @@ -19,6 +19,7 @@ import {
RadioUnchecked,
TaskCategoriesContainer,
SharedByContainer,
PriorityContainer,
} from "./tasks.styled";
import { calculateDateDifference, formatDate, getFontColor, systemInfo } from "../../utils";
import { RenderTaskDescription } from "./RenderTaskDescription";
Expand Down Expand Up @@ -169,6 +170,52 @@ export const TaskItem = memo(
</Tooltip>
)}

{task.startDate && (
<Tooltip
title={new Intl.DateTimeFormat(navigator.language, {
dateStyle: "full",
timeStyle: "medium",
}).format(new Date(task.startDate))}
placement="bottom-start"
>
<TimeLeft done={task.done} translate="yes">
<RingAlarm
fontSize="small"
sx={{
color: `${getFontColor(task.color)} !important`,
}}
/>{" "}
&nbsp; Start: {new Date(task.startDate).toLocaleDateString()} {" • "}
{new Date(task.startDate).toLocaleTimeString()}
</TimeLeft>
</Tooltip>
)}

{task.endDate && (
<Tooltip
title={new Intl.DateTimeFormat(navigator.language, {
dateStyle: "full",
timeStyle: "medium",
}).format(new Date(task.endDate))}
placement="bottom-start"
>
<TimeLeft done={task.done} translate="yes">
<RingAlarm
fontSize="small"
sx={{
color: `${getFontColor(task.color)} !important`,
}}
/>{" "}
&nbsp; End: {new Date(task.endDate).toLocaleDateString()} {" • "}
{new Date(task.endDate).toLocaleTimeString()}
</TimeLeft>
</Tooltip>
)}

<PriorityContainer translate="yes">
<FlagRounded fontSize="small" /> &nbsp; Priority: {task.priority ?? "None"}
</PriorityContainer>

{task.sharedBy && (
<SharedByContainer translate="yes">
<Link /> Shared by{" "}
Expand Down
Loading