Add today page
This commit is contained in:
parent
0cd010b4b1
commit
2f46b25eba
14 changed files with 859 additions and 58 deletions
|
|
@ -21,6 +21,7 @@ import ProfileSettings from "./components/Profile/ProfileSettings";
|
|||
import Layout from "./Layout";
|
||||
import { DataProvider } from "./contexts/DataContext";
|
||||
import { User } from "./entities/User";
|
||||
import TasksToday from "./components/Task/TasksToday";
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||
|
|
@ -85,7 +86,7 @@ const App: React.FC = () => {
|
|||
|
||||
useEffect(() => {
|
||||
if (currentUser && location.pathname === "/") {
|
||||
navigate("/tasks?type=today", { replace: true });
|
||||
navigate("/today", { replace: true }); // Navigate to /today instead of /tasks?type=today
|
||||
}
|
||||
}, [currentUser, location.pathname, navigate]);
|
||||
|
||||
|
|
@ -104,12 +105,13 @@ const App: React.FC = () => {
|
|||
{currentUser ? (
|
||||
<Layout
|
||||
currentUser={currentUser}
|
||||
setCurrentUser={setCurrentUser}
|
||||
setCurrentUser={setCurrentUser}
|
||||
isDarkMode={isDarkMode}
|
||||
toggleDarkMode={toggleDarkMode}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/tasks" replace />} />
|
||||
<Route path="/" element={<Navigate to="/today" replace />} />
|
||||
<Route path="/today" element={<TasksToday />} />
|
||||
<Route path="/tasks" element={<Tasks />} />
|
||||
<Route path="/projects" element={<Projects />} />
|
||||
<Route path="/project/:id" element={<ProjectDetails />} />
|
||||
|
|
@ -119,7 +121,10 @@ const App: React.FC = () => {
|
|||
<Route path="/tag/:id" element={<TagDetails />} />
|
||||
<Route path="/notes" element={<Notes />} />
|
||||
<Route path="/note/:id" element={<NoteDetails />} />
|
||||
<Route path="/profile" element={<ProfileSettings currentUser={currentUser} />} />
|
||||
<Route
|
||||
path="/profile"
|
||||
element={<ProfileSettings currentUser={currentUser} />}
|
||||
/>
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const Login: React.FC = () => {
|
|||
|
||||
if (response.ok) {
|
||||
console.log('Login successful:', data);
|
||||
navigate('/tasks?type=today&order_by=due_date%3Aasc');
|
||||
navigate('/today');
|
||||
} else {
|
||||
setError(data.errors[0] || 'Login failed. Please try again.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ interface SidebarNavProps {
|
|||
}
|
||||
|
||||
const navLinks = [
|
||||
{ path: '/tasks?type=today', title: 'Today', icon: <CalendarDaysIcon className="h-5 w-5" />, query: 'type=today' },
|
||||
{ path: '/today', title: 'Today', icon: <CalendarDaysIcon className="h-5 w-5" />, query: 'type=today' },
|
||||
{ path: '/tasks?type=upcoming', title: 'Upcoming', icon: <CalendarIcon className="h-5 w-5" />, query: 'type=upcoming' },
|
||||
{ path: '/tasks?type=next', title: 'Next Actions', icon: <ArrowRightCircleIcon className="h-5 w-5" />, query: 'type=next' },
|
||||
{ path: '/tasks?type=inbox', title: 'Inbox', icon: <InboxIcon className="h-5 w-5" />, query: 'type=inbox' },
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center flex-wrap justify-start md:justify-end space-x-1">
|
||||
<div className="flex items-center flex-wrap justify-start md:justify-end space-x-2">
|
||||
{/* Tags without onTagRemove prop */}
|
||||
<TaskTags tags={task.tags || []} />
|
||||
{task.due_date && <TaskDueDate dueDate={task.due_date} />}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@ const TaskTags: React.FC<TaskTagsProps> = ({ tags = [], onTagRemove, className }
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={`flex flex-wrap gap-1 ${className}`}>
|
||||
<div className={`flex flex-wrap gap-2 ${className}`}>
|
||||
{tags.map((tag, index) => (
|
||||
<div
|
||||
key={tag.id || index}
|
||||
className="flex items-center bg-gray-200 text-gray-800 text-xs font-medium mr-2 px-2.5 py-1 rounded-md dark:bg-gray-700 dark:text-gray-200 cursor-pointer"
|
||||
className="flex items-center bg-gray-200 text-gray-800 text-xs font-medium px-2 py-1.5 rounded-md dark:bg-gray-700 dark:text-gray-200 cursor-pointer"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
188
app/frontend/components/Task/TasksToday.tsx
Normal file
188
app/frontend/components/Task/TasksToday.tsx
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
import React from "react";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
ClipboardDocumentListIcon,
|
||||
ClockIcon,
|
||||
ArrowPathIcon,
|
||||
CalendarDaysIcon, // Import the icon for due tasks
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
import { Task } from "../../entities/Task";
|
||||
import { Project } from "../../entities/Project";
|
||||
|
||||
import useFetchTasks from "../../hooks/useFetchTasks";
|
||||
import useFetchProjects from "../../hooks/useFetchProjects";
|
||||
import useManageTasks from "../../hooks/useManageTasks";
|
||||
|
||||
import NewTask from "./NewTask";
|
||||
import TaskList from "./TaskList";
|
||||
|
||||
const TasksToday: React.FC = () => {
|
||||
// Fetch tasks and metrics
|
||||
const {
|
||||
tasks,
|
||||
metrics,
|
||||
isLoading: loadingTasks,
|
||||
isError: errorTasks,
|
||||
} = useFetchTasks({
|
||||
type: "today",
|
||||
});
|
||||
|
||||
// Fetch projects
|
||||
const {
|
||||
projects,
|
||||
isLoading: loadingProjects,
|
||||
isError: errorProjects,
|
||||
} = useFetchProjects();
|
||||
|
||||
// Task management functions
|
||||
const { updateTask, deleteTask } = useManageTasks();
|
||||
|
||||
// Handle task updates
|
||||
const handleTaskUpdate = (updatedTask: Task): void => {
|
||||
if (updatedTask.id === undefined) {
|
||||
console.error("Error updating task: Task ID is undefined.");
|
||||
return;
|
||||
}
|
||||
updateTask(updatedTask.id, updatedTask)
|
||||
.then(() => {
|
||||
// Optionally, refetch tasks or update local state
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error updating task:", error);
|
||||
});
|
||||
};
|
||||
|
||||
// Handle task deletion
|
||||
const handleTaskDelete = (taskId: number): void => {
|
||||
deleteTask(taskId)
|
||||
.then(() => {
|
||||
// Optionally, refetch tasks or update local state
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error deleting task:", error);
|
||||
});
|
||||
};
|
||||
|
||||
// Handle loading and error states
|
||||
if (loadingTasks || loadingProjects) {
|
||||
return <p>Loading...</p>;
|
||||
}
|
||||
|
||||
if (errorTasks) {
|
||||
return <p className="text-red-500">Error loading tasks.</p>;
|
||||
}
|
||||
|
||||
if (errorProjects) {
|
||||
return <p className="text-red-500">Error loading projects.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center px-4 lg:px-2">
|
||||
<div className="w-full max-w-5xl">
|
||||
{/* Header */}
|
||||
<div className="flex items-center mb-4">
|
||||
<h2 className="text-2xl font-light flex items-center">
|
||||
<CalendarDaysIcon className="h-5 w-5 mr-2" /> Today
|
||||
</h2>
|
||||
<span className="ml-4 text-gray-500">
|
||||
{format(new Date(), "EEEE, MMMM d, yyyy")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Overview of Tasks */}
|
||||
<div className="mb-6 grid grid-cols-1 sm:grid-cols-4 gap-4">
|
||||
{/* Total Open Tasks */}
|
||||
<div className="p-4 bg-white dark:bg-gray-900 rounded-lg shadow flex items-center">
|
||||
<ClipboardDocumentListIcon className="h-8 w-8 text-blue-500 mr-4" />
|
||||
<div>
|
||||
<p className="text-gray-500 dark:text-gray-400">Backlog</p>
|
||||
<p className="text-2xl font-semibold">{metrics.total_open_tasks}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tasks Pending Over a Month */}
|
||||
<div className="p-4 bg-white dark:bg-gray-900 rounded-lg shadow flex items-center">
|
||||
<ClockIcon className="h-8 w-8 text-yellow-500 mr-4" />
|
||||
<div>
|
||||
<p className="text-gray-500 dark:text-gray-400">Stale</p>
|
||||
<p className="text-2xl font-semibold">
|
||||
{metrics.tasks_pending_over_month}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tasks In Progress */}
|
||||
<div className="p-4 bg-white dark:bg-gray-900 rounded-lg shadow flex items-center">
|
||||
<ArrowPathIcon className="h-8 w-8 text-green-500 mr-4" />
|
||||
<div>
|
||||
<p className="text-gray-500 dark:text-gray-400">In Progress</p>
|
||||
<p className="text-2xl font-semibold">
|
||||
{metrics.tasks_in_progress_count}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tasks Due Today */}
|
||||
<div className="p-4 bg-white dark:bg-gray-900 rounded-lg shadow flex items-center">
|
||||
<CalendarDaysIcon className="h-8 w-8 text-red-500 mr-4" />
|
||||
<div>
|
||||
<p className="text-gray-500 dark:text-gray-400">Due Today</p>
|
||||
<p className="text-2xl font-semibold">
|
||||
{metrics.tasks_due_today.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tasks Due Today */}
|
||||
{metrics.tasks_due_today.length > 0 && (
|
||||
<>
|
||||
<h3 className="text-xl font-medium mt-6 mb-2">Tasks Due Today</h3>
|
||||
<TaskList
|
||||
tasks={metrics.tasks_due_today}
|
||||
onTaskUpdate={handleTaskUpdate}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
projects={projects}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Tasks In Progress */}
|
||||
{metrics.tasks_in_progress.length > 0 && (
|
||||
<>
|
||||
<h3 className="text-xl font-medium mt-6 mb-2">Tasks In Progress</h3>
|
||||
<TaskList
|
||||
tasks={metrics.tasks_in_progress}
|
||||
onTaskUpdate={handleTaskUpdate}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
projects={projects}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Suggested Tasks */}
|
||||
{metrics.suggested_tasks.length > 0 && (
|
||||
<>
|
||||
<h3 className="text-xl font-medium mt-6 mb-2">Suggested Tasks</h3>
|
||||
<TaskList
|
||||
tasks={metrics.suggested_tasks}
|
||||
onTaskUpdate={handleTaskUpdate}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
projects={projects}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Fallback Message */}
|
||||
{tasks.length === 0 && (
|
||||
<p className="text-gray-500 text-center mt-4">
|
||||
No tasks available for today.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TasksToday;
|
||||
|
|
@ -77,7 +77,7 @@ const Tasks: React.FC = () => {
|
|||
|
||||
if (tasksResponse.ok) {
|
||||
const tasksData = await tasksResponse.json();
|
||||
setTasks(tasksData || []);
|
||||
setTasks(tasksData.tasks || []);
|
||||
} else {
|
||||
throw new Error("Failed to fetch tasks.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export interface Task {
|
|||
note?: string;
|
||||
tags?: Tag[];
|
||||
project_id?: number;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export type StatusType = 'not_started' | 'in_progress' | 'done' | 'archived';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Task } from '../entities/Task';
|
||||
|
||||
interface UseFetchTasksOptions {
|
||||
type?: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
interface Metrics {
|
||||
total_open_tasks: number;
|
||||
tasks_pending_over_month: number;
|
||||
tasks_in_progress_count: number;
|
||||
tasks_in_progress: Task[];
|
||||
tasks_due_today: Task[];
|
||||
suggested_tasks: Task[];
|
||||
}
|
||||
|
||||
interface UseFetchTasksResult {
|
||||
tasks: Task[];
|
||||
metrics: Metrics;
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
mutate: () => void;
|
||||
}
|
||||
|
||||
const initialMetrics: Metrics = {
|
||||
total_open_tasks: 0,
|
||||
tasks_pending_over_month: 0,
|
||||
tasks_in_progress_count: 0,
|
||||
tasks_in_progress: [],
|
||||
tasks_due_today: [],
|
||||
suggested_tasks: [],
|
||||
};
|
||||
|
||||
const useFetchTasks = (options?: UseFetchTasksOptions): UseFetchTasksResult => {
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [metrics, setMetrics] = useState<Metrics>(initialMetrics);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [isError, setIsError] = useState<boolean>(false);
|
||||
|
||||
const fetchTasks = async () => {
|
||||
setIsLoading(true);
|
||||
setIsError(false);
|
||||
try {
|
||||
let url = '/api/tasks';
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (options?.type) {
|
||||
params.append('type', options.type);
|
||||
}
|
||||
if (options?.tag) {
|
||||
params.append('tag', options.tag);
|
||||
}
|
||||
|
||||
if (params.toString()) {
|
||||
url += `?${params.toString()}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
credentials: 'include',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setTasks(data.tasks || []);
|
||||
setMetrics(data.metrics || initialMetrics);
|
||||
} else {
|
||||
throw new Error('Failed to fetch tasks.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching tasks:', error);
|
||||
setIsError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTasks();
|
||||
}, [options?.type, options?.tag]);
|
||||
|
||||
return { tasks, metrics, isLoading, isError, mutate: fetchTasks };
|
||||
};
|
||||
|
||||
export default useFetchTasks;
|
||||
|
|
@ -6,9 +6,10 @@ class Task < ActiveRecord::Base
|
|||
enum priority: { low: 0, medium: 1, high: 2 }
|
||||
enum status: { not_started: 0, in_progress: 1, done: 2, archived: 3, waiting: 4 }
|
||||
|
||||
# Existing scopes
|
||||
scope :complete, -> { where(status: statuses[:done]) }
|
||||
scope :incomplete, -> { where.not(status: statuses[:done]) }
|
||||
scope :due_today, -> { incomplete.where('due_date <= ?', Date.today.end_of_day) }
|
||||
scope :due_today, -> { incomplete.where('DATE(due_date) < ?', Date.today) }
|
||||
scope :upcoming, -> { incomplete.where('due_date BETWEEN ? AND ?', Date.today, Date.today + 7.days) }
|
||||
scope :someday, -> { incomplete.where(due_date: nil) }
|
||||
scope :next_actions, -> { incomplete.where(due_date: nil, project_id: nil) }
|
||||
|
|
@ -26,5 +27,79 @@ class Task < ActiveRecord::Base
|
|||
scope :by_status, ->(status) { where(status: statuses[status]) }
|
||||
scope :by_priority, ->(priority) { where(priority: priorities[priority]) }
|
||||
|
||||
scope :order_by_priority, -> { order(priority: :desc) }
|
||||
|
||||
validates :name, presence: true, uniqueness: { scope: :user_id }
|
||||
|
||||
# New class method to filter tasks based on params
|
||||
def self.filter_by_params(params, user)
|
||||
tasks = user.tasks.includes(:project, :tags)
|
||||
|
||||
tasks = case params[:type]
|
||||
when 'today'
|
||||
tasks
|
||||
when 'upcoming'
|
||||
tasks.upcoming
|
||||
when 'next'
|
||||
tasks.next_actions
|
||||
when 'inbox'
|
||||
tasks.inbox
|
||||
when 'someday'
|
||||
tasks.someday
|
||||
when 'waiting'
|
||||
tasks.waiting_for
|
||||
else
|
||||
params[:status] == 'done' ? tasks.complete : tasks.incomplete
|
||||
end
|
||||
|
||||
tasks = tasks.with_tag(params[:tag]) if params[:tag]
|
||||
|
||||
tasks = tasks.apply_ordering(params[:order_by]) if params[:order_by]
|
||||
|
||||
tasks.left_joins(:tags).distinct
|
||||
end
|
||||
|
||||
scope :apply_ordering, lambda { |order_by|
|
||||
order_column, order_direction = order_by.split(':')
|
||||
order_direction ||= 'asc'
|
||||
order_direction = order_direction.downcase == 'desc' ? :desc : :asc
|
||||
|
||||
allowed_columns = %w[created_at updated_at name priority status due_date]
|
||||
raise ArgumentError, 'Invalid order column specified.' unless allowed_columns.include?(order_column)
|
||||
|
||||
if order_column == 'due_date'
|
||||
ordered_by_due_date(order_direction)
|
||||
else
|
||||
order("tasks.#{order_column} #{order_direction}")
|
||||
end
|
||||
}
|
||||
|
||||
def self.compute_metrics(user)
|
||||
total_open_tasks = user.tasks.incomplete.count
|
||||
|
||||
one_month_ago = Date.today - 30
|
||||
tasks_pending_over_month = user.tasks.incomplete.where('created_at < ?', one_month_ago).count
|
||||
|
||||
tasks_in_progress = user.tasks.incomplete.where(status: statuses[:in_progress])
|
||||
tasks_in_progress_count = tasks_in_progress.count
|
||||
|
||||
tasks_due_today = user.tasks.due_today
|
||||
|
||||
# Suggested tasks
|
||||
excluded_task_ids = tasks_in_progress.pluck(:id) + tasks_due_today.pluck(:id)
|
||||
suggested_tasks = user.tasks.incomplete
|
||||
.where(status: statuses[:not_started])
|
||||
.where.not(id: excluded_task_ids)
|
||||
.order_by_priority
|
||||
.limit(5)
|
||||
|
||||
{
|
||||
total_open_tasks: total_open_tasks,
|
||||
tasks_pending_over_month: tasks_pending_over_month,
|
||||
tasks_in_progress: tasks_in_progress,
|
||||
tasks_in_progress_count: tasks_in_progress_count,
|
||||
tasks_due_today: tasks_due_today,
|
||||
suggested_tasks: suggested_tasks
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,47 +15,31 @@ module Sinatra
|
|||
get '/api/tasks' do
|
||||
content_type :json
|
||||
|
||||
@tasks = current_user.tasks.includes(:project, :tags)
|
||||
|
||||
@tasks = case params[:type]
|
||||
when 'today'
|
||||
@tasks.due_today
|
||||
when 'upcoming'
|
||||
@tasks.upcoming
|
||||
when 'next'
|
||||
@tasks.next_actions
|
||||
when 'inbox'
|
||||
@tasks.inbox
|
||||
when 'someday'
|
||||
@tasks.someday
|
||||
when 'waiting'
|
||||
@tasks.waiting_for
|
||||
else
|
||||
params[:status] == 'done' ? @tasks.complete : @tasks.incomplete
|
||||
end
|
||||
|
||||
@tasks = @tasks.with_tag(params[:tag]) if params[:tag]
|
||||
|
||||
if params[:order_by]
|
||||
order_column, order_direction = params[:order_by].split(':')
|
||||
order_direction ||= 'asc'
|
||||
order_direction = order_direction.downcase == 'desc' ? :desc : :asc
|
||||
|
||||
allowed_columns = %w[created_at updated_at name priority status due_date]
|
||||
if allowed_columns.include?(order_column)
|
||||
@tasks = if order_column == 'due_date'
|
||||
@tasks.ordered_by_due_date(order_direction)
|
||||
else
|
||||
@tasks.order("tasks.#{order_column} #{order_direction}")
|
||||
end
|
||||
else
|
||||
halt 400, { error: 'Invalid order column specified.' }.to_json
|
||||
end
|
||||
begin
|
||||
tasks = Task.filter_by_params(params, current_user)
|
||||
rescue ArgumentError => e
|
||||
halt 400, { error: e.message }.to_json
|
||||
end
|
||||
|
||||
@tasks = @tasks.left_joins(:tags).distinct
|
||||
metrics = Task.compute_metrics(current_user)
|
||||
|
||||
@tasks.to_json(include: { tags: { only: %i[id name] }, project: { only: :name } })
|
||||
# Prepare the response
|
||||
response = {
|
||||
tasks: tasks.as_json(include: { tags: { only: %i[id name] }, project: { only: :name } }),
|
||||
metrics: {
|
||||
total_open_tasks: metrics[:total_open_tasks],
|
||||
tasks_pending_over_month: metrics[:tasks_pending_over_month],
|
||||
tasks_in_progress_count: metrics[:tasks_in_progress_count],
|
||||
tasks_in_progress: metrics[:tasks_in_progress].as_json(include: { tags: { only: %i[id name] },
|
||||
project: { only: :name } }),
|
||||
tasks_due_today: metrics[:tasks_due_today].as_json(include: { tags: { only: %i[id name] },
|
||||
project: { only: :name } }),
|
||||
suggested_tasks: metrics[:suggested_tasks].as_json(include: { tags: { only: %i[id name] },
|
||||
project: { only: :name } })
|
||||
}
|
||||
}
|
||||
|
||||
response.to_json
|
||||
end
|
||||
|
||||
post '/api/task' do
|
||||
|
|
@ -70,7 +54,7 @@ module Sinatra
|
|||
|
||||
task_attributes = {
|
||||
name: task_data['name'],
|
||||
priority: task_data['priority'] || 'medium',
|
||||
priority: task_data['priority'],
|
||||
due_date: task_data['due_date'],
|
||||
status: task_data['status'] || Task.statuses[:not_started],
|
||||
note: task_data['note'],
|
||||
|
|
|
|||
10
package-lock.json
generated
10
package-lock.json
generated
|
|
@ -11,6 +11,7 @@
|
|||
"dependencies": {
|
||||
"@heroicons/react": "^2.1.5",
|
||||
"@yaireo/tagify": "^4.31.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.26.2",
|
||||
|
|
@ -4222,6 +4223,15 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
"dependencies": {
|
||||
"@heroicons/react": "^2.1.5",
|
||||
"@yaireo/tagify": "^4.31.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.26.2",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue