generated from ludops/ludops-skeleton
Compare commits
No commits in common. "fix/timeline" and "main" have entirely different histories.
fix/timeli
...
main
|
|
@ -39,22 +39,16 @@ fastify.post<{ Body: { name: string; doesRepeat: boolean; repeatsEvery?: string;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate lastCompletedOn so the next due date lands correctly
|
// Calculate lastCompletedOn based on toDoToday
|
||||||
let lastCompletedOn: Date | null = null;
|
let lastCompletedOn: Date | null = null;
|
||||||
if (doesRepeat && repeatsEvery) {
|
if (!toDoToday && doesRepeat && repeatsEvery) {
|
||||||
|
// Set it in the future so it doesn't appear as due today
|
||||||
const [amount, unit] = repeatsEvery.split('_');
|
const [amount, unit] = repeatsEvery.split('_');
|
||||||
const days = unit === 'DAYS' ? parseInt(amount) :
|
const days = unit === 'DAYS' ? parseInt(amount) :
|
||||||
unit === 'WEEKS' ? parseInt(amount) * 7 :
|
unit === 'WEEKS' ? parseInt(amount) * 7 :
|
||||||
unit === 'MONTHS' ? parseInt(amount) * 30 :
|
unit === 'MONTHS' ? parseInt(amount) * 30 :
|
||||||
parseInt(amount) * 365;
|
parseInt(amount) * 365;
|
||||||
const msInterval = days * 24 * 60 * 60 * 1000;
|
lastCompletedOn = new Date(Date.now() - days * 24 * 60 * 60 * 1000 + 24 * 60 * 60 * 1000);
|
||||||
if (toDoToday) {
|
|
||||||
// next due = today: set lastCompletedOn = now - interval
|
|
||||||
lastCompletedOn = new Date(Date.now() - msInterval);
|
|
||||||
} else {
|
|
||||||
// next due = now + interval: set lastCompletedOn = now
|
|
||||||
lastCompletedOn = new Date();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [newTask] = await db.insert(tasks).values({
|
const [newTask] = await db.insert(tasks).values({
|
||||||
|
|
@ -87,19 +81,6 @@ fastify.patch<{ Params: { id: string } }>('/api/tasks/:id/complete', async (requ
|
||||||
reply.send(updatedTask);
|
reply.send(updatedTask);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.delete<{ Params: { id: string } }>('/api/tasks/:id', async (request, reply) => {
|
|
||||||
const id = parseInt(request.params.id);
|
|
||||||
|
|
||||||
const deleted = await db.delete(tasks).where(eq(tasks.id, id)).returning();
|
|
||||||
|
|
||||||
if (!deleted.length) {
|
|
||||||
reply.code(404).send({ error: 'Task not found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
reply.code(204).send();
|
|
||||||
});
|
|
||||||
|
|
||||||
fastify.patch<{ Params: { id: string } }>('/api/tasks/:id/renew', async (request, reply) => {
|
fastify.patch<{ Params: { id: string } }>('/api/tasks/:id/renew', async (request, reply) => {
|
||||||
const id = parseInt(request.params.id);
|
const id = parseInt(request.params.id);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,16 @@
|
||||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||||
<path fill="none" stroke="#de6c90" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||||
<path fill="none" stroke="#de6c90" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||||
<path fill="none" stroke="#de6c90" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||||
<path fill="none" stroke="#de6c90" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||||
<path fill="none" stroke="#de6c90" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
|
@ -1,54 +1,21 @@
|
||||||
main {
|
main {
|
||||||
max-width: 960px;
|
max-width: 1400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 48px 24px;
|
padding: 40px 20px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
main {
|
main {
|
||||||
padding: 24px 16px;
|
padding: 20px 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: #e8e8e8;
|
|
||||||
margin: 40px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lang-toggle {
|
|
||||||
background: none;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 4px 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #555;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
transition: border-color 0.15s, color 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lang-toggle:hover {
|
|
||||||
border-color: #111;
|
|
||||||
color: #111;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 36px;
|
font-size: 42px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #de6c90;
|
margin-bottom: 16px;
|
||||||
margin: 0;
|
color: #2c3e50;
|
||||||
letter-spacing: -0.5px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|
@ -57,35 +24,28 @@ h1 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shared section title — used by TodaysTasks and Timeline */
|
.subtitle {
|
||||||
.section-title {
|
color: var(--text);
|
||||||
font-size: 18px;
|
margin: 0;
|
||||||
font-weight: 600;
|
|
||||||
color: #111;
|
|
||||||
margin: 0 0 16px 0;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
user-select: none;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title--clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title--clickable:hover {
|
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
padding: 6px 12px;
|
padding: 8px 16px;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
color: #888;
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status--loading {
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status--ok {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status--error {
|
.status--error {
|
||||||
|
|
@ -97,43 +57,47 @@ h1 {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
background: white;
|
background: #ffebee;
|
||||||
color: #111;
|
color: #c62828;
|
||||||
border: 1px solid #e8e8e8;
|
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
max-width: 380px;
|
max-width: 400px;
|
||||||
font-size: 14px;
|
animation: slideIn 0.3s ease;
|
||||||
animation: slideIn 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.error-message {
|
.error-message {
|
||||||
left: 16px;
|
left: 12px;
|
||||||
right: 16px;
|
right: 12px;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from { transform: translateY(-8px); opacity: 0; }
|
from {
|
||||||
to { transform: translateY(0); opacity: 1; }
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-dismiss {
|
.error-dismiss {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: #999;
|
color: #c62828;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 16px;
|
font-size: 18px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 20px;
|
width: 24px;
|
||||||
height: 20px;
|
height: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -141,7 +105,7 @@ h1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-dismiss:hover {
|
.error-dismiss:hover {
|
||||||
color: #111;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,12 @@ import TodaysTasks from './components/TodaysTasks'
|
||||||
import CreateTaskForm from './components/CreateTaskForm'
|
import CreateTaskForm from './components/CreateTaskForm'
|
||||||
import type { Task, NewTaskInput } from './types'
|
import type { Task, NewTaskInput } from './types'
|
||||||
import { isTaskDueToday } from './utils/taskUtils'
|
import { isTaskDueToday } from './utils/taskUtils'
|
||||||
import { useLanguage } from './i18n/LanguageContext'
|
|
||||||
|
const APP_NAME = 'TODO'
|
||||||
|
|
||||||
type ApiStatus = 'loading' | 'ok' | 'error'
|
type ApiStatus = 'loading' | 'ok' | 'error'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { t, toggleLocale } = useLanguage()
|
|
||||||
const [apiStatus, setApiStatus] = useState<ApiStatus>('loading')
|
const [apiStatus, setApiStatus] = useState<ApiStatus>('loading')
|
||||||
const [tasks, setTasks] = useState<Task[]>([])
|
const [tasks, setTasks] = useState<Task[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
@ -52,29 +52,14 @@ function App() {
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json()
|
const errorData = await response.json()
|
||||||
setError(errorData.error || t.errorCreateTask)
|
setError(errorData.error || 'Failed to create task')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetchTasks()
|
await fetchTasks()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating task:', error)
|
console.error('Error creating task:', error)
|
||||||
setError(t.errorCreateTask)
|
setError('Failed to create task')
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeleteTask = async (taskId: number) => {
|
|
||||||
setError(null)
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/tasks/${taskId}`, { method: 'DELETE' })
|
|
||||||
if (!response.ok) {
|
|
||||||
setError(t.errorDeleteTask)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await fetchTasks()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting task:', error)
|
|
||||||
setError(t.errorDeleteTask)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,13 +71,31 @@ function App() {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
setError(t.errorCompleteTask)
|
setError('Failed to complete task')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await fetchTasks()
|
await fetchTasks()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error completing task:', error)
|
console.error('Error completing task:', error)
|
||||||
setError(t.errorCompleteTask)
|
setError('Failed to complete task')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRenewTask = async (taskId: number) => {
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/tasks/${taskId}/renew`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
setError('Failed to renew task')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await fetchTasks()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error renewing task:', error)
|
||||||
|
setError('Failed to renew task')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,16 +103,13 @@ function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<div className="app-header">
|
<h1>{APP_NAME}</h1>
|
||||||
<h1>{t.appName}</h1>
|
|
||||||
<button className="lang-toggle" onClick={toggleLocale}>{t.switchLang}</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{apiStatus === 'error' && (
|
|
||||||
<div className={`status status--${apiStatus}`}>
|
<div className={`status status--${apiStatus}`}>
|
||||||
{t.statusError}
|
{apiStatus === 'loading' && 'Connecting to API…'}
|
||||||
</div>)
|
{apiStatus === 'ok' && '✓ API & database connected'}
|
||||||
}
|
{apiStatus === 'error' && '⚠️ Could not reach API'}
|
||||||
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="error-message">
|
<div className="error-message">
|
||||||
|
|
@ -119,16 +119,16 @@ function App() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p>{t.loadingTasks}</p>
|
<p>Loading tasks...</p>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
<Timeline tasks={tasks} />
|
||||||
<TodaysTasks
|
<TodaysTasks
|
||||||
tasks={todaysTasks}
|
tasks={todaysTasks}
|
||||||
onComplete={handleCompleteTask}
|
onComplete={handleCompleteTask}
|
||||||
|
onRenew={handleRenewTask}
|
||||||
/>
|
/>
|
||||||
<div className="section-divider" />
|
|
||||||
<CreateTaskForm onCreateTask={handleCreateTask} />
|
<CreateTaskForm onCreateTask={handleCreateTask} />
|
||||||
<Timeline tasks={tasks} onDeleteTask={handleDeleteTask} />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,264 +1,13 @@
|
||||||
.create-task-form {
|
.create-task-form {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.open-form-btn {
|
.open-form-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
background: #de6c90;
|
background: #2c3e50;
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.open-form-btn:hover {
|
|
||||||
background: #de6c90;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.open-form-btn {
|
|
||||||
padding: 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-form {
|
|
||||||
background: white;
|
|
||||||
padding: 24px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.task-form {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-form h3 {
|
|
||||||
margin: 0 0 20px 0;
|
|
||||||
color: #de6c90;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.task-form h3 {
|
|
||||||
font-size: 16px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
color: #555;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Name input row with confirm button */
|
|
||||||
.name-input-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-input-row input[type="text"] {
|
|
||||||
flex: 1;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
box-sizing: border-box;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.name-input-row input[type="text"] {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-input-row input[type="text"]:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #de6c90;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-confirm-btn {
|
|
||||||
padding: 0 14px;
|
|
||||||
background: #de6c90;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-confirm-btn:hover {
|
|
||||||
background: #de6c90;
|
|
||||||
}
|
|
||||||
|
|
||||||
.char-count {
|
|
||||||
display: block;
|
|
||||||
text-align: right;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #999;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toggle button groups (replaces radio buttons) */
|
|
||||||
.btn-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-option {
|
|
||||||
padding: 8px 18px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: white;
|
|
||||||
color: #555;
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-option:hover {
|
|
||||||
border-color: #de6c90;
|
|
||||||
color: #de6c90;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-option--active {
|
|
||||||
background: #de6c90;
|
|
||||||
color: white;
|
|
||||||
border-color: #de6c90;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-option--active:hover {
|
|
||||||
background: #de6c90;
|
|
||||||
border-color: #de6c90;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repeat-input {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repeat-amount {
|
|
||||||
width: 80px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.repeat-amount {
|
|
||||||
width: 70px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.repeat-amount:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #de6c90;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repeat-unit {
|
|
||||||
flex: 1;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.repeat-unit {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.repeat-unit:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #de6c90;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.form-actions {
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn {
|
|
||||||
flex: 1;
|
|
||||||
padding: 12px;
|
|
||||||
background: #de6c90;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn:hover {
|
|
||||||
background: #de6c90;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn {
|
|
||||||
flex: 1;
|
|
||||||
padding: 12px;
|
|
||||||
background: white;
|
|
||||||
color: #666;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn:hover {
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-color: #bbb;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.open-form-btn {
|
|
||||||
width: 100%;
|
|
||||||
padding: 14px;
|
|
||||||
background: #9d4d67;
|
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
@ -276,7 +25,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.open-form-btn:hover {
|
.open-form-btn:hover {
|
||||||
background: #de6c90;
|
background: #1a252f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-form {
|
.task-form {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { useState, useRef } from 'react';
|
import { useState } from 'react';
|
||||||
import type { NewTaskInput } from '../types';
|
import type { NewTaskInput } from '../types';
|
||||||
import { useLanguage } from '../i18n/LanguageContext';
|
|
||||||
import './CreateTaskForm.css';
|
import './CreateTaskForm.css';
|
||||||
|
|
||||||
interface CreateTaskFormProps {
|
interface CreateTaskFormProps {
|
||||||
|
|
@ -8,27 +7,33 @@ interface CreateTaskFormProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateTaskForm({ onCreateTask }: CreateTaskFormProps) {
|
export default function CreateTaskForm({ onCreateTask }: CreateTaskFormProps) {
|
||||||
const { t } = useLanguage();
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [doesRepeat, setDoesRepeat] = useState(false);
|
const [doesRepeat, setDoesRepeat] = useState(true);
|
||||||
const [repeatAmount, setRepeatAmount] = useState('1');
|
const [repeatAmount, setRepeatAmount] = useState('1');
|
||||||
const [repeatUnit, setRepeatUnit] = useState<'WEEKS' | 'MONTHS' | 'YEARS'>('WEEKS');
|
const [repeatUnit, setRepeatUnit] = useState<'WEEKS' | 'MONTHS' | 'YEARS'>('WEEKS');
|
||||||
const [toDoToday, setToDoToday] = useState(true);
|
const [toDoToday, setToDoToday] = useState(true);
|
||||||
const [priority, setPriority] = useState<'ESSENTIAL' | 'WHEN_I_HAVE_TIME'>('ESSENTIAL');
|
const [priority, setPriority] = useState<'ESSENTIAL' | 'WHEN_I_HAVE_TIME'>('ESSENTIAL');
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!name.trim()) return;
|
|
||||||
|
if (!name.trim()) {
|
||||||
|
alert('Please enter a task name');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const taskInput: NewTaskInput = {
|
const taskInput: NewTaskInput = {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
doesRepeat,
|
doesRepeat,
|
||||||
repeatsEvery: doesRepeat ? `${repeatAmount}_${repeatUnit}` : undefined,
|
repeatsEvery: doesRepeat ? `${repeatAmount}_${repeatUnit}` : undefined,
|
||||||
priority,
|
priority,
|
||||||
toDoToday: doesRepeat ? toDoToday : true,
|
toDoToday,
|
||||||
};
|
};
|
||||||
|
|
||||||
onCreateTask(taskInput);
|
onCreateTask(taskInput);
|
||||||
|
|
||||||
|
// Reset form
|
||||||
setName('');
|
setName('');
|
||||||
setDoesRepeat(true);
|
setDoesRepeat(true);
|
||||||
setRepeatAmount('1');
|
setRepeatAmount('1');
|
||||||
|
|
@ -42,47 +47,50 @@ export default function CreateTaskForm({ onCreateTask }: CreateTaskFormProps) {
|
||||||
<div className="create-task-form">
|
<div className="create-task-form">
|
||||||
{!isOpen ? (
|
{!isOpen ? (
|
||||||
<button className="open-form-btn" onClick={() => setIsOpen(true)}>
|
<button className="open-form-btn" onClick={() => setIsOpen(true)}>
|
||||||
{t.createNewTask}
|
+ Create new task
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={handleSubmit} className="task-form">
|
<form onSubmit={handleSubmit} className="task-form">
|
||||||
<h3>{t.createNewTaskTitle}</h3>
|
<h3>Create New Task</h3>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>{t.taskLabel}</label>
|
<label>Task</label>
|
||||||
<div className="name-input-row">
|
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
|
||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value.slice(0, 60))}
|
onChange={(e) => setName(e.target.value.slice(0, 60))}
|
||||||
placeholder={t.taskPlaceholder}
|
placeholder="task name"
|
||||||
maxLength={60}
|
maxLength={60}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="name-confirm-btn"
|
|
||||||
onClick={() => inputRef.current?.blur()}
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
✓
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<span className="char-count">{name.length}/60</span>
|
<span className="char-count">{name.length}/60</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>{t.repeats}</label>
|
<label>Repeats?</label>
|
||||||
<div className="btn-group">
|
<div className="radio-group">
|
||||||
<button type="button" className={`btn-option${doesRepeat ? ' btn-option--active' : ''}`} onClick={() => setDoesRepeat(true)}>{t.yes}</button>
|
<label>
|
||||||
<button type="button" className={`btn-option${!doesRepeat ? ' btn-option--active' : ''}`} onClick={() => setDoesRepeat(false)}>{t.no}</button>
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={doesRepeat}
|
||||||
|
onChange={() => setDoesRepeat(true)}
|
||||||
|
/>
|
||||||
|
Yes
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={!doesRepeat}
|
||||||
|
onChange={() => setDoesRepeat(false)}
|
||||||
|
/>
|
||||||
|
No
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{doesRepeat && (
|
{doesRepeat && (
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>{t.every}</label>
|
<label>Every</label>
|
||||||
<div className="repeat-input">
|
<div className="repeat-input">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
|
@ -97,36 +105,64 @@ export default function CreateTaskForm({ onCreateTask }: CreateTaskFormProps) {
|
||||||
onChange={(e) => setRepeatUnit(e.target.value as any)}
|
onChange={(e) => setRepeatUnit(e.target.value as any)}
|
||||||
className="repeat-unit"
|
className="repeat-unit"
|
||||||
>
|
>
|
||||||
<option value="DAYS">{repeatAmount !== '1' ? t.days : t.day}</option>
|
<option value="DAYS">day{repeatAmount !== '1' ? 's' : ''}</option>
|
||||||
<option value="WEEKS">{repeatAmount !== '1' ? t.weeks : t.week}</option>
|
<option value="WEEKS">week{repeatAmount !== '1' ? 's' : ''}</option>
|
||||||
<option value="MONTHS">{repeatAmount !== '1' ? t.months : t.month}</option>
|
<option value="MONTHS">month{repeatAmount !== '1' ? 's' : ''}</option>
|
||||||
<option value="YEARS">{repeatAmount !== '1' ? t.years : t.year}</option>
|
<option value="YEARS">year{repeatAmount !== '1' ? 's' : ''}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{doesRepeat && (
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>{t.toDoRightNow}</label>
|
<label>To do today?</label>
|
||||||
<div className="btn-group">
|
<div className="radio-group">
|
||||||
<button type="button" className={`btn-option${toDoToday ? ' btn-option--active' : ''}`} onClick={() => setToDoToday(true)}>{t.yes}</button>
|
<label>
|
||||||
<button type="button" className={`btn-option${!toDoToday ? ' btn-option--active' : ''}`} onClick={() => setToDoToday(false)}>{t.no}</button>
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={toDoToday}
|
||||||
|
onChange={() => setToDoToday(true)}
|
||||||
|
/>
|
||||||
|
Yes
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={!toDoToday}
|
||||||
|
onChange={() => setToDoToday(false)}
|
||||||
|
/>
|
||||||
|
No
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>{t.priority}</label>
|
<label>Priority</label>
|
||||||
<div className="btn-group">
|
<div className="radio-group">
|
||||||
<button type="button" className={`btn-option${priority === 'ESSENTIAL' ? ' btn-option--active' : ''}`} onClick={() => setPriority('ESSENTIAL')}>{t.essential}</button>
|
<label>
|
||||||
<button type="button" className={`btn-option${priority === 'WHEN_I_HAVE_TIME' ? ' btn-option--active' : ''}`} onClick={() => setPriority('WHEN_I_HAVE_TIME')}>{t.whenIHaveTimePriority}</button>
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={priority === 'ESSENTIAL'}
|
||||||
|
onChange={() => setPriority('ESSENTIAL')}
|
||||||
|
/>
|
||||||
|
Essential
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={priority === 'WHEN_I_HAVE_TIME'}
|
||||||
|
onChange={() => setPriority('WHEN_I_HAVE_TIME')}
|
||||||
|
/>
|
||||||
|
When I have time
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-actions">
|
<div className="form-actions">
|
||||||
<button type="submit" className="submit-btn">{t.createTask}</button>
|
<button type="submit" className="submit-btn">Create Task</button>
|
||||||
<button type="button" className="cancel-btn" onClick={() => setIsOpen(false)}>{t.cancel}</button>
|
<button type="button" className="cancel-btn" onClick={() => setIsOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,139 +1,141 @@
|
||||||
.upcoming-section {
|
.timeline-container {
|
||||||
margin-top: 48px;
|
width: 100%;
|
||||||
padding-top: 32px;
|
padding: 40px 20px;
|
||||||
border-top: 1px solid #e8e8e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upcoming-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border: 1px solid #e8e8e8;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upcoming-list--scrollable {
|
|
||||||
max-height: calc(5 * 49px);
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upcoming-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto auto auto;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
background: white;
|
|
||||||
transition: background 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upcoming-row:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upcoming-row:hover {
|
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
}
|
border-radius: 12px;
|
||||||
|
margin-bottom: 30px;
|
||||||
.upcoming-name {
|
overflow-x: auto;
|
||||||
position: relative;
|
|
||||||
min-width: 0;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upcoming-name-text {
|
|
||||||
display: block;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #111;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upcoming-badge {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #888;
|
|
||||||
white-space: nowrap;
|
|
||||||
border: 1px solid #e8e8e8;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upcoming-due {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #111;
|
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
|
||||||
min-width: 80px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-upcoming {
|
|
||||||
color: #aaa;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: italic;
|
|
||||||
padding: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* shared tooltip style */
|
|
||||||
.name-tooltip {
|
|
||||||
position: absolute;
|
|
||||||
bottom: calc(100% + 6px);
|
|
||||||
left: 0;
|
|
||||||
background: #111;
|
|
||||||
color: white;
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
white-space: nowrap;
|
|
||||||
max-width: 280px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
z-index: 100;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-tooltip::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: 12px;
|
|
||||||
border: 5px solid transparent;
|
|
||||||
border-top-color: #111;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upcoming-delete {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #bbb;
|
|
||||||
font-size: 13px;
|
|
||||||
padding: 2px 4px;
|
|
||||||
line-height: 1;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: color 0.15s, background 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upcoming-delete:hover {
|
|
||||||
color: #111;
|
|
||||||
background: #f0f0f0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.upcoming-row {
|
.timeline-container {
|
||||||
grid-template-columns: 1fr auto auto;
|
padding: 24px 12px;
|
||||||
gap: 4px 12px;
|
margin-bottom: 20px;
|
||||||
padding: 10px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upcoming-badge {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upcoming-due {
|
|
||||||
font-size: 12px;
|
|
||||||
min-width: auto;
|
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
position: relative;
|
||||||
|
height: 150px;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 1200px;
|
||||||
|
min-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.timeline {
|
||||||
|
height: 120px;
|
||||||
|
min-width: 500px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-line {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.today-marker {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.today-arrow {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #e74c3c;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 0;
|
||||||
|
margin-bottom: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.today-arrow {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.today-label {
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 11px;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.today-label {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-day-group {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-date {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #999;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.timeline-date {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-tasks {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.timeline-tasks {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-task {
|
||||||
|
background: #555;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.timeline-task {
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
max-width: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-task.essential {
|
||||||
|
background: #2c3e50;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,75 +1,101 @@
|
||||||
import type { Task } from '../types';
|
import type { Task } from '../types';
|
||||||
import { calculateNextDueDate } from '../utils/taskUtils';
|
import { calculateNextDueDate, formatDate } from '../utils/taskUtils';
|
||||||
import { useLanguage } from '../i18n/LanguageContext';
|
|
||||||
import './Timeline.css';
|
import './Timeline.css';
|
||||||
|
|
||||||
interface TimelineProps {
|
interface TimelineProps {
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
onDeleteTask: (id: number) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_VISIBLE = 5;
|
export default function Timeline({ tasks }: TimelineProps) {
|
||||||
|
|
||||||
export default function Timeline({ tasks, onDeleteTask }: TimelineProps) {
|
|
||||||
const { t, locale } = useLanguage();
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
const upcomingItems = tasks
|
// Timeline range: 15 days past, 30 days future
|
||||||
.map(task => {
|
const startDate = new Date(today);
|
||||||
|
startDate.setDate(startDate.getDate() - 15);
|
||||||
|
|
||||||
|
const endDate = new Date(today);
|
||||||
|
endDate.setDate(endDate.getDate() + 30);
|
||||||
|
|
||||||
|
// Calculate positions for tasks on timeline
|
||||||
|
const totalDays = 45; // 15 past + 30 future
|
||||||
|
const todayPosition = (15 / totalDays) * 100; // 33.33% from left
|
||||||
|
|
||||||
|
const taskPositions = tasks.map(task => {
|
||||||
const dueDate = calculateNextDueDate(task);
|
const dueDate = calculateNextDueDate(task);
|
||||||
if (!dueDate) return null;
|
if (!dueDate) return null;
|
||||||
const due = new Date(dueDate);
|
|
||||||
due.setHours(0, 0, 0, 0);
|
|
||||||
if (due <= today) return null;
|
|
||||||
return { task, dueDate: due };
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
.sort((a, b) => a!.dueDate.getTime() - b!.dueDate.getTime()) as { task: Task; dueDate: Date }[];
|
|
||||||
|
|
||||||
const hasMore = upcomingItems.length > MAX_VISIBLE;
|
// Only show tasks within timeline range
|
||||||
|
if (dueDate < startDate || dueDate > endDate) return null;
|
||||||
|
|
||||||
const formatDueDate = (date: Date): string => {
|
const daysDiff = Math.floor((dueDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
const diff = Math.ceil((date.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
const position = (daysDiff / totalDays) * 100;
|
||||||
if (diff === 1) return t.tomorrow;
|
|
||||||
if (diff <= 7) return t.inDays(diff);
|
return {
|
||||||
return date.toLocaleDateString(locale === 'fr' ? 'fr-FR' : 'en-US', { month: 'short', day: 'numeric' });
|
task,
|
||||||
|
position,
|
||||||
|
dueDate,
|
||||||
};
|
};
|
||||||
|
}).filter(Boolean) as { task: Task; position: number; dueDate: Date }[];
|
||||||
|
|
||||||
|
// Group tasks by day
|
||||||
|
const tasksByDay = taskPositions.reduce((acc, item) => {
|
||||||
|
const dateKey = item.dueDate.toDateString();
|
||||||
|
if (!acc[dateKey]) acc[dateKey] = [];
|
||||||
|
acc[dateKey].push(item);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, typeof taskPositions>);
|
||||||
|
|
||||||
|
// Sort tasks within each day by priority
|
||||||
|
Object.values(tasksByDay).forEach(dayTasks => {
|
||||||
|
dayTasks.sort((a, b) => {
|
||||||
|
if (a.task.priority === 'ESSENTIAL' && b.task.priority !== 'ESSENTIAL') return -1;
|
||||||
|
if (a.task.priority !== 'ESSENTIAL' && b.task.priority === 'ESSENTIAL') return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="upcoming-section">
|
<div className="timeline-container">
|
||||||
<h2 className="section-title">{t.whatTheFutureHolds}</h2>
|
<div className="timeline">
|
||||||
{upcomingItems.length === 0 ? (
|
<div className="timeline-line"></div>
|
||||||
<p className="no-upcoming">{t.noUpcoming}</p>
|
|
||||||
) : (
|
{/* TODAY marker */}
|
||||||
<div className={`upcoming-list${hasMore ? ' upcoming-list--scrollable' : ''}`}>
|
<div className="today-marker" style={{ left: `${todayPosition}%` }}>
|
||||||
{upcomingItems.map(({ task, dueDate }) => (
|
<div className="today-arrow">↑</div>
|
||||||
<UpcomingRow key={task.id} task={task} dueLabel={formatDueDate(dueDate)} onDelete={() => onDeleteTask(task.id)} />
|
<div className="today-label">TODAY</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tasks */}
|
||||||
|
{Object.entries(tasksByDay).map(([dateKey, dayTasks]) => {
|
||||||
|
const avgPosition = dayTasks.reduce((sum, item) => sum + item.position, 0) / dayTasks.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={dateKey}
|
||||||
|
className="timeline-day-group"
|
||||||
|
style={{ left: `${avgPosition}%` }}
|
||||||
|
>
|
||||||
|
<div className="timeline-date">{formatDate(dayTasks[0].dueDate)}</div>
|
||||||
|
<div className="timeline-tasks">
|
||||||
|
{dayTasks.map(({ task }, index) => (
|
||||||
|
<div
|
||||||
|
key={task.id}
|
||||||
|
className={`timeline-task ${task.priority.toLowerCase()}`}
|
||||||
|
style={{
|
||||||
|
marginLeft: `${index * 8}px`,
|
||||||
|
zIndex: dayTasks.length - index
|
||||||
|
}}
|
||||||
|
title={task.name}
|
||||||
|
>
|
||||||
|
{task.name}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</section>
|
|
||||||
);
|
);
|
||||||
}
|
})}
|
||||||
|
</div>
|
||||||
interface UpcomingRowProps {
|
|
||||||
task: Task;
|
|
||||||
dueLabel: string;
|
|
||||||
onDelete: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpcomingRow({ task, dueLabel, onDelete }: UpcomingRowProps) {
|
|
||||||
const { t } = useLanguage();
|
|
||||||
return (
|
|
||||||
<div className="upcoming-row">
|
|
||||||
<span className="upcoming-name">
|
|
||||||
<span className="upcoming-name-text">{task.name}</span>
|
|
||||||
</span>
|
|
||||||
<span className="upcoming-badge">
|
|
||||||
{task.priority === 'ESSENTIAL' ? t.essential : t.whenIHaveTime}
|
|
||||||
</span>
|
|
||||||
<span className="upcoming-due">{dueLabel}</span>
|
|
||||||
<button className="upcoming-delete" onClick={onDelete} title={t.removeTask}>✕</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,112 +1,13 @@
|
||||||
.todays-tasks {
|
.todays-tasks {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-section {
|
.todays-tasks h2 {
|
||||||
margin-bottom: 36px;
|
font-size: 24px;
|
||||||
}
|
margin-bottom: 20px;
|
||||||
|
color: #2c3e50;
|
||||||
.count-badge {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
background: #de6c90;
|
|
||||||
color: white;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 1px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-icon {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.task-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 540px) {
|
|
||||||
.task-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid #e8e8e8;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: white;
|
|
||||||
min-width: 0;
|
|
||||||
transition: border-color 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card:hover {
|
|
||||||
border-color: #bbb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card-name {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
position: relative;
|
|
||||||
cursor: default;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #111;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border: 1px solid #e8e8e8;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
background: white;
|
|
||||||
color: #555;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all 0.15s;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn:hover {
|
|
||||||
background: #111;
|
|
||||||
border-color: #111;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.action-btn {
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-tasks {
|
|
||||||
color: #aaa;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|
@ -116,77 +17,162 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* When I have time — drawer */
|
.task-section {
|
||||||
.wiht-drawer {
|
margin-bottom: 24px;
|
||||||
width: 100%;
|
}
|
||||||
border: 2px dashed #ddd;
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.task-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.priority-header {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 10px 14px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #555;
|
||||||
|
border-left: 3px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.priority-header {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.priority-header.essential {
|
||||||
|
background: #fafafa;
|
||||||
|
color: #2c3e50;
|
||||||
|
border-left-color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priority-header.when-i-have-time {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #666;
|
||||||
|
border-left-color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priority-header.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: border-color 0.2s, background 0.2s;
|
transition: background 0.2s;
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wiht-drawer:hover {
|
.priority-header.clickable:hover {
|
||||||
border-color: #de6c90;
|
background: #efefef;
|
||||||
background: rgba(170, 59, 255, 0.03);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wiht-drawer--open {
|
.toggle-icon {
|
||||||
border-style: solid;
|
font-size: 12px;
|
||||||
border-color: #de6c90;
|
margin-left: 8px;
|
||||||
border-bottom-left-radius: 0;
|
color: #999;
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
border-bottom: none;
|
|
||||||
background: rgba(170, 59, 255, 0.04);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wiht-drawer-header {
|
.task-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
padding: 14px 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wiht-drawer-icon {
|
@media (max-width: 768px) {
|
||||||
font-size: 13px;
|
.task-list {
|
||||||
color: #de6c90;
|
gap: 6px;
|
||||||
width: 14px;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.task-item {
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item:hover {
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||||
|
border-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-name {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #2c3e50;
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.task-name {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wiht-drawer-label {
|
.action-btn {
|
||||||
flex: 1;
|
width: 32px;
|
||||||
font-size: 15px;
|
height: 32px;
|
||||||
font-weight: 600;
|
border: 1px solid #e0e0e0;
|
||||||
color: #444;
|
border-radius: 4px;
|
||||||
}
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
.wiht-drawer:hover .wiht-drawer-label {
|
transition: all 0.2s;
|
||||||
color: #de6c90;
|
display: flex;
|
||||||
}
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
.wiht-drawer--open .wiht-drawer-label {
|
|
||||||
color: #de6c90;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wiht-drawer-badge {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
background: #de6c90;
|
|
||||||
color: white;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 2px 9px;
|
|
||||||
min-width: 24px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wiht-drawer-body {
|
|
||||||
border: 2px solid #de6c90;
|
|
||||||
border-top: none;
|
|
||||||
border-bottom-left-radius: 6px;
|
|
||||||
border-bottom-right-radius: 6px;
|
|
||||||
padding: 16px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
background: white;
|
background: white;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.action-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.complete-btn:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-color: #2c3e50;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.renew-btn:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-color: #666;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-tasks {
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.no-tasks {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,100 +1,96 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { Task } from '../types';
|
import type { Task } from '../types';
|
||||||
import { useLanguage } from '../i18n/LanguageContext';
|
|
||||||
import './TodaysTasks.css';
|
import './TodaysTasks.css';
|
||||||
|
|
||||||
interface TodaysTasksProps {
|
interface TodaysTasksProps {
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
onComplete: (taskId: number) => void;
|
onComplete: (taskId: number) => void;
|
||||||
|
onRenew: (taskId: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TodaysTasks({ tasks, onComplete }: TodaysTasksProps) {
|
export default function TodaysTasks({ tasks, onComplete, onRenew }: TodaysTasksProps) {
|
||||||
const { t } = useLanguage();
|
const essentialTasks = tasks.filter(t => t.priority === 'ESSENTIAL');
|
||||||
const essentialTasks = tasks.filter(task => task.priority === 'ESSENTIAL');
|
const whenIHaveTimeTasks = tasks.filter(t => t.priority === 'WHEN_I_HAVE_TIME');
|
||||||
const whenIHaveTimeTasks = tasks.filter(task => task.priority === 'WHEN_I_HAVE_TIME');
|
|
||||||
const [showWhenIHaveTime, setShowWhenIHaveTime] = useState(false);
|
const [showWhenIHaveTime, setShowWhenIHaveTime] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="todays-tasks">
|
<div className="todays-tasks">
|
||||||
|
<h2>Today's Tasks</h2>
|
||||||
|
|
||||||
<section className="task-section">
|
{/* Essential Tasks */}
|
||||||
<h2 className="section-title">{t.needToDo}
|
<div className="task-section">
|
||||||
<span className="count-badge">{essentialTasks.length}</span></h2>
|
<h3 className="priority-header essential">Essential</h3>
|
||||||
{essentialTasks.length === 0 ? (
|
{essentialTasks.length === 0 ? (
|
||||||
<p className="no-tasks">{t.nothingEssential}</p>
|
<p className="no-tasks">No essential tasks for today</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="task-grid">
|
<div className="task-list">
|
||||||
{essentialTasks.map(task => (
|
{essentialTasks.map(task => (
|
||||||
<TaskCard
|
<TaskItem
|
||||||
key={task.id}
|
key={task.id}
|
||||||
task={task}
|
task={task}
|
||||||
onComplete={onComplete}
|
onComplete={onComplete}
|
||||||
|
onRenew={onRenew}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
{/* When I have time — collapsible drawer */}
|
{/* When I Have Time Tasks */}
|
||||||
<div
|
<div className="task-section">
|
||||||
className={`wiht-drawer${showWhenIHaveTime ? ' wiht-drawer--open' : ''}`}
|
<h3
|
||||||
|
className="priority-header when-i-have-time clickable"
|
||||||
onClick={() => setShowWhenIHaveTime(!showWhenIHaveTime)}
|
onClick={() => setShowWhenIHaveTime(!showWhenIHaveTime)}
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={(e) => e.key === 'Enter' && setShowWhenIHaveTime(v => !v)}
|
|
||||||
>
|
>
|
||||||
<div className="wiht-drawer-header">
|
When I have time ({whenIHaveTimeTasks.length})
|
||||||
<span className="wiht-drawer-icon">{showWhenIHaveTime ? '▾' : '▸'}</span>
|
<span className="toggle-icon">{showWhenIHaveTime ? '▼' : '▶'}</span>
|
||||||
<span className="wiht-drawer-label">{t.whenIHaveTime}</span>
|
</h3>
|
||||||
<span className="wiht-drawer-badge">{whenIHaveTimeTasks.length}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showWhenIHaveTime && (
|
{showWhenIHaveTime && (
|
||||||
<div className="wiht-drawer-body">
|
<div className="task-list">
|
||||||
{whenIHaveTimeTasks.length === 0 ? (
|
{whenIHaveTimeTasks.length === 0 ? (
|
||||||
<p className="no-tasks">{t.nothingHere}</p>
|
<p className="no-tasks">No tasks in this category</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="task-grid">
|
whenIHaveTimeTasks.map(task => (
|
||||||
{whenIHaveTimeTasks.map(task => (
|
<TaskItem
|
||||||
<TaskCard key={task.id} task={task} onComplete={onComplete} />
|
key={task.id}
|
||||||
))}
|
task={task}
|
||||||
</div>
|
onComplete={onComplete}
|
||||||
|
onRenew={onRenew}
|
||||||
|
/>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaskCardProps {
|
interface TaskItemProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
onComplete: (taskId: number) => void;
|
onComplete: (taskId: number) => void;
|
||||||
|
onRenew: (taskId: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TaskCard({ task, onComplete }: TaskCardProps) {
|
function TaskItem({ task, onComplete, onRenew }: TaskItemProps) {
|
||||||
const { t } = useLanguage();
|
|
||||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="task-card">
|
<div className="task-item">
|
||||||
<span
|
<span className="task-name">{task.name}</span>
|
||||||
className="task-card-name"
|
<div className="task-actions">
|
||||||
onClick={() => setTooltipVisible(v => !v)}
|
|
||||||
onMouseEnter={() => setTooltipVisible(true)}
|
|
||||||
onMouseLeave={() => setTooltipVisible(false)}
|
|
||||||
>
|
|
||||||
{task.name}
|
|
||||||
{tooltipVisible && <span className="name-tooltip">{task.name}</span>}
|
|
||||||
</span>
|
|
||||||
<div className="task-card-actions">
|
|
||||||
<button
|
<button
|
||||||
className="action-btn"
|
className="action-btn complete-btn"
|
||||||
onClick={() => onComplete(task.id)}
|
onClick={() => onComplete(task.id)}
|
||||||
title={t.markCompleted}
|
title="Mark as completed"
|
||||||
>
|
>
|
||||||
✓
|
✓
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="action-btn renew-btn"
|
||||||
|
onClick={() => onRenew(task.id)}
|
||||||
|
title="Renew task"
|
||||||
|
>
|
||||||
|
↻
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { createContext, useContext, useState } from 'react';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
import { translations } from './translations';
|
|
||||||
import type { Locale, Translations } from './translations';
|
|
||||||
|
|
||||||
interface LanguageContextType {
|
|
||||||
locale: Locale;
|
|
||||||
t: Translations;
|
|
||||||
toggleLocale: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LanguageContext = createContext<LanguageContextType>({
|
|
||||||
locale: 'fr',
|
|
||||||
t: translations.fr,
|
|
||||||
toggleLocale: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
export function LanguageProvider({ children }: { children: ReactNode }) {
|
|
||||||
const [locale, setLocale] = useState<Locale>('fr');
|
|
||||||
|
|
||||||
const toggleLocale = () => setLocale(l => (l === 'en' ? 'fr' : 'en'));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LanguageContext.Provider value={{ locale, t: translations[locale], toggleLocale }}>
|
|
||||||
{children}
|
|
||||||
</LanguageContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useLanguage() {
|
|
||||||
return useContext(LanguageContext);
|
|
||||||
}
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
export type Locale = 'en' | 'fr';
|
|
||||||
|
|
||||||
export const translations = {
|
|
||||||
en: {
|
|
||||||
appName: 'Stuff to do',
|
|
||||||
// API status
|
|
||||||
statusLoading: 'Connecting to API…',
|
|
||||||
statusOk: '✓ API & database connected',
|
|
||||||
statusError: '⚠️ Could not reach API',
|
|
||||||
loadingTasks: 'Loading tasks…',
|
|
||||||
// Errors
|
|
||||||
errorCreateTask: 'Failed to create task',
|
|
||||||
errorDeleteTask: 'Failed to delete task',
|
|
||||||
errorCompleteTask: 'Failed to complete task',
|
|
||||||
// TodaysTasks
|
|
||||||
needToDo: 'Need to do',
|
|
||||||
nothingEssential: 'Nothing essential for today',
|
|
||||||
whenIHaveTime: 'When I have time',
|
|
||||||
nothingHere: 'Nothing here',
|
|
||||||
markCompleted: 'Mark as completed',
|
|
||||||
// Timeline
|
|
||||||
whatTheFutureHolds: 'Upcoming tasks',
|
|
||||||
noUpcoming: 'No upcoming tasks scheduled',
|
|
||||||
tomorrow: 'Tomorrow',
|
|
||||||
inDays: (n: number) => `In ${n} days`,
|
|
||||||
removeTask: 'Remove task',
|
|
||||||
// Priority badges
|
|
||||||
essential: 'Essential',
|
|
||||||
// CreateTaskForm
|
|
||||||
createNewTask: '+ Create new task',
|
|
||||||
createNewTaskTitle: 'Create New Task',
|
|
||||||
taskLabel: 'Task',
|
|
||||||
taskPlaceholder: 'task name',
|
|
||||||
repeats: 'Repeats?',
|
|
||||||
yes: 'Yes',
|
|
||||||
no: 'No',
|
|
||||||
every: 'Every',
|
|
||||||
day: 'day',
|
|
||||||
days: 'days',
|
|
||||||
week: 'week',
|
|
||||||
weeks: 'weeks',
|
|
||||||
month: 'month',
|
|
||||||
months: 'months',
|
|
||||||
year: 'year',
|
|
||||||
years: 'years',
|
|
||||||
toDoRightNow: 'To do right now?',
|
|
||||||
priority: 'Priority',
|
|
||||||
whenIHaveTimePriority: 'When I have time',
|
|
||||||
createTask: 'Create Task',
|
|
||||||
cancel: 'Cancel',
|
|
||||||
// Lang toggle
|
|
||||||
switchLang: 'FR',
|
|
||||||
},
|
|
||||||
fr: {
|
|
||||||
appName: 'Y\'a des trucs à faire ?',
|
|
||||||
// API status
|
|
||||||
statusLoading: 'Connexion à l\'API…',
|
|
||||||
statusOk: '✓ API & base de données connectées',
|
|
||||||
statusError: '⚠️ Impossible de joindre l\'API',
|
|
||||||
loadingTasks: 'Chargement des tâches…',
|
|
||||||
// Errors
|
|
||||||
errorCreateTask: 'Impossible de créer la tâche',
|
|
||||||
errorDeleteTask: 'Impossible de supprimer la tâche',
|
|
||||||
errorCompleteTask: 'Impossible de marquer la tâche comme terminée',
|
|
||||||
// TodaysTasks
|
|
||||||
needToDo: 'À faire maintenant',
|
|
||||||
nothingEssential: 'Rien d\'essentiel pour aujourd\'hui',
|
|
||||||
whenIHaveTime: 'À faire quand j\'ai le temps',
|
|
||||||
nothingHere: 'Rien ici',
|
|
||||||
markCompleted: 'Marquer comme fait',
|
|
||||||
// Timeline
|
|
||||||
whatTheFutureHolds: 'Tâches à venir',
|
|
||||||
noUpcoming: 'Aucune tâche à venir',
|
|
||||||
tomorrow: 'Demain',
|
|
||||||
inDays: (n: number) => `Dans ${n} jours`,
|
|
||||||
removeTask: 'Supprimer la tâche',
|
|
||||||
// Priority badges
|
|
||||||
essential: 'Essentiel',
|
|
||||||
// CreateTaskForm
|
|
||||||
createNewTask: '+ Créer une tâche',
|
|
||||||
createNewTaskTitle: 'Créer une nouvelle tâche',
|
|
||||||
taskLabel: 'Tâche',
|
|
||||||
taskPlaceholder: 'nom de la tâche',
|
|
||||||
repeats: 'Se répète ?',
|
|
||||||
yes: 'Oui',
|
|
||||||
no: 'Non',
|
|
||||||
every: 'Tous les',
|
|
||||||
day: 'jour',
|
|
||||||
days: 'jours',
|
|
||||||
week: 'semaine',
|
|
||||||
weeks: 'semaines',
|
|
||||||
month: 'mois',
|
|
||||||
months: 'mois',
|
|
||||||
year: 'an',
|
|
||||||
years: 'ans',
|
|
||||||
toDoRightNow: 'À faire maintenant ?',
|
|
||||||
priority: 'Priorité',
|
|
||||||
whenIHaveTimePriority: 'Quand j\'ai le temps',
|
|
||||||
createTask: 'Créer la tâche',
|
|
||||||
cancel: 'Annuler',
|
|
||||||
// Lang toggle
|
|
||||||
switchLang: 'EN',
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type Translations = (typeof translations)[Locale];
|
|
||||||
|
|
@ -1,15 +1,47 @@
|
||||||
:root {
|
:root {
|
||||||
|
--text: #6b6375;
|
||||||
|
--text-h: #08060d;
|
||||||
|
--bg: #fff;
|
||||||
|
--border: #e5e4e7;
|
||||||
|
--code-bg: #f4f3ec;
|
||||||
|
--accent: #aa3bff;
|
||||||
|
--accent-bg: rgba(170, 59, 255, 0.1);
|
||||||
|
--accent-border: rgba(170, 59, 255, 0.5);
|
||||||
|
--shadow:
|
||||||
|
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
||||||
|
|
||||||
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||||
--mono: ui-monospace, Consolas, monospace;
|
--mono: ui-monospace, Consolas, monospace;
|
||||||
|
|
||||||
font: 16px/150% var(--sans);
|
font: 18px/145% var(--sans);
|
||||||
color: #111;
|
letter-spacing: 0.18px;
|
||||||
background: #fff;
|
color-scheme: light dark;
|
||||||
color-scheme: light;
|
color: var(--text);
|
||||||
|
background: var(--bg);
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--text: #9ca3af;
|
||||||
|
--text-h: #f3f4f6;
|
||||||
|
--bg: #16171d;
|
||||||
|
--border: #2e303a;
|
||||||
|
--code-bg: #1f2028;
|
||||||
|
--accent: #c084fc;
|
||||||
|
--accent-bg: rgba(192, 132, 252, 0.15);
|
||||||
|
--accent-border: rgba(192, 132, 252, 0.5);
|
||||||
|
--shadow:
|
||||||
|
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
@ -18,18 +50,42 @@
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
min-height: 100svh;
|
min-height: 100svh;
|
||||||
background: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3 {
|
h1,
|
||||||
|
h2 {
|
||||||
|
font-family: var(--heading);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-h);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 48px;
|
||||||
|
letter-spacing: -1.5px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--code-bg);
|
||||||
|
color: var(--text-h);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,9 @@ import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import { LanguageProvider } from './i18n/LanguageContext.tsx'
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<LanguageProvider>
|
|
||||||
<App />
|
<App />
|
||||||
</LanguageProvider>
|
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue