Add new logos (#463)
* Add new logos * fixup! Add new logos * fixup! fixup! Add new logos * Setup login screen * fixup! Setup login screen
32
README.md
|
|
@ -1,7 +1,14 @@
|
||||||
# 📝 tududi
|
<p align="center">
|
||||||
|
<img src="public/wide-logo-light.png" alt="tududi" width="400">
|
||||||
|
</p>
|
||||||
|
|
||||||
`tududi` is the self-hosted task management tool that puts you in control. Organize your life and projects with a clear, hierarchical structure,
|
<p align="center">
|
||||||
smart recurring tasks, and seamless Telegram integration. Get focused, stay productive, and keep your data private.
|
<h2 align="center">Productivity made simple</p></h2>
|
||||||
|
<p align="center">Organize your life and projects with a clear, hierarchical structure,<br>
|
||||||
|
smart recurring tasks, and seamless Telegram integration.<br>
|
||||||
|
Get focused, stay productive, and keep your data private.
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
@ -23,12 +30,12 @@ For the thinking behind tududi, read:
|
||||||
- **Task Management**: Create, update, and delete tasks. Mark tasks as completed and view them by different filters (Today, Upcoming, Someday). Order them by Name, Due Date, Date Created, or Priority.
|
- **Task Management**: Create, update, and delete tasks. Mark tasks as completed and view them by different filters (Today, Upcoming, Someday). Order them by Name, Due Date, Date Created, or Priority.
|
||||||
- **Subtasks**: Break down complex tasks into smaller, manageable subtasks with progress tracking and seamless navigation.
|
- **Subtasks**: Break down complex tasks into smaller, manageable subtasks with progress tracking and seamless navigation.
|
||||||
- **Recurring Tasks**: Comprehensive recurring task system with intelligent parent-child relationships:
|
- **Recurring Tasks**: Comprehensive recurring task system with intelligent parent-child relationships:
|
||||||
- **Multiple Recurrence Patterns**: Daily, weekly, monthly, monthly on specific weekdays, and monthly last day
|
- **Multiple Recurrence Patterns**: Daily, weekly, monthly, monthly on specific weekdays, and monthly last day
|
||||||
- **Completion-Based Recurrence**: Option to repeat based on completion date rather than due date
|
- **Completion-Based Recurrence**: Option to repeat based on completion date rather than due date
|
||||||
- **Smart Parent-Child Linking**: Generated task instances maintain connection to their original recurring pattern
|
- **Smart Parent-Child Linking**: Generated task instances maintain connection to their original recurring pattern
|
||||||
- **Direct Parent Editing**: Edit recurrence settings directly from any generated task instance
|
- **Direct Parent Editing**: Edit recurrence settings directly from any generated task instance
|
||||||
- **Flexible Scheduling**: Set custom intervals (every 2 weeks, every 3 months, etc.)
|
- **Flexible Scheduling**: Set custom intervals (every 2 weeks, every 3 months, etc.)
|
||||||
- **End Date Control**: Optional end dates for recurring series
|
- **End Date Control**: Optional end dates for recurring series
|
||||||
- **Project Sharing & Collaboration**: Share projects with team members and collaborate effectively
|
- **Project Sharing & Collaboration**: Share projects with team members and collaborate effectively
|
||||||
- **Quick Notes**: Create, update, delete, or assign text notes to projects.
|
- **Quick Notes**: Create, update, delete, or assign text notes to projects.
|
||||||
- **Tags**: Create tags for tasks and notes to enhance organization.
|
- **Tags**: Create tags for tasks and notes to enhance organization.
|
||||||
|
|
@ -38,9 +45,9 @@ For the thinking behind tududi, read:
|
||||||
- **Responsive Design**: Accessible from various devices, ensuring a consistent experience across desktops, tablets, and mobile phones.
|
- **Responsive Design**: Accessible from various devices, ensuring a consistent experience across desktops, tablets, and mobile phones.
|
||||||
- **Multi-Language Support**: Available in 24 languages with full localization support for a truly global productivity experience.
|
- **Multi-Language Support**: Available in 24 languages with full localization support for a truly global productivity experience.
|
||||||
- **Telegram Integration**:
|
- **Telegram Integration**:
|
||||||
- Create tasks directly through Telegram messages
|
- Create tasks directly through Telegram messages
|
||||||
- Receive daily digests of your tasks
|
- Receive daily digests of your tasks
|
||||||
- Quick capture of ideas and todos on the go
|
- Quick capture of ideas and todos on the go
|
||||||
|
|
||||||
## 🗺️ Roadmap
|
## 🗺️ Roadmap
|
||||||
|
|
||||||
|
|
@ -119,6 +126,7 @@ Contributions to tududi are welcome! Whether it's bug fixes, new features, docum
|
||||||
6. Push to your fork and open a Pull Request
|
6. Push to your fork and open a Pull Request
|
||||||
|
|
||||||
**Read our [Contributing Guide](.github/CONTRIBUTING.md) for:**
|
**Read our [Contributing Guide](.github/CONTRIBUTING.md) for:**
|
||||||
|
|
||||||
- Development setup and workflow
|
- Development setup and workflow
|
||||||
- Code standards and best practices
|
- Code standards and best practices
|
||||||
- Testing requirements
|
- Testing requirements
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,10 @@ const App: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="/about" element={<About />} />
|
<Route
|
||||||
|
path="/about"
|
||||||
|
element={<About isDarkMode={isDarkMode} />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/admin/users"
|
path="/admin/users"
|
||||||
element={
|
element={
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@ import React, { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { HeartIcon } from '@heroicons/react/24/outline';
|
import { HeartIcon } from '@heroicons/react/24/outline';
|
||||||
|
|
||||||
const About: React.FC = () => {
|
interface AboutProps {
|
||||||
|
isDarkMode?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const About: React.FC<AboutProps> = ({ isDarkMode = false }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [version, setVersion] = useState<string>('0.3');
|
const [version, setVersion] = useState<string>('0.3');
|
||||||
|
|
||||||
|
|
@ -33,9 +37,17 @@ const About: React.FC = () => {
|
||||||
<div className="max-w-2xl mx-auto">
|
<div className="max-w-2xl mx-auto">
|
||||||
{/* Logo and Version */}
|
{/* Logo and Version */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h2 className="text-4xl font-bold text-gray-900 dark:text-white mb-2">
|
<div className="flex justify-center mb-4">
|
||||||
tududi
|
<img
|
||||||
</h2>
|
src={
|
||||||
|
isDarkMode
|
||||||
|
? '/wide-logo-light.png'
|
||||||
|
: '/wide-logo-dark.png'
|
||||||
|
}
|
||||||
|
alt="tududi"
|
||||||
|
className="h-16 w-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<p className="text-lg text-gray-600 dark:text-gray-400">
|
<p className="text-lg text-gray-600 dark:text-gray-400">
|
||||||
{t('about.version', 'Version')} {version}
|
{t('about.version', 'Version')} {version}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
@ -9,6 +9,16 @@ const Login: React.FC = () => {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [isDarkMode] = useState<boolean>(() => {
|
||||||
|
const storedPreference = localStorage.getItem('isDarkMode');
|
||||||
|
return storedPreference !== null
|
||||||
|
? storedPreference === 'true'
|
||||||
|
: window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||||
|
}, [isDarkMode]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -45,56 +55,96 @@ const Login: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-100 flex flex-col items-center justify-center min-h-screen px-4">
|
<>
|
||||||
<h1 className="text-5xl font-bold text-gray-300 mb-6">tududi</h1>
|
{/* Navbar */}
|
||||||
<div className="bg-white p-8 rounded-lg shadow-md w-full max-w-sm">
|
<nav className="fixed top-0 left-0 right-0 z-50 text-gray-900 dark:text-white">
|
||||||
{error && (
|
<div className="h-16 flex items-center px-4 sm:px-6 lg:px-8">
|
||||||
<div className="mb-4 text-center text-red-500">{error}</div>
|
<img
|
||||||
)}
|
src={
|
||||||
<form onSubmit={handleSubmit}>
|
isDarkMode
|
||||||
<div className="mb-4">
|
? '/wide-logo-light.png'
|
||||||
<label
|
: '/wide-logo-dark.png'
|
||||||
htmlFor="email"
|
}
|
||||||
className="block text-gray-600 mb-1"
|
alt="tududi"
|
||||||
>
|
className="h-9 w-auto"
|
||||||
{t('auth.email', 'Email')}
|
/>
|
||||||
</label>
|
</div>
|
||||||
<input
|
</nav>
|
||||||
type="email"
|
|
||||||
id="email"
|
{/* Main Content */}
|
||||||
name="email"
|
<div className="bg-gray-100 dark:bg-gray-900 min-h-screen px-4 pt-16 flex items-center justify-center">
|
||||||
value={email}
|
<div className="w-full max-w-7xl flex flex-col lg:flex-row items-center justify-center gap-12 lg:gap-16">
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
{/* Left side - Login Form */}
|
||||||
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
<div className="w-full lg:w-auto flex flex-col items-center">
|
||||||
required
|
<div className="bg-white dark:bg-gray-800 p-10 rounded-lg shadow-md w-full max-w-2xl">
|
||||||
|
<h2 className="text-center text-2xl font-semibold text-gray-700 dark:text-gray-200 mb-12">
|
||||||
|
{t('auth.login', 'Login')}
|
||||||
|
</h2>
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 text-center text-red-500">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="block text-gray-600 dark:text-gray-300 mb-1"
|
||||||
|
>
|
||||||
|
{t('auth.email', 'Email')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEmail(e.target.value)
|
||||||
|
}
|
||||||
|
className="w-full px-4 py-2 border dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="block text-gray-600 dark:text-gray-300 mb-1"
|
||||||
|
>
|
||||||
|
{t('auth.password', 'Password')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) =>
|
||||||
|
setPassword(e.target.value)
|
||||||
|
}
|
||||||
|
className="w-full px-4 py-2 border dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-blue-500 text-white py-2 rounded-lg hover:bg-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
{t('auth.login', 'Login')}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right side - Graphic */}
|
||||||
|
<div className="hidden lg:flex items-center justify-center">
|
||||||
|
<img
|
||||||
|
src="/login-gfx.png"
|
||||||
|
alt="Login illustration"
|
||||||
|
className="max-w-md w-full h-auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
</div>
|
||||||
<label
|
|
||||||
htmlFor="password"
|
|
||||||
className="block text-gray-600 mb-1"
|
|
||||||
>
|
|
||||||
{t('auth.password', 'Password')}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="w-full bg-blue-500 text-white py-2 rounded-lg hover:bg-blue-600 transition-colors"
|
|
||||||
>
|
|
||||||
{t('auth.login', 'Login')}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||||
isSidebarOpen,
|
isSidebarOpen,
|
||||||
setIsSidebarOpen,
|
setIsSidebarOpen,
|
||||||
openTaskModal,
|
openTaskModal,
|
||||||
|
isDarkMode,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
|
|
@ -164,12 +165,17 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className={`flex items-center no-underline text-gray-900 dark:text-white ml-2 ${isSidebarOpen ? 'sm:ml-0' : 'sm:ml-2'}`}
|
className={`flex items-center no-underline ml-2 ${isSidebarOpen ? 'sm:ml-0' : 'sm:ml-2'}`}
|
||||||
>
|
>
|
||||||
<span className="text-2xl font-bold">
|
<img
|
||||||
<span className="sm:hidden">t</span>
|
src={
|
||||||
<span className="hidden sm:inline">tududi</span>
|
isDarkMode
|
||||||
</span>
|
? '/wide-logo-light.png'
|
||||||
|
: '/wide-logo-dark.png'
|
||||||
|
}
|
||||||
|
alt="tududi"
|
||||||
|
className="h-9 w-auto"
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,25 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const SidebarHeader: React.FC = () => {
|
interface SidebarHeaderProps {
|
||||||
|
isDarkMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarHeader: React.FC<SidebarHeaderProps> = ({ isDarkMode }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center mb-6 mt-2">
|
<div className="flex justify-center mb-6 mt-2">
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
className="flex justify-center items-center mb-2 no-underline text-gray-900 dark:text-white"
|
className="flex justify-center items-center mb-2 no-underline"
|
||||||
>
|
>
|
||||||
<span className="text-2xl font-bold mt-1">tududi</span>
|
<img
|
||||||
|
src={
|
||||||
|
isDarkMode
|
||||||
|
? '/wide-logo-light.png'
|
||||||
|
: '/wide-logo-dark.png'
|
||||||
|
}
|
||||||
|
alt="tududi"
|
||||||
|
className="h-12 w-auto"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
49
index.html
|
|
@ -150,34 +150,33 @@
|
||||||
.logo {
|
.logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 1.75rem;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #1e293b;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .logo {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo:hover {
|
.logo:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-icon {
|
.logo-image {
|
||||||
margin-right: 8px;
|
height: 36px;
|
||||||
display: flex;
|
width: auto;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-icon svg {
|
.logo-light {
|
||||||
color: #1e293b;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .logo-icon svg {
|
.logo-dark {
|
||||||
color: white;
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .logo-light {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .logo-dark {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-links {
|
.nav-links {
|
||||||
|
|
@ -1002,13 +1001,8 @@
|
||||||
padding: 15px 0;
|
padding: 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo-image {
|
||||||
font-size: 1.5rem;
|
height: 28px;
|
||||||
}
|
|
||||||
|
|
||||||
.logo-icon svg {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-links {
|
.nav-links {
|
||||||
|
|
@ -1292,13 +1286,8 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/" class="logo">
|
<a href="/" class="logo">
|
||||||
<div class="logo-icon">
|
<img src="public/wide-logo-dark.png" alt="tududi" class="logo-image logo-light">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="28" height="28">
|
<img src="public/wide-logo-light.png" alt="tududi" class="logo-image logo-dark">
|
||||||
<circle cx="16" cy="16" r="13" stroke="currentColor" stroke-width="3.5" fill="none"/>
|
|
||||||
<path d="M10 16l4 4 8-8" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<span>tududi</span>
|
|
||||||
</a>
|
</a>
|
||||||
<button class="mobile-menu-toggle" onclick="toggleMobileMenu()">
|
<button class="mobile-menu-toggle" onclick="toggleMobileMenu()">
|
||||||
<i class="fas fa-bars"></i>
|
<i class="fas fa-bars"></i>
|
||||||
|
|
|
||||||
83
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "tududi",
|
"name": "tududi",
|
||||||
"version": "v0.84.1",
|
"version": "v0.85.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "tududi",
|
"name": "tududi",
|
||||||
"version": "v0.84.1",
|
"version": "v0.85.1",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|
@ -74,6 +74,7 @@
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"babel-jest": "^29.0.0",
|
"babel-jest": "^29.0.0",
|
||||||
"babel-loader": "^9.2.1",
|
"babel-loader": "^9.2.1",
|
||||||
|
"copy-webpack-plugin": "^13.0.1",
|
||||||
"cross-env": "~7.0.3",
|
"cross-env": "~7.0.3",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
"eslint": "^8.0.0",
|
"eslint": "^8.0.0",
|
||||||
|
|
@ -6436,6 +6437,30 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/copy-webpack-plugin": {
|
||||||
|
"version": "13.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.1.tgz",
|
||||||
|
"integrity": "sha512-J+YV3WfhY6W/Xf9h+J1znYuqTye2xkBUIGyTPWuBAT27qajBa5mR4f8WBmfDY3YjRftT2kqZZiLi1qf0H+UOFw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"glob-parent": "^6.0.1",
|
||||||
|
"normalize-path": "^3.0.0",
|
||||||
|
"schema-utils": "^4.2.0",
|
||||||
|
"serialize-javascript": "^6.0.2",
|
||||||
|
"tinyglobby": "^0.2.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18.12.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/webpack"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"webpack": "^5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.44.0",
|
"version": "3.44.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.44.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.44.0.tgz",
|
||||||
|
|
@ -7204,9 +7229,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.0.4",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
|
|
@ -18414,6 +18439,54 @@
|
||||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyglobby": {
|
||||||
|
"version": "0.2.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
|
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fdir": "^6.5.0",
|
||||||
|
"picomatch": "^4.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinyglobby/node_modules/fdir": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"picomatch": "^3 || ^4"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"picomatch": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tmpl": {
|
"node_modules/tmpl": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"babel-jest": "^29.0.0",
|
"babel-jest": "^29.0.0",
|
||||||
"babel-loader": "^9.2.1",
|
"babel-loader": "^9.2.1",
|
||||||
|
"copy-webpack-plugin": "^13.0.1",
|
||||||
"cross-env": "~7.0.3",
|
"cross-env": "~7.0.3",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
"eslint": "^8.0.0",
|
"eslint": "^8.0.0",
|
||||||
|
|
|
||||||
BIN
public/favicon-16.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/favicon-32.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
public/favicon-48.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 15 KiB |
BIN
public/favicon.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -1,23 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
|
||||||
<style>
|
|
||||||
.circle {
|
|
||||||
stroke: #4a5568;
|
|
||||||
fill: none;
|
|
||||||
}
|
|
||||||
.checkmark {
|
|
||||||
stroke: #4a5568;
|
|
||||||
fill: none;
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.circle {
|
|
||||||
stroke: #e2e8f0;
|
|
||||||
}
|
|
||||||
.checkmark {
|
|
||||||
stroke: #e2e8f0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<rect width="32" height="32" fill="transparent"/>
|
|
||||||
<circle class="circle" cx="16" cy="16" r="13" stroke-width="2"/>
|
|
||||||
<path class="checkmark" d="M10 16l4 4 8-8" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 617 B |
BIN
public/icon-logo.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
|
|
@ -9,17 +9,10 @@
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
|
||||||
<!-- SVG favicon with built-in light/dark mode support -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
|
||||||
<!-- Light mode favicon for browsers that support it -->
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16.png">
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon-light.ico" media="(prefers-color-scheme: light)">
|
|
||||||
|
|
||||||
<!-- Dark mode favicon for browsers that support it -->
|
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon-dark.ico" media="(prefers-color-scheme: dark)">
|
|
||||||
|
|
||||||
<!-- Fallback favicon (medium gray - works reasonably in both modes) -->
|
|
||||||
<link rel="shortcut icon" href="/favicon.ico">
|
|
||||||
|
|
||||||
<!-- Web app manifest for PWA support -->
|
<!-- Web app manifest for PWA support -->
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
|
|
||||||
BIN
public/login-gfx.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
|
|
@ -8,14 +8,29 @@
|
||||||
"background_color": "#ffffff",
|
"background_color": "#ffffff",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/favicon.svg",
|
"src": "/icon-logo.png",
|
||||||
"sizes": "any",
|
"sizes": "512x512",
|
||||||
"type": "image/svg+xml",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/favicon.ico",
|
"src": "/favicon.png",
|
||||||
|
"sizes": "32x32",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/favicon-32.png",
|
||||||
|
"sizes": "32x32",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/favicon-16.png",
|
||||||
"sizes": "16x16",
|
"sizes": "16x16",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/favicon.ico",
|
||||||
|
"sizes": "16x16 32x32 48x48",
|
||||||
"type": "image/x-icon"
|
"type": "image/x-icon"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
BIN
public/wide-logo-dark.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
public/wide-logo-light.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
|
@ -2,6 +2,7 @@ const path = require('path');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
|
|
||||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
|
|
@ -67,6 +68,17 @@ module.exports = {
|
||||||
filename: 'index.html',
|
filename: 'index.html',
|
||||||
template: 'public/index.html'
|
template: 'public/index.html'
|
||||||
}),
|
}),
|
||||||
|
new CopyWebpackPlugin({
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
from: 'public',
|
||||||
|
to: '',
|
||||||
|
globOptions: {
|
||||||
|
ignore: ['**/index.html'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
|
|
||||||