From 898cacc263fb9d779c3586e262cf2796340888e6 Mon Sep 17 00:00:00 2001 From: tildetilde Date: Tue, 20 May 2025 11:29:06 +0200 Subject: [PATCH 01/22] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index caf6289..9634852 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,6 @@ "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^15.15.0", - "vite": "^6.2.0" + "vite": "^6.3.5" } } From 2a870bc7bf8c3bbfd402d509adc9292961fb4322 Mon Sep 17 00:00:00 2001 From: tildetilde Date: Tue, 20 May 2025 11:44:41 +0200 Subject: [PATCH 02/22] Create js-project-todo.code-workspace --- js-project-todo.code-workspace | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 js-project-todo.code-workspace diff --git a/js-project-todo.code-workspace b/js-project-todo.code-workspace new file mode 100644 index 0000000..40fe023 --- /dev/null +++ b/js-project-todo.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "." + } + ] +} \ No newline at end of file From 990d36d06e27f0f0648fc1fa11a08b6f0d6aaa9c Mon Sep 17 00:00:00 2001 From: tildetilde Date: Tue, 20 May 2025 12:59:56 +0200 Subject: [PATCH 03/22] Initial sprint --- README.md | 2 +- app/about/page.tsx | 5 + app/page.tsx | 5 + index.html | 11 +- package.json | 1 + src/App.jsx | 112 +++++++++++++++++++- src/components/AboutPage.jsx | 56 ++++++++++ src/components/EmptyState.jsx | 42 ++++++++ src/components/ThemeToggle.jsx | 34 ++++++ src/components/TodoForm.jsx | 151 +++++++++++++++++++++++++++ src/components/TodoItem.jsx | 185 +++++++++++++++++++++++++++++++++ src/components/TodoList.jsx | 57 ++++++++++ src/components/TodoStats.jsx | 54 ++++++++++ src/main.jsx | 13 +-- src/store.js | 98 +++++++++++++++++ src/theme.js | 35 +++++++ vite.config.js | 8 +- 17 files changed, 848 insertions(+), 21 deletions(-) create mode 100644 app/about/page.tsx create mode 100644 app/page.tsx create mode 100644 src/components/AboutPage.jsx create mode 100644 src/components/EmptyState.jsx create mode 100644 src/components/ThemeToggle.jsx create mode 100644 src/components/TodoForm.jsx create mode 100644 src/components/TodoItem.jsx create mode 100644 src/components/TodoList.jsx create mode 100644 src/components/TodoStats.jsx create mode 100644 src/store.js create mode 100644 src/theme.js diff --git a/README.md b/README.md index d1c68b5..2ebcad7 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# Todo \ No newline at end of file +# Inktick – A minimalistic, modern todo app with a notebook feel diff --git a/app/about/page.tsx b/app/about/page.tsx new file mode 100644 index 0000000..e800226 --- /dev/null +++ b/app/about/page.tsx @@ -0,0 +1,5 @@ +import AboutPage from "../../src/components/AboutPage.jsx"; + +export default function About() { + return ; +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..afa72a5 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,5 @@ +import App from "../src/App"; + +export default function Page() { + return ; +} diff --git a/index.html b/index.html index f7ac4e4..da43dd5 100644 --- a/index.html +++ b/index.html @@ -4,13 +4,14 @@ - Todo + + Inktick
- + diff --git a/package.json b/package.json index 9634852..8e754b3 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "date-fns": "^4.1.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/src/App.jsx b/src/App.jsx index 5427540..82a8764 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,111 @@ -export const App = () => { +"use client"; + +import { useEffect } from "react"; +import styled, { ThemeProvider, createGlobalStyle } from "styled-components"; +import { useStore } from "./store"; +import TodoList from "./components/TodoList"; +import TodoForm from "./components/TodoForm"; +import TodoStats from "./components/TodoStats"; +import EmptyState from "./components/EmptyState"; +import ThemeToggle from "./components/ThemeToggle"; +import { lightTheme, darkTheme } from "./theme"; + +const GlobalStyle = createGlobalStyle` + @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Libre+Baskerville:ital@0;1&display=swap'); + + * { + box-sizing: border-box; + margin: 0; + padding: 0; + } + + body { + font-family: 'Inter', sans-serif; + background-color: ${(props) => props.theme.background}; + color: ${(props) => props.theme.text}; + transition: all 0.3s ease; + line-height: 1.6; + } + + button, input, select { + font-family: 'Inter', sans-serif; + } +`; + +const AppContainer = styled.div` + max-width: 1200px; + margin: 0 auto; + padding: 2rem 1rem; + + @media (max-width: 600px) { + padding: 1rem; + } +`; + +const NotebookContainer = styled.div` + background-color: ${(props) => props.theme.paper}; + padding: 0; + position: relative; +`; + +const Header = styled.header` + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4rem; + padding-bottom: 1rem; +`; + +const Title = styled.h1` + font-size: 8rem; + font-weight: 700; + color: ${(props) => props.theme.title}; + letter-spacing: -2px; + + @media (max-width: 768px) { + font-size: 4rem; + } + + @media (max-width: 480px) { + font-size: 3rem; + } +`; + +function App() { + const { tasks, darkMode, toggleDarkMode, loadTasks } = useStore(); + + useEffect(() => { + loadTasks(); + }, [loadTasks]); + return ( -

React Boilerplate

- ) + + + +
+

+ This place to record information about daily events, emotions +

+
+ LISTAT + +
+
+ + + + + {tasks.length > 0 ? : } + +
+
+ ); } + +export default App; diff --git a/src/components/AboutPage.jsx b/src/components/AboutPage.jsx new file mode 100644 index 0000000..062bda2 --- /dev/null +++ b/src/components/AboutPage.jsx @@ -0,0 +1,56 @@ +import styled from "styled-components"; + +const AboutContainer = styled.div` + max-width: 1200px; + margin: 0 auto; + padding: 2rem 1rem; +`; + +const HeaderImage = styled.div` + width: 100%; + height: 400px; + background-image: url("/placeholder.svg?height=400&width=1200"); + background-size: cover; + background-position: center; + margin-bottom: 3rem; +`; + +const AboutTitle = styled.h1` + font-size: 3rem; + font-weight: 600; + margin-bottom: 2rem; + color: ${(props) => props.theme.title}; +`; + +const AboutText = styled.p` + font-family: "Libre Baskerville", serif; + font-size: 1.1rem; + line-height: 1.8; + max-width: 800px; + margin-bottom: 2rem; + color: ${(props) => props.theme.text}; +`; + +const AboutPage = () => { + return ( + + + About LISTAT + + LISTAT is a minimalist task management application designed with a + clean, editorial aesthetic inspired by Japanese design principles. The + application focuses on simplicity and elegance, allowing users to record + and track their daily activities, thoughts, and emotions without + distraction. + + + The design philosophy behind LISTAT emphasizes the beauty of negative + space, typography, and subtle interactions. Each element has been + carefully considered to create a calm, focused environment for managing + your tasks and reflections. + + + ); +}; + +export default AboutPage; diff --git a/src/components/EmptyState.jsx b/src/components/EmptyState.jsx new file mode 100644 index 0000000..0e09167 --- /dev/null +++ b/src/components/EmptyState.jsx @@ -0,0 +1,42 @@ +import styled from "styled-components"; + +const EmptyContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 5rem 1rem; + text-align: center; +`; + +const EmptyIllustration = styled.div` + font-size: 3rem; + margin-bottom: 1rem; + color: ${(props) => props.theme.accent}; + opacity: 0.7; +`; + +const EmptyTitle = styled.h2` + font-size: 2rem; + font-weight: 500; + margin-bottom: 1rem; + color: ${(props) => props.theme.title}; +`; + +const EmptyText = styled.p` + color: ${(props) => props.theme.text}; + max-width: 400px; + margin: 0 auto; + font-size: 1rem; +`; + +const EmptyState = () => { + return ( + + No tasks yet + Add your first task using the form above. + + ); +}; + +export default EmptyState; diff --git a/src/components/ThemeToggle.jsx b/src/components/ThemeToggle.jsx new file mode 100644 index 0000000..59f4d6f --- /dev/null +++ b/src/components/ThemeToggle.jsx @@ -0,0 +1,34 @@ +"use client"; + +import styled from "styled-components"; + +const ToggleButton = styled.button` + background: none; + border: none; + cursor: pointer; + font-size: 1rem; + color: ${(props) => props.theme.text}; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.3s ease; + text-transform: uppercase; + letter-spacing: 1px; + + &:hover { + opacity: 0.7; + } +`; + +const ThemeToggle = ({ darkMode, toggleDarkMode }) => { + return ( + + {darkMode ? "Light" : "Dark"} + + ); +}; + +export default ThemeToggle; diff --git a/src/components/TodoForm.jsx b/src/components/TodoForm.jsx new file mode 100644 index 0000000..afdbf95 --- /dev/null +++ b/src/components/TodoForm.jsx @@ -0,0 +1,151 @@ +"use client"; + +import { useState } from "react"; +import styled from "styled-components"; +import { useStore } from "../store"; + +const FormContainer = styled.form` + margin: 2rem 0 3rem; + display: flex; + gap: 1rem; + + @media (max-width: 768px) { + flex-direction: column; + } +`; + +const InputRow = styled.div` + display: flex; + gap: 1rem; + flex: 1; + + @media (max-width: 768px) { + flex-direction: column; + } +`; + +const Input = styled.input` + flex: 1; + padding: 0.75rem 0; + font-size: 1rem; + border: none; + border-bottom: 1px solid ${(props) => props.theme.border}; + background-color: transparent; + color: ${(props) => props.theme.text}; + + &:focus { + outline: none; + border-color: ${(props) => props.theme.accent}; + } + + &::placeholder { + color: ${(props) => props.theme.completed}; + } +`; + +const Select = styled.select` + padding: 0.75rem 0; + font-size: 1rem; + border: none; + border-bottom: 1px solid ${(props) => props.theme.border}; + background-color: transparent; + color: ${(props) => props.theme.text}; + + &:focus { + outline: none; + border-color: ${(props) => props.theme.accent}; + } +`; + +const Button = styled.button` + padding: 0.75rem 1.5rem; + font-size: 0.9rem; + background-color: transparent; + color: ${(props) => props.theme.text}; + border: 1px solid ${(props) => props.theme.accent}; + cursor: pointer; + transition: all 0.2s; + text-transform: uppercase; + letter-spacing: 1px; + + &:hover { + background-color: ${(props) => props.theme.accent}; + color: ${(props) => props.theme.paper}; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +`; + +const CompleteAllButton = styled(Button)` + background-color: transparent; + border: 1px solid ${(props) => props.theme.accent}; + + &:hover { + background-color: ${(props) => props.theme.accent}; + color: ${(props) => props.theme.paper}; + } +`; + +const TodoForm = () => { + const { addTask, completeAllTasks, categories } = useStore(); + const [text, setText] = useState(""); + const [category, setCategory] = useState("Personal"); + const [dueDate, setDueDate] = useState(""); + + const handleSubmit = (e) => { + e.preventDefault(); + if (text.trim()) { + addTask(text.trim(), category, dueDate || null); + setText(""); + setDueDate(""); + } + }; + + return ( + + + setText(e.target.value)} + aria-label="Task description" + /> + + + + + + setDueDate(e.target.value)} + aria-label="Due date" + /> + + + + + Complete All + + + + ); +}; + +export default TodoForm; diff --git a/src/components/TodoItem.jsx b/src/components/TodoItem.jsx new file mode 100644 index 0000000..234be14 --- /dev/null +++ b/src/components/TodoItem.jsx @@ -0,0 +1,185 @@ +"use client"; + +import { useState } from "react"; +import styled, { css, keyframes } from "styled-components"; +import { useStore } from "../store"; +import { format } from "date-fns"; + +const fadeIn = keyframes` + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +`; + +const ItemContainer = styled.li` + display: flex; + align-items: flex-start; + padding: 1.5rem 0; + border-bottom: 1px solid ${(props) => props.theme.border}; + animation: ${fadeIn} 0.3s ease; + position: relative; +`; + +const Checkbox = styled.div` + width: 18px; + height: 18px; + border: 1px solid ${(props) => props.theme.accent}; + margin-right: 12px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + ${(props) => + props.checked && + css` + background-color: ${(props) => props.theme.accent}; + + &::after { + content: "✓"; + color: ${(props) => props.theme.paper}; + font-size: 12px; + } + `} +`; + +const ContentContainer = styled.div` + flex: 1; +`; + +const TaskText = styled.p` + font-size: 2rem; + font-weight: 600; + margin-bottom: 0.5rem; + position: relative; + + ${(props) => + props.completed && + css` + color: ${(props) => props.theme.completed}; + text-decoration: line-through; + `} +`; + +const TaskMeta = styled.div` + display: flex; + font-size: 0.85rem; + color: ${(props) => props.theme.completed}; + flex-wrap: wrap; + gap: 12px; +`; + +const TaskDate = styled.span` + display: inline-block; +`; + +const TaskDueDate = styled.span` + display: inline-block; + color: ${(props) => + props.overdue ? props.theme.overdue : props.theme.completed}; + font-weight: ${(props) => (props.overdue ? "bold" : "normal")}; +`; + +const TaskCategory = styled.span` + display: inline-block; + color: ${(props) => props.theme.categoryText}; + font-size: 0.8rem; + font-weight: 500; +`; + +const TallyContainer = styled.div` + margin-top: 0.5rem; + font-size: 1rem; + color: ${(props) => props.theme.accent}; + letter-spacing: 4px; +`; + +const DeleteButton = styled.button` + background: none; + border: none; + color: ${(props) => props.theme.completed}; + cursor: pointer; + font-size: 1.2rem; + opacity: 0.6; + transition: opacity 0.2s; + margin-left: 8px; + + &:hover { + opacity: 1; + } +`; + +const TodoItem = ({ task }) => { + const { toggleTask, removeTask, getFormattedDate, isTaskOverdue } = + useStore(); + const [showDelete, setShowDelete] = useState(false); + + const handleToggle = () => { + toggleTask(task.id); + }; + + const formatTally = (completed) => { + if (!completed) return ""; + + // Create tally marks (groups of 5) + const fullGroups = Math.floor(1 / 5); + const remainder = 1 % 5; + + let tally = ""; + + // Add full groups (||||) + for (let i = 0; i < fullGroups; i++) { + tally += "||||/"; + } + + // Add remainder + for (let i = 0; i < remainder; i++) { + tally += "|"; + } + + return tally; + }; + + const overdue = task.dueDate && isTaskOverdue(task); + + return ( + setShowDelete(true)} + onMouseLeave={() => setShowDelete(false)} + > + + + + {task.text} + + + Created: {getFormattedDate(task.createdAt)} + + {task.dueDate && ( + + Due: {format(new Date(task.dueDate), "MMM d, yyyy")} + {overdue && " (overdue)"} + + )} + + {task.category} + + + {task.completed && ( + {formatTally(task.completed)} + )} + + + {showDelete && ( + removeTask(task.id)} + aria-label="Delete task" + > + × + + )} + + ); +}; + +export default TodoItem; diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx new file mode 100644 index 0000000..3b0ebdc --- /dev/null +++ b/src/components/TodoList.jsx @@ -0,0 +1,57 @@ +import styled from "styled-components"; +import { useStore } from "../store"; +import TodoItem from "./TodoItem"; + +const ListContainer = styled.ul` + list-style: none; + margin: 2rem 0; +`; + +const CategorySection = styled.div` + margin-bottom: 3rem; +`; + +const CategoryTitle = styled.h2` + font-size: 1.2rem; + font-weight: 500; + margin-bottom: 1rem; + color: ${(props) => props.theme.title}; + text-transform: uppercase; + letter-spacing: 1px; +`; + +const TodoList = () => { + const { tasks, categories } = useStore(); + + // Group tasks by category + const tasksByCategory = categories.reduce((acc, category) => { + const categoryTasks = tasks.filter((task) => task.category === category); + if (categoryTasks.length > 0) { + acc[category] = categoryTasks; + } + return acc; + }, {}); + + // Handle tasks without a category + const uncategorizedTasks = tasks.filter( + (task) => !categories.includes(task.category) + ); + if (uncategorizedTasks.length > 0) { + tasksByCategory["Other"] = uncategorizedTasks; + } + + return ( + + {Object.entries(tasksByCategory).map(([category, categoryTasks]) => ( + + {category} + {categoryTasks.map((task) => ( + + ))} + + ))} + + ); +}; + +export default TodoList; diff --git a/src/components/TodoStats.jsx b/src/components/TodoStats.jsx new file mode 100644 index 0000000..a0a90b0 --- /dev/null +++ b/src/components/TodoStats.jsx @@ -0,0 +1,54 @@ +import styled from "styled-components"; +import { useStore } from "../store"; + +const StatsContainer = styled.div` + display: flex; + justify-content: space-between; + padding: 1rem 0; + font-size: 0.9rem; + color: ${(props) => props.theme.text}; + border-top: 1px solid ${(props) => props.theme.border}; + border-bottom: 1px solid ${(props) => props.theme.border}; + margin: 2rem 0; + text-transform: uppercase; + letter-spacing: 1px; + + @media (max-width: 600px) { + flex-direction: column; + gap: 0.5rem; + } +`; + +const StatItem = styled.div` + display: flex; + align-items: center; + gap: 0.5rem; +`; + +const StatValue = styled.span` + font-weight: 500; + color: ${(props) => props.theme.accent}; +`; + +const TodoStats = () => { + const { getUncompletedCount, getCompletedCount, getTotalCount } = useStore(); + + return ( + + + Total: + {getTotalCount()} + + + Completed: + {getCompletedCount()} + + + Uncompleted: + {getUncompletedCount()} + + + ); +}; + +export default TodoStats; diff --git a/src/main.jsx b/src/main.jsx index 1b8ffe9..0d1758a 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,12 +1,9 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; -import { App } from './App.jsx' - -import './index.css' - -ReactDOM.createRoot(document.getElementById('root')).render( +ReactDOM.createRoot(document.getElementById("root")).render( -) +); diff --git a/src/store.js b/src/store.js new file mode 100644 index 0000000..28c0aba --- /dev/null +++ b/src/store.js @@ -0,0 +1,98 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { format } from "date-fns"; + +const useStore = create( + persist( + (set, get) => ({ + tasks: [], + darkMode: false, + categories: ["Personal", "Work", "Shopping", "Housework"], + + // Load tasks from localStorage + loadTasks: () => { + // This is handled by the persist middleware + }, + + // Add a new task + addTask: (text, category = "Personal", dueDate = null) => { + const newTask = { + id: Date.now().toString(), + text, + completed: false, + createdAt: new Date().toISOString(), + category, + dueDate: dueDate ? new Date(dueDate).toISOString() : null, + }; + + set((state) => ({ + tasks: [...state.tasks, newTask], + })); + }, + + // Toggle task completion + toggleTask: (id) => { + set((state) => ({ + tasks: state.tasks.map((task) => + task.id === id ? { ...task, completed: !task.completed } : task + ), + })); + }, + + // Remove a task + removeTask: (id) => { + set((state) => ({ + tasks: state.tasks.filter((task) => task.id !== id), + })); + }, + + // Complete all tasks + completeAllTasks: () => { + set((state) => ({ + tasks: state.tasks.map((task) => ({ ...task, completed: true })), + })); + }, + + // Toggle dark mode + toggleDarkMode: () => { + set((state) => ({ darkMode: !state.darkMode })); + }, + + // Get formatted date + getFormattedDate: (dateString) => { + return format(new Date(dateString), "MMM d, yyyy h:mm a"); + }, + + // Check if task is overdue + isTaskOverdue: (task) => { + if (!task.dueDate || task.completed) return false; + return new Date(task.dueDate) < new Date(); + }, + + // Get tasks by category + getTasksByCategory: (category) => { + return get().tasks.filter((task) => task.category === category); + }, + + // Get uncompleted tasks count + getUncompletedCount: () => { + return get().tasks.filter((task) => !task.completed).length; + }, + + // Get completed tasks count + getCompletedCount: () => { + return get().tasks.filter((task) => task.completed).length; + }, + + // Get total tasks count + getTotalCount: () => { + return get().tasks.length; + }, + }), + { + name: "notebook-todo-storage", + } + ) +); + +export { useStore }; diff --git a/src/theme.js b/src/theme.js new file mode 100644 index 0000000..172229b --- /dev/null +++ b/src/theme.js @@ -0,0 +1,35 @@ +export const lightTheme = { + background: "#f2f2f0", + paper: "#f8f8f6", + text: "#000000", + title: "#000000", + accent: "#000000", + border: "#d0d0d0", + completed: "#888888", + margin: "#e0e0e0", + buttonBg: "#f0f0f0", + buttonText: "#000000", + buttonHover: "#e0e0e0", + paperLines: "none", + overdue: "#000000", + categoryBg: "#f0f0f0", + categoryText: "#333333", +}; + +export const darkTheme = { + background: "#1a1a1a", + paper: "#222222", + text: "#e0e0e0", + title: "#ffffff", + accent: "#ffffff", + border: "#444444", + completed: "#888888", + margin: "#444444", + buttonBg: "#333333", + buttonText: "#e0e0e0", + buttonHover: "#444444", + paperLines: "none", + overdue: "#bbbbbb", + categoryBg: "#333333", + categoryText: "#dddddd", +}; diff --git a/vite.config.js b/vite.config.js index ba24244..1ff0da0 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,7 +1,7 @@ -import react from '@vitejs/plugin-react' -import { defineConfig } from 'vite' +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()] -}) + plugins: [react()], +}); From ca474dfa53ba51426da3c159c766550070b03d1d Mon Sep 17 00:00:00 2001 From: tildetilde Date: Tue, 20 May 2025 16:15:27 +0200 Subject: [PATCH 04/22] Improving fonts and use of styled components --- index.html | 10 ++++++- src/App.jsx | 69 ++++---------------------------------------- src/GlobalStyle.jsx | 31 ++++++++++++++++++++ src/LayoutStyles.jsx | 40 +++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 64 deletions(-) create mode 100644 src/GlobalStyle.jsx create mode 100644 src/LayoutStyles.jsx diff --git a/index.html b/index.html index da43dd5..6e13306 100644 --- a/index.html +++ b/index.html @@ -8,7 +8,15 @@ name="description" content="A minimalistic, modern todo app with a notebook feel" /> - Inktick + + + + + + Listat
diff --git a/src/App.jsx b/src/App.jsx index 82a8764..06698ae 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,76 +1,18 @@ "use client"; import { useEffect } from "react"; -import styled, { ThemeProvider, createGlobalStyle } from "styled-components"; +import { ThemeProvider } from "styled-components"; +import { AppContainer, NotebookContainer, Header, Title } from "./LayoutStyles"; import { useStore } from "./store"; +import GlobalStyle from "./GlobalStyle"; import TodoList from "./components/TodoList"; import TodoForm from "./components/TodoForm"; import TodoStats from "./components/TodoStats"; import EmptyState from "./components/EmptyState"; +import AboutPage from "./components/AboutPage"; import ThemeToggle from "./components/ThemeToggle"; import { lightTheme, darkTheme } from "./theme"; -const GlobalStyle = createGlobalStyle` - @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Libre+Baskerville:ital@0;1&display=swap'); - - * { - box-sizing: border-box; - margin: 0; - padding: 0; - } - - body { - font-family: 'Inter', sans-serif; - background-color: ${(props) => props.theme.background}; - color: ${(props) => props.theme.text}; - transition: all 0.3s ease; - line-height: 1.6; - } - - button, input, select { - font-family: 'Inter', sans-serif; - } -`; - -const AppContainer = styled.div` - max-width: 1200px; - margin: 0 auto; - padding: 2rem 1rem; - - @media (max-width: 600px) { - padding: 1rem; - } -`; - -const NotebookContainer = styled.div` - background-color: ${(props) => props.theme.paper}; - padding: 0; - position: relative; -`; - -const Header = styled.header` - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 4rem; - padding-bottom: 1rem; -`; - -const Title = styled.h1` - font-size: 8rem; - font-weight: 700; - color: ${(props) => props.theme.title}; - letter-spacing: -2px; - - @media (max-width: 768px) { - font-size: 4rem; - } - - @media (max-width: 480px) { - font-size: 3rem; - } -`; - function App() { const { tasks, darkMode, toggleDarkMode, loadTasks } = useStore(); @@ -84,7 +26,7 @@ function App() {

- This place to record information about daily events, emotions + List it. Do it. Get over it.

{tasks.length > 0 ? : } + ); diff --git a/src/GlobalStyle.jsx b/src/GlobalStyle.jsx new file mode 100644 index 0000000..41b2a24 --- /dev/null +++ b/src/GlobalStyle.jsx @@ -0,0 +1,31 @@ +import { createGlobalStyle } from "styled-components"; + +const GlobalStyle = createGlobalStyle` + * { + box-sizing: border-box; + margin: 0; + padding: 0; + } + + body { + font-family: 'Inter', sans-serif; + background-color: ${(props) => props.theme.background}; + color: ${(props) => props.theme.text}; + line-height: 1.6; + transition: all 0.3s ease; + } + + h1, h2, h3 { + font-family: 'Zen Kaku Gothic New', sans-serif; +} + + button, input, select { + font-family: 'Inter', sans-serif; + } + + small, em { + font-family: 'Libre Baskerville', serif; +} +`; + +export default GlobalStyle; diff --git a/src/LayoutStyles.jsx b/src/LayoutStyles.jsx new file mode 100644 index 0000000..8899a12 --- /dev/null +++ b/src/LayoutStyles.jsx @@ -0,0 +1,40 @@ +import styled from "styled-components"; + +export const AppContainer = styled.div` + max-width: 1200px; + margin: 0 auto; + padding: 2rem 1rem; + + @media (max-width: 600px) { + padding: 1rem; + } +`; + +export const NotebookContainer = styled.div` + background-color: ${(props) => props.theme.paper}; + padding: 0; + position: relative; +`; + +export const Header = styled.header` + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4rem; + padding-bottom: 1rem; +`; + +export const Title = styled.h1` + font-size: 8rem; + font-weight: 700; + color: ${(props) => props.theme.title}; + letter-spacing: -2px; + + @media (max-width: 768px) { + font-size: 4rem; + } + + @media (max-width: 480px) { + font-size: 3rem; + } +`; From ed115cb984d1a224eafc0ff0fd7dccfff6d0cb51 Mon Sep 17 00:00:00 2001 From: tildetilde Date: Tue, 20 May 2025 16:15:43 +0200 Subject: [PATCH 05/22] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ebcad7..c2e9efd 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# Inktick – A minimalistic, modern todo app with a notebook feel +# Listat – A minimalistic, modern todo app with a notebook feel From 62e65fa0449bb53bcd53a308ec608a3e3c36148e Mon Sep 17 00:00:00 2001 From: tildetilde Date: Tue, 20 May 2025 17:08:53 +0200 Subject: [PATCH 06/22] Styled form using react select --- index.html | 2 +- package.json | 3 +- public/listat.svg | 269 ++++++++++++++++++++++++++++++++++++ public/vite.svg | 1 - src/App.jsx | 2 +- src/components/TodoForm.jsx | 97 +++++++++---- src/store.js | 2 +- 7 files changed, 345 insertions(+), 31 deletions(-) create mode 100644 public/listat.svg delete mode 100644 public/vite.svg diff --git a/index.html b/index.html index 6e13306..08b98b7 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 06698ae..b4a0d4a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -26,7 +26,7 @@ function App() {

- List it. Do it. Get over it. + List it. Do it. Leave it.

props.theme.border}; - background-color: transparent; - color: ${(props) => props.theme.text}; - - &:focus { - outline: none; - border-color: ${(props) => props.theme.accent}; - } -`; - const Button = styled.button` padding: 0.75rem 1.5rem; font-size: 0.9rem; @@ -89,12 +76,72 @@ const CompleteAllButton = styled(Button)` } `; +const customStyles = { + control: (base) => ({ + ...base, + backgroundColor: "transparent", + border: "none", + borderBottom: "1px solid #000", + borderRadius: 0, + boxShadow: "none", + fontSize: "1rem", + color: "#000", + fontFamily: "Zen Kaku Gothic New, sans-serif", + padding: "0.5rem 0", + cursor: "pointer", + }), + menu: (base) => ({ + ...base, + backgroundColor: "#f5f5f3", + border: "1px solid #000", + borderRadius: 0, + boxShadow: "none", + fontFamily: "Zen Kaku Gothic New, sans-serif", + }), + option: (base, { isFocused, isSelected }) => ({ + ...base, + backgroundColor: isSelected + ? "#000" + : isFocused + ? "#dcdcdc" + : "transparent", + color: isSelected ? "#fff" : "#000", + cursor: "pointer", + fontWeight: isSelected ? "600" : "400", + padding: "0.5rem 1rem", + ":active": { + backgroundColor: "#000", + color: "#fff", + }, + ":focus": { + backgroundColor: "#000", + color: "#fff", + }, + }), + singleValue: (base) => ({ + ...base, + color: "#000", + }), + placeholder: (base) => ({ + ...base, + color: "#999", + }), + indicatorSeparator: () => ({ + display: "none", + }), +}; + const TodoForm = () => { const { addTask, completeAllTasks, categories } = useStore(); const [text, setText] = useState(""); const [category, setCategory] = useState("Personal"); const [dueDate, setDueDate] = useState(""); + const categoryOptions = categories.map((cat) => ({ + value: cat, + label: cat, + })); + const handleSubmit = (e) => { e.preventDefault(); if (text.trim()) { @@ -109,23 +156,21 @@ const TodoForm = () => { setText(e.target.value)} aria-label="Task description" /> - +
+ setText(e.target.value)} aria-label="Task description" diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx index c19d50f..61de0fd 100644 --- a/src/components/TodoList.jsx +++ b/src/components/TodoList.jsx @@ -34,14 +34,6 @@ const TodoList = () => { return acc; }, {}); - // Handle tasks without a category - const uncategorizedTasks = tasks - .filter((task) => !categories.includes(task.category)) - .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - if (uncategorizedTasks.length > 0) { - tasksByCategory["Other"] = uncategorizedTasks; - } - return ( {Object.entries(tasksByCategory).map(([category, categoryTasks]) => ( From 445950a50a48d56c0085b3157e603ee7188038f9 Mon Sep 17 00:00:00 2001 From: Tilde Egebrand <141025499+tildetilde@users.noreply.github.com> Date: Mon, 26 May 2025 10:49:50 +0200 Subject: [PATCH 19/22] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0b21c58..c249ac5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# (O)FÄRDIG – A minimalistic, modern todo app with a notebook feel +# (O)FÄRDIG +A minimalistic, modern todo app with a notebook feel Check it out -https://app.netlify.com/projects/listdoleave/ +[https://app.netlify.com/projects/listdoleave/](https://nevercomplete.netlify.app/) From 3c7859a2d47c0614271183fc191edf870bb4bfc8 Mon Sep 17 00:00:00 2001 From: Tilde Egebrand <141025499+tildetilde@users.noreply.github.com> Date: Mon, 26 May 2025 10:50:28 +0200 Subject: [PATCH 20/22] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index c249ac5..aa6c0a5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ # (O)FÄRDIG A minimalistic, modern todo app with a notebook feel -Check it out -[https://app.netlify.com/projects/listdoleave/](https://nevercomplete.netlify.app/) +Check it out[https://nevercomplete.netlify.app/] From d89f7149a31535df348fa3ec87adbf517350b3ab Mon Sep 17 00:00:00 2001 From: Tilde Egebrand <141025499+tildetilde@users.noreply.github.com> Date: Mon, 26 May 2025 10:50:41 +0200 Subject: [PATCH 21/22] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aa6c0a5..fa18b34 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ # (O)FÄRDIG A minimalistic, modern todo app with a notebook feel -Check it out[https://nevercomplete.netlify.app/] +Check it out: https://nevercomplete.netlify.app/ From 057431a973a791b2b0e97c34b54dd2c55f7e925c Mon Sep 17 00:00:00 2001 From: tildetilde Date: Tue, 27 May 2025 18:13:00 +0200 Subject: [PATCH 22/22] Accessibility fixes --- src/components/TodoItem.jsx | 4 ++-- src/components/TodoList.jsx | 12 +++++++----- src/js-project-todo.code-workspace | 10 ++++++++++ src/theme.js | 18 +++++++++--------- 4 files changed, 28 insertions(+), 16 deletions(-) create mode 100644 src/js-project-todo.code-workspace diff --git a/src/components/TodoItem.jsx b/src/components/TodoItem.jsx index bcbae66..1bdf28e 100644 --- a/src/components/TodoItem.jsx +++ b/src/components/TodoItem.jsx @@ -78,7 +78,7 @@ const TaskDate = styled.span` const TaskDueDate = styled.span` display: inline-block; color: ${(props) => - props.$overdue ? props.theme.overdue : props.theme.completed}; + props.$overdue ? props.theme.categoryText : props.theme.completed}; font-weight: ${(props) => (props.$overdue ? "bold" : "normal")}; `; @@ -151,4 +151,4 @@ const TodoItem = ({ task }) => { ); }; -export default TodoItem; \ No newline at end of file +export default TodoItem; diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx index 61de0fd..b7df6ca 100644 --- a/src/components/TodoList.jsx +++ b/src/components/TodoList.jsx @@ -35,16 +35,18 @@ const TodoList = () => { }, {}); return ( - + <> {Object.entries(tasksByCategory).map(([category, categoryTasks]) => ( {category} - {categoryTasks.map((task) => ( - - ))} +
    + {categoryTasks.map((task) => ( + + ))} +
))} -
+ ); }; diff --git a/src/js-project-todo.code-workspace b/src/js-project-todo.code-workspace new file mode 100644 index 0000000..1d122d3 --- /dev/null +++ b/src/js-project-todo.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { + "path": ".." + }, + { + "path": ".." + } + ] +} \ No newline at end of file diff --git a/src/theme.js b/src/theme.js index f1b9882..94eaf50 100644 --- a/src/theme.js +++ b/src/theme.js @@ -5,7 +5,7 @@ export const lightTheme = { title: "#000000", accent: "#000000", border: "#d0d0d0", - completed: "#888888", + completed: "#333333", margin: "#e0e0e0", buttonBg: "#f0f0f0", buttonText: "#000000", @@ -29,27 +29,27 @@ export const lightTheme = { export const darkTheme = { background: "#1a1a1a", paper: "#222222", - text: "#e0e0e0", + text: "#f0f0f0", title: "#ffffff", accent: "#ffffff", border: "#444444", - completed: "#888888", + completed: "#aaaaaa", margin: "#444444", buttonBg: "#333333", - buttonText: "#e0e0e0", + buttonText: "#f0f0f0", buttonHover: "#444444", paperLines: "none", - overdue: "#bbbbbb", + overdue: "#dddddd", categoryBg: "#333333", - categoryText: "#dddddd", + categoryText: "#eeeeee", select: { background: "#222222", border: "#555555", - text: "#e0e0e0", + text: "#f0f0f0", placeholder: "#888888", optionBg: "#1c1c1c", - optionHover: "#333333", + optionHover: "#444444", optionSelected: "#ffffff", optionSelectedText: "#000000", }, -}; \ No newline at end of file +};