Add details page
This commit is contained in:
parent
ac995b1b83
commit
a41b9a9227
@ -28,3 +28,5 @@ APP_SECRET=71bf50bfb778d456b3a376ff60d5dcd8
|
||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
||||
DATABASE_URL="postgresql://postgres:postgres@calendi-postgres.test:5432/postgres?serverVersion=16&charset=utf8"
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
OPENAI_API_KEY="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"
|
||||
@ -14,21 +14,22 @@
|
||||
"nelmio/api-doc-bundle": "^5.0",
|
||||
"phpdocumentor/reflection-docblock": "^5.6",
|
||||
"phpstan/phpdoc-parser": "^2.1",
|
||||
"symfony/asset": "6.4.*",
|
||||
"symfony/console": "6.4.*",
|
||||
"symfony/dotenv": "6.4.*",
|
||||
"prinsfrank/standards": "^3.12",
|
||||
"symfony/asset": "7.2.*",
|
||||
"symfony/console": "7.2.*",
|
||||
"symfony/dotenv": "7.2.*",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/framework-bundle": "6.4.*",
|
||||
"symfony/http-client": "6.4.*",
|
||||
"symfony/framework-bundle": "7.2.*",
|
||||
"symfony/http-client": "7.2.*",
|
||||
"symfony/monolog-bundle": "^3.10",
|
||||
"symfony/property-access": "6.4.*",
|
||||
"symfony/property-info": "6.4.*",
|
||||
"symfony/runtime": "6.4.*",
|
||||
"symfony/serializer": "6.4.*",
|
||||
"symfony/twig-bundle": "6.4.*",
|
||||
"symfony/uid": "6.4.*",
|
||||
"symfony/validator": "6.4.*",
|
||||
"symfony/yaml": "6.4.*"
|
||||
"symfony/property-access": "7.2.*",
|
||||
"symfony/property-info": "7.2.*",
|
||||
"symfony/runtime": "7.2.*",
|
||||
"symfony/serializer": "7.2.*",
|
||||
"symfony/twig-bundle": "7.2.*",
|
||||
"symfony/uid": "7.2.*",
|
||||
"symfony/validator": "7.2.*",
|
||||
"symfony/yaml": "7.2.*"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
@ -77,7 +78,7 @@
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "6.4.*"
|
||||
"require": "7.2.*"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
@ -85,7 +86,7 @@
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpstan/phpstan-symfony": "^2.0",
|
||||
"symfony/maker-bundle": "^1.62",
|
||||
"symfony/stopwatch": "6.4.*",
|
||||
"symfony/web-profiler-bundle": "6.4.*"
|
||||
"symfony/stopwatch": "7.2.*",
|
||||
"symfony/web-profiler-bundle": "7.2.*"
|
||||
}
|
||||
}
|
||||
|
||||
1397
backend/composer.lock
generated
1397
backend/composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -57,7 +57,7 @@ class GetEventsController extends AbstractController
|
||||
)]
|
||||
public function get(string $id): JsonResponse
|
||||
{
|
||||
$events = $this->readEventsHandler->handle(new ReadEvents((int)$id));
|
||||
$events = $this->readEventsHandler->handle(new ReadEvents($id));
|
||||
|
||||
if (count($events) === 0) {
|
||||
return $this->json(['error' => 'Event not found'], Response::HTTP_NOT_FOUND);
|
||||
|
||||
@ -7,11 +7,11 @@ use App\Domain\Model\PersistedEvent;
|
||||
class ReadEvents
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ?int $id = null
|
||||
private readonly ?string $id = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function id(): ?int
|
||||
public function id(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
@ -17,6 +17,10 @@ class ReadEventsHandler
|
||||
*/
|
||||
public function handle(ReadEvents $readEvents): array
|
||||
{
|
||||
if ($readEvents->id() !== null) {
|
||||
return array_filter([$this->eventRepository->find($readEvents->id())], static fn (?PersistedEvent $event) => $event !== null);
|
||||
}
|
||||
|
||||
return $this->eventRepository->findAll();
|
||||
}
|
||||
}
|
||||
|
||||
22
backend/src/Domain/Location/Model/Address.php
Normal file
22
backend/src/Domain/Location/Model/Address.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Location\Model;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
class Address
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $addressLine,
|
||||
public readonly City $city,
|
||||
) {
|
||||
if (strlen($addressLine) >= 100) {
|
||||
throw new InvalidArgumentException('Address line cannot be longer than 100 characters');
|
||||
}
|
||||
}
|
||||
|
||||
public static function create(string $addressLine, string $city, string $countryAlpha2, ?string $zipCode = null): self
|
||||
{
|
||||
return new self($addressLine, City::create($city, $countryAlpha2, $zipCode));
|
||||
}
|
||||
}
|
||||
23
backend/src/Domain/Location/Model/City.php
Normal file
23
backend/src/Domain/Location/Model/City.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Location\Model;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
class City
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
public readonly Country $country,
|
||||
public readonly ?ZipCode $zipCode = null,
|
||||
) {
|
||||
if (strlen($name) >= 100) {
|
||||
throw new InvalidArgumentException('City name cannot be longer than 100 characters');
|
||||
}
|
||||
}
|
||||
|
||||
public static function create(string $name, string $countryAlpha2, ?string $zipCode = null): self
|
||||
{
|
||||
return new self($name, new Country($countryAlpha2), $zipCode !== null ? new ZipCode($zipCode) : null);
|
||||
}
|
||||
}
|
||||
11
backend/src/Domain/Location/Model/Country.php
Normal file
11
backend/src/Domain/Location/Model/Country.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Location\Model;
|
||||
|
||||
class Country
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $alpha2,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Location\Model\Persisted;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'addresses')]
|
||||
class PersistedAddress
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
public private(set) ?int $id = null {
|
||||
get => $this->id;
|
||||
set => $this->id = $value;
|
||||
}
|
||||
|
||||
#[ORM\Column(type: 'string', length: 100)]
|
||||
public private(set) string $addressLine {
|
||||
get => $this->addressLine;
|
||||
set => $this->addressLine = $value;
|
||||
}
|
||||
|
||||
#[ORM\Column(type: 'string', length: 100)]
|
||||
public private(set) string $city {
|
||||
get => $this->city;
|
||||
set => $this->city = $value;
|
||||
}
|
||||
|
||||
#[ORM\Column(type: 'string', length: 2)]
|
||||
public private(set) string $countryAlpha2 {
|
||||
get => $this->countryAlpha2;
|
||||
set => $this->countryAlpha2 = $value;
|
||||
}
|
||||
|
||||
#[ORM\Column(type: 'string', length: 10, nullable: true)]
|
||||
public private(set) ?string $zipCode = null {
|
||||
get => $this->zipCode;
|
||||
set => $this->zipCode = $value;
|
||||
}
|
||||
|
||||
public function __construct(
|
||||
string $addressLine,
|
||||
string $city,
|
||||
string $countryAlpha2,
|
||||
?string $zipCode = null,
|
||||
) {
|
||||
$this->addressLine = $addressLine;
|
||||
$this->city = $city;
|
||||
$this->countryAlpha2 = $countryAlpha2;
|
||||
$this->zipCode = $zipCode;
|
||||
}
|
||||
}
|
||||
|
||||
16
backend/src/Domain/Location/Model/ZipCode.php
Normal file
16
backend/src/Domain/Location/Model/ZipCode.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Location\Model;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
class ZipCode
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $code,
|
||||
) {
|
||||
if (strlen($code) >= 16) {
|
||||
throw new InvalidArgumentException('Zip code cannot be longer than 16 characters');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -16,7 +16,7 @@ services:
|
||||
- proxy
|
||||
|
||||
postgres:
|
||||
hostname: calendi-postgres
|
||||
hostname: calendi-postgres.test
|
||||
image: postgres:15
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
|
||||
@ -17,18 +17,18 @@
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
background-color: var(--color-secondary);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
color: var(--text-on-dark);
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
|
||||
@ -3,6 +3,7 @@ import { TabView } from './components/navigation/TabView';
|
||||
import Home from './pages/home/Home';
|
||||
import Profile from './pages/profile/Profile';
|
||||
import EditEvent from './pages/edit-event/EditEvent';
|
||||
import EventDetails from './pages/event-details/EventDetails';
|
||||
import { faCalendar, faUser } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faHome } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useUser } from './lib/context';
|
||||
@ -32,6 +33,7 @@ function App() {
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/edit-event/:id" element={<EditEvent />} />
|
||||
<Route path="/event/:id" element={<EventDetails />} />
|
||||
<Route path="*" element={<TabView tabs={tabs} />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
max-width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #f8fafc;
|
||||
background-color: var(--bg-primary);
|
||||
overflow-x: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@ -24,10 +24,10 @@
|
||||
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
background-color: var(--bg-primary);
|
||||
border-top: 1px solid var(--border-light);
|
||||
margin-top: auto;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
|
||||
box-shadow: 0 -2px 10px var(--shadow-color);
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 5px 10px;
|
||||
position: fixed;
|
||||
@ -45,7 +45,7 @@
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
color: var(--color-slate);
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 12px;
|
||||
margin: 0 4px;
|
||||
@ -60,15 +60,15 @@
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 3px;
|
||||
background-color: #3182ce;
|
||||
background-color: var(--color-accent);
|
||||
transition: all 0.3s ease;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.tab-bar-item:hover {
|
||||
color: #334155;
|
||||
background-color: #f1f5f9;
|
||||
color: var(--text-secondary);
|
||||
background-color: var(--state-hover);
|
||||
}
|
||||
|
||||
.tab-bar-item:hover::after {
|
||||
@ -76,9 +76,9 @@
|
||||
}
|
||||
|
||||
.tab-bar-item.active {
|
||||
color: #3182ce;
|
||||
background-color: #ebf8ff;
|
||||
box-shadow: 0 2px 6px rgba(49, 130, 206, 0.15);
|
||||
color: var(--color-secondary);
|
||||
background-color: var(--state-active);
|
||||
box-shadow: 0 2px 6px var(--shadow-color);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
@ -115,8 +115,8 @@
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
|
||||
background-color: var(--bg-primary);
|
||||
box-shadow: 0 5px 15px var(--shadow-color);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
.calendar {
|
||||
background-color: #fff;
|
||||
background-color: var(--bg-primary);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 4px 12px var(--shadow-color);
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
@ -23,15 +23,15 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
background-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
.headerTitle {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: #333;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.navButton {
|
||||
@ -44,18 +44,18 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
color: var(--color-slate);
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.navButton:hover {
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
background-color: var(--state-hover);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.navButton:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.3);
|
||||
box-shadow: 0 0 0 2px var(--state-focus);
|
||||
}
|
||||
|
||||
/* Calendar grid styles */
|
||||
@ -71,8 +71,8 @@
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #777;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
color: var(--color-slate);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.day, .emptyDay {
|
||||
@ -84,7 +84,7 @@
|
||||
}
|
||||
|
||||
.day:hover {
|
||||
background-color: #f5f5f5;
|
||||
background-color: var(--state-hover);
|
||||
}
|
||||
|
||||
.dayNumber {
|
||||
@ -93,7 +93,7 @@
|
||||
left: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #444;
|
||||
color: var(--text-primary);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
display: flex;
|
||||
@ -103,12 +103,12 @@
|
||||
}
|
||||
|
||||
.today .dayNumber {
|
||||
background-color: #4285f4;
|
||||
color: white;
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--text-on-dark);
|
||||
}
|
||||
|
||||
.selectedDay {
|
||||
background-color: rgba(66, 133, 244, 0.08);
|
||||
background-color: var(--state-active);
|
||||
}
|
||||
|
||||
.selectedDay .dayNumber {
|
||||
@ -134,21 +134,21 @@
|
||||
|
||||
.moreEvents {
|
||||
font-size: 10px;
|
||||
color: #777;
|
||||
color: var(--color-slate);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Events list styles */
|
||||
.eventsList {
|
||||
padding: 16px;
|
||||
border-left: 1px solid #f0f0f0;
|
||||
border-left: 1px solid var(--border-light);
|
||||
flex: 1;
|
||||
max-height: 460px;
|
||||
overflow-y: auto;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
border-left: none;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,13 +156,13 @@
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 16px;
|
||||
color: #333;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.eventsListEmpty {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #777;
|
||||
color: var(--color-slate);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@ -175,26 +175,26 @@
|
||||
.event {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background-color: #f8f9fa;
|
||||
border-left: 4px solid #4285f4;
|
||||
background-color: var(--state-hover);
|
||||
border-left: 4px solid var(--event-default);
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.event:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 2px 8px var(--shadow-color);
|
||||
}
|
||||
|
||||
.eventTime {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
color: var(--color-slate);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.eventTitle {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { CalendarHeader } from './CalendarHeader';
|
||||
import { CalendarGrid } from './CalendarGrid';
|
||||
import { CalendarEventsList } from './CalendarEventsList';
|
||||
@ -25,6 +26,7 @@ export const Calendar = ({
|
||||
onEventClick,
|
||||
initialDate = new Date()
|
||||
}: CalendarProps) => {
|
||||
const navigate = useNavigate();
|
||||
const [currentDate, setCurrentDate] = useState<Date>(initialDate);
|
||||
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
||||
const [daysInMonth, setDaysInMonth] = useState<number[]>([]);
|
||||
@ -64,6 +66,15 @@ export const Calendar = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleEventClick = (event: CalendarEvent) => {
|
||||
if (onEventClick) {
|
||||
onEventClick(event);
|
||||
} else {
|
||||
// Default behavior: navigate to event details
|
||||
navigate(`/event/${event.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredEvents = selectedDate
|
||||
? events.filter(event => formatDate(event.date) === formatDate(selectedDate))
|
||||
: [];
|
||||
@ -90,7 +101,7 @@ export const Calendar = ({
|
||||
<CalendarEventsList
|
||||
events={filteredEvents}
|
||||
date={selectedDate}
|
||||
onEventClick={onEventClick}
|
||||
onEventClick={handleEventClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -35,9 +35,9 @@
|
||||
}
|
||||
|
||||
.event-draft-card {
|
||||
background-color: #ffffff;
|
||||
background-color: var(--bg-primary);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2), 0 6px 12px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 0 10px 30px var(--shadow-color), 0 6px 12px var(--shadow-color);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 24px;
|
||||
@ -58,7 +58,7 @@
|
||||
border: none;
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
color: var(--color-slate);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
@ -69,8 +69,8 @@
|
||||
}
|
||||
|
||||
.draft-close-button:hover {
|
||||
background-color: #f0f0f0;
|
||||
color: #000;
|
||||
background-color: var(--state-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.draft-card-content {
|
||||
@ -82,14 +82,14 @@
|
||||
font-weight: 600;
|
||||
margin: 0 0 16px 0;
|
||||
line-height: 1.2;
|
||||
color: #000000;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.draft-description {
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
@ -104,7 +104,7 @@
|
||||
.draft-time {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
color: #555;
|
||||
color: var(--color-slate);
|
||||
}
|
||||
|
||||
.draft-label {
|
||||
@ -116,8 +116,8 @@
|
||||
|
||||
.draft-all-day {
|
||||
display: inline-block;
|
||||
background-color: #F2ADAD;
|
||||
color: #000;
|
||||
background-color: var(--color-accent);
|
||||
color: var(--text-primary);
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
@ -144,21 +144,21 @@
|
||||
}
|
||||
|
||||
.draft-save-button {
|
||||
background-color: #F2ADAD;
|
||||
color: #000000;
|
||||
background-color: var(--color-accent);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.draft-save-button:hover {
|
||||
background-color: #f09e9e;
|
||||
background-color: var(--color-mint);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.draft-edit-button {
|
||||
background-color: #e8f0fe;
|
||||
color: #1a73e8;
|
||||
background-color: var(--state-hover);
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.draft-edit-button:hover {
|
||||
background-color: #d2e3fc;
|
||||
background-color: var(--state-active);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
@ -4,7 +4,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
background-color: var(--overlay-dark);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -14,14 +14,14 @@
|
||||
}
|
||||
|
||||
.loading-overlay.transparent-background {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
background-color: var(--overlay-light);
|
||||
}
|
||||
|
||||
.loading-overlay-content {
|
||||
padding: 2rem;
|
||||
border-radius: 1rem;
|
||||
background-color: white;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
background-color: var(--bg-primary);
|
||||
box-shadow: 0 10px 25px var(--shadow-color);
|
||||
animation: scaleInOverlay 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
background-color: var(--overlay-dark);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
color: #F2ADAD;
|
||||
color: var(--color-accent);
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
animation: scaleIn 0.6s ease-out forwards 0.2s;
|
||||
@ -47,7 +47,7 @@
|
||||
.loading-spinner-message {
|
||||
margin-top: 1rem;
|
||||
font-size: 1.2rem;
|
||||
color: #555;
|
||||
color: var(--color-slate);
|
||||
font-weight: 500;
|
||||
opacity: 0;
|
||||
animation: slideUp 0.6s ease-out forwards 0.3s;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
@import './lib/utils/colors.css';
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
@ -17,6 +18,8 @@ body {
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: var(--text-primary);
|
||||
background-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
code {
|
||||
|
||||
@ -86,4 +86,66 @@ export class DateUtils {
|
||||
result.setFullYear(result.getFullYear() + years);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date to a human-readable date string (e.g., "Monday, January 1, 2023")
|
||||
*/
|
||||
export const formatDate = (date: Date): string => {
|
||||
return date.toLocaleDateString(undefined, {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a date to a time string (e.g., "9:00 AM")
|
||||
*/
|
||||
export const formatTime = (date: Date): string => {
|
||||
return date.toLocaleTimeString(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a date range for display
|
||||
*/
|
||||
export const formatDateRange = (start: Date, end: Date, allDay = false): string => {
|
||||
// Same day
|
||||
if (start.toDateString() === end.toDateString()) {
|
||||
if (allDay) {
|
||||
return formatDate(start);
|
||||
}
|
||||
return `${formatDate(start)}, ${formatTime(start)} - ${formatTime(end)}`;
|
||||
}
|
||||
|
||||
// Different days
|
||||
if (allDay) {
|
||||
return `${formatDate(start)} - ${formatDate(end)}`;
|
||||
}
|
||||
return `${formatDate(start)}, ${formatTime(start)} - ${formatDate(end)}, ${formatTime(end)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a short formatted date (e.g., "Jan 1")
|
||||
*/
|
||||
export const formatShortDate = (date: Date): string => {
|
||||
return date.toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if two dates are on the same day
|
||||
*/
|
||||
export const isSameDay = (date1: Date, date2: Date): boolean => {
|
||||
return (
|
||||
date1.getFullYear() === date2.getFullYear() &&
|
||||
date1.getMonth() === date2.getMonth() &&
|
||||
date1.getDate() === date2.getDate()
|
||||
);
|
||||
};
|
||||
51
frontend/src/lib/utils/colors.css
Normal file
51
frontend/src/lib/utils/colors.css
Normal file
@ -0,0 +1,51 @@
|
||||
:root {
|
||||
/* Color Palette */
|
||||
--color-dark: #222222;
|
||||
--color-white: #FFFFFF;
|
||||
--color-light: #F5F5F5;
|
||||
--color-indigo: #4B4E6D;
|
||||
--color-mint: #84DCC6;
|
||||
--color-slate: #95A3B3;
|
||||
|
||||
/* 60-30-10 Rule Application */
|
||||
--color-primary: var(--color-white); /* 60% - dominant */
|
||||
--color-secondary: var(--color-indigo); /* 30% - secondary */
|
||||
--color-accent: var(--color-mint); /* 10% - accent */
|
||||
|
||||
/* Text Colors */
|
||||
--text-primary: var(--color-dark);
|
||||
--text-secondary: var(--color-indigo);
|
||||
--text-on-dark: var(--color-white);
|
||||
|
||||
/* Background Colors */
|
||||
--bg-primary: var(--color-white);
|
||||
--bg-secondary: var(--color-slate);
|
||||
--bg-tertiary: var(--color-indigo);
|
||||
|
||||
/* Border and Shadow Colors */
|
||||
--border-light: rgba(34, 34, 34, 0.1);
|
||||
--shadow-color: rgba(34, 34, 34, 0.08);
|
||||
--shadow-accent: rgba(132, 220, 198, 0.3);
|
||||
--shadow-accent-hover: rgba(132, 220, 198, 0.4);
|
||||
|
||||
/* Overlay Colors */
|
||||
--overlay-background: rgba(255, 255, 255, 0.8);
|
||||
--overlay-light: rgba(255, 255, 255, 0.5);
|
||||
--overlay-dark: rgba(255, 255, 255, 0.9);
|
||||
|
||||
/* State Colors */
|
||||
--state-hover: rgba(75, 78, 109, 0.08);
|
||||
--state-active: rgba(75, 78, 109, 0.15);
|
||||
--state-focus: rgba(132, 220, 198, 0.3);
|
||||
|
||||
/* Calendar Event Colors */
|
||||
--event-default: var(--color-indigo);
|
||||
--event-highlight: var(--color-mint);
|
||||
--event-secondary: var(--color-slate);
|
||||
|
||||
/* Feedback Colors */
|
||||
--color-error: #e53e3e;
|
||||
--color-success: #38a169;
|
||||
--color-warning: #d69e2e;
|
||||
--color-info: #3182ce;
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
background-color: #ffffff;
|
||||
background-color: var(--bg-primary);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2rem;
|
||||
color: #000000;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.edit-event-form {
|
||||
@ -28,7 +28,7 @@
|
||||
.form-group label {
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
color: #333;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
@ -36,10 +36,10 @@
|
||||
.form-group input[type="date"],
|
||||
.form-group input[type="time"] {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
background-color: #f9f9f9;
|
||||
background-color: var(--bg-primary);
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
@ -48,8 +48,8 @@
|
||||
.form-group input[type="date"]:focus,
|
||||
.form-group input[type="time"]:focus {
|
||||
outline: none;
|
||||
border-color: #F2ADAD;
|
||||
box-shadow: 0 0 0 2px rgba(242, 173, 173, 0.2);
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 2px var(--state-focus);
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
@ -61,7 +61,7 @@
|
||||
.form-checkbox input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: #F2ADAD;
|
||||
accent-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
@ -91,22 +91,22 @@
|
||||
}
|
||||
|
||||
.save-button {
|
||||
background-color: #F2ADAD;
|
||||
color: #000000;
|
||||
background-color: var(--color-accent);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.save-button:hover {
|
||||
background-color: #f09e9e;
|
||||
background-color: var(--color-mint);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
background-color: #f0f0f0;
|
||||
color: #555;
|
||||
background-color: var(--state-hover);
|
||||
color: var(--color-slate);
|
||||
}
|
||||
|
||||
.cancel-button:hover {
|
||||
background-color: #e5e5e5;
|
||||
background-color: var(--state-active);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
|
||||
116
frontend/src/pages/event-details/EventDetails.css
Normal file
116
frontend/src/pages/event-details/EventDetails.css
Normal file
@ -0,0 +1,116 @@
|
||||
.event-details-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.event-details-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.event-details-title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.back-button, .edit-button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.back-button:hover, .edit-button:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
background-color: #4285f4;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.edit-button:hover {
|
||||
background-color: #3367d6;
|
||||
}
|
||||
|
||||
.event-details-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.event-details-time {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.event-date, .event-time, .event-all-day {
|
||||
margin-bottom: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.event-all-day {
|
||||
font-weight: 500;
|
||||
color: #4285f4;
|
||||
}
|
||||
|
||||
.event-description {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.event-description h3 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.event-description p {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: #555;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.event-details-error {
|
||||
max-width: 600px;
|
||||
margin: 100px auto;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.event-details-error p {
|
||||
margin-bottom: 20px;
|
||||
font-size: 18px;
|
||||
color: #d93025;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.event-details-container {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.event-details-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
117
frontend/src/pages/event-details/EventDetails.tsx
Normal file
117
frontend/src/pages/event-details/EventDetails.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { getEvent, Event } from '../../lib/api/endpoints';
|
||||
import LoadingSpinner from '../../components/ui/LoadingSpinner';
|
||||
import { formatDate, formatTime } from '../../lib/utils/DateUtils';
|
||||
import './EventDetails.css';
|
||||
|
||||
const EventDetails: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [event, setEvent] = useState<Event | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchEventDetails = async () => {
|
||||
if (!id) {
|
||||
setError('Event ID is missing');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const eventData = await getEvent(id);
|
||||
setEvent(eventData);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setError('Failed to load event details');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchEventDetails();
|
||||
}, [id]);
|
||||
|
||||
const handleBack = () => {
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
if (event) {
|
||||
// Convert the Event to EventDraft format and store in session storage
|
||||
const eventDraft = {
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
description: event.description || '',
|
||||
start: event.start,
|
||||
end: event.end || null,
|
||||
allDay: event.allDay || false
|
||||
};
|
||||
|
||||
// Store the draft data in sessionStorage for use on the edit page
|
||||
sessionStorage.setItem('editingEventDraft', JSON.stringify(eventDraft));
|
||||
|
||||
// Navigate to the edit page
|
||||
navigate(`/edit-event/${event.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <LoadingSpinner message="Loading event details..." size="large" />;
|
||||
}
|
||||
|
||||
if (error || !event) {
|
||||
return (
|
||||
<div className="event-details-error">
|
||||
<p>{error || 'Event not found'}</p>
|
||||
<button className="back-button" onClick={handleBack}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const startDate = new Date(event.start);
|
||||
const endDate = event.end ? new Date(event.end) : null;
|
||||
|
||||
return (
|
||||
<div className="event-details-container">
|
||||
<div className="event-details-header">
|
||||
<button className="back-button" onClick={handleBack}>
|
||||
Back
|
||||
</button>
|
||||
<h1 className="event-details-title">{event.title}</h1>
|
||||
<button className="edit-button" onClick={handleEdit}>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="event-details-content">
|
||||
<div className="event-details-time">
|
||||
<div className="event-date">
|
||||
<strong>Date:</strong> {formatDate(startDate)}
|
||||
</div>
|
||||
{event.allDay ? (
|
||||
<div className="event-all-day">All day</div>
|
||||
) : (
|
||||
<div className="event-time">
|
||||
<strong>Time:</strong> {formatTime(startDate)}
|
||||
{endDate && ` - ${formatTime(endDate)}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{event.description && (
|
||||
<div className="event-description">
|
||||
<h3>Description</h3>
|
||||
<p>{event.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventDetails;
|
||||
1
frontend/src/pages/event-details/index.ts
Normal file
1
frontend/src/pages/event-details/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './EventDetails';
|
||||
@ -2,7 +2,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.25rem;
|
||||
background-color: #ffffff;
|
||||
background-color: var(--bg-primary);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
@ -23,7 +23,7 @@
|
||||
.greeting {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: #000000;
|
||||
color: var(--text-primary);
|
||||
line-height: 0.75em;
|
||||
font-family: 'Inter', sans-serif;
|
||||
margin: 0;
|
||||
@ -33,21 +33,21 @@
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #F2ADAD;
|
||||
color: #000000;
|
||||
background-color: var(--color-accent);
|
||||
color: var(--text-primary);
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 4px var(--shadow-color);
|
||||
transition: transform 0.2s, background-color 0.2s;
|
||||
}
|
||||
|
||||
.add-button:hover {
|
||||
transform: scale(1.05);
|
||||
background-color: #f09e9e;
|
||||
background-color: var(--color-mint);
|
||||
}
|
||||
|
||||
.add-button span {
|
||||
@ -105,13 +105,13 @@
|
||||
}
|
||||
|
||||
.textbox-container {
|
||||
background: white;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
max-width: 90%;
|
||||
position: relative;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
||||
box-shadow: 0 4px 20px var(--shadow-color);
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
@ -128,7 +128,7 @@
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 20px;
|
||||
resize: none;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 4px 12px var(--shadow-color);
|
||||
}
|
||||
|
||||
.close-button {
|
||||
@ -139,12 +139,12 @@
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
color: var(--color-slate);
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
color: #000;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.events-section {
|
||||
@ -155,7 +155,7 @@
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
color: #000000;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'Inter', sans-serif;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
@ -190,52 +190,54 @@
|
||||
}
|
||||
|
||||
.event-item {
|
||||
background-color: #F2ADAD;
|
||||
background-color: var(--color-light);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.125rem 1.25rem;
|
||||
color: #000000;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
box-shadow: 0 2px 8px rgba(242, 173, 173, 0.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 80px;
|
||||
transition: transform 0.2s, box-shadow 0.2s, border-left-width 0.2s;
|
||||
box-shadow: 0 2px 8px var(--shadow-light);
|
||||
border-left: 8px solid var(--color-accent);
|
||||
}
|
||||
|
||||
.event-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(242, 173, 173, 0.4);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 12px var(--shadow-color);
|
||||
border-left-width: 12px;
|
||||
}
|
||||
|
||||
.event-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
word-break: break-word;
|
||||
font-size: 1rem;
|
||||
line-height: 1.3;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.event-time {
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.9;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-primary);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.no-events {
|
||||
color: #888;
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
color: var(--color-slate);
|
||||
font-style: italic;
|
||||
padding: 1rem 0;
|
||||
background-color: var(--state-hover);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.loading, .error {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
font-size: 1.125rem;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
color: var(--color-slate);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #F2ADAD;
|
||||
color: var(--color-error, #e53e3e);
|
||||
}
|
||||
|
||||
.draft-loading-overlay {
|
||||
@ -244,37 +246,35 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
background-color: var(--overlay-background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
backdrop-filter: blur(3px);
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.greeting {
|
||||
font-size: 1.75rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.events-section {
|
||||
margin-bottom: 2rem;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.tomorrow-events {
|
||||
flex-direction: column;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tomorrow-events .event-item {
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
min-width: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -284,11 +284,11 @@
|
||||
}
|
||||
|
||||
.greeting {
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.tomorrow-events .event-item {
|
||||
width: 100%;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.week-events {
|
||||
@ -299,6 +299,6 @@
|
||||
/* Ensure scrolling works properly within the TabView */
|
||||
@media (max-height: 700px) {
|
||||
.home-container {
|
||||
padding-bottom: 6.25rem;
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState, useRef, KeyboardEvent } from 'react';
|
||||
import { getEvents, Event, generateDraft, EventDraft, GenerateDraftRequest } from '../../lib/api/endpoints';
|
||||
import { getEvents, Event, generateDraft, EventDraft, GenerateDraftRequest, persistEvent, PersistEventRequest } from '../../lib/api/endpoints';
|
||||
import LoadingSpinner from '../../components/ui/LoadingSpinner';
|
||||
import EventDraftCard from '../../components/ui/EventDraftCard';
|
||||
import { useUser } from '../../lib/context';
|
||||
@ -20,6 +20,7 @@ const Home: React.FC = () => {
|
||||
const [inputText, setInputText] = useState('');
|
||||
const [isDraftLoading, setIsDraftLoading] = useState(false);
|
||||
const [eventDraft, setEventDraft] = useState<EventDraft | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const textInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@ -135,8 +136,43 @@ const Home: React.FC = () => {
|
||||
navigate(`/edit-event/${draft.id}`);
|
||||
};
|
||||
|
||||
const saveEvent = async (draft: EventDraft) => {
|
||||
try {
|
||||
setIsSaving(true);
|
||||
const request: PersistEventRequest = { draft };
|
||||
const savedEvent = await persistEvent(request);
|
||||
|
||||
// Add the new event to the appropriate list
|
||||
const eventDate = new Date(savedEvent.start);
|
||||
eventDate.setHours(0, 0, 0, 0);
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
const weekEnd = new Date(today);
|
||||
weekEnd.setDate(weekEnd.getDate() + 7);
|
||||
|
||||
if (eventDate.getTime() === today.getTime()) {
|
||||
setTodayEvents(prev => [...prev, savedEvent]);
|
||||
} else if (eventDate.getTime() === tomorrow.getTime()) {
|
||||
setTomorrowEvents(prev => [...prev, savedEvent]);
|
||||
} else if (eventDate > today && eventDate <= weekEnd) {
|
||||
setWeekEvents(prev => [...prev, savedEvent]);
|
||||
}
|
||||
|
||||
setIsSaving(false);
|
||||
handleDraftClose();
|
||||
} catch (err) {
|
||||
setError('Failed to save event');
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const EventItem = ({ event }: { event: Event }) => (
|
||||
<div className="event-item">
|
||||
<div className="event-item" onClick={() => navigate(`/event/${event.id}`)}>
|
||||
<div className="event-title">{event.title}</div>
|
||||
<div className="event-time">
|
||||
{new Date(event.start).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
@ -156,9 +192,9 @@ const Home: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="home-container">
|
||||
{isDraftLoading && (
|
||||
{(isDraftLoading || isSaving) && (
|
||||
<div className="draft-loading-overlay">
|
||||
<LoadingSpinner message="Termin wird erstellt..." size="medium" />
|
||||
<LoadingSpinner message={isSaving ? "Termin wird gespeichert..." : "Termin wird erstellt..."} size="medium" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -167,19 +203,7 @@ const Home: React.FC = () => {
|
||||
draft={eventDraft}
|
||||
onClose={handleDraftClose}
|
||||
onEdit={handleEditDraft}
|
||||
onSave={() => {
|
||||
// For now we just close the card and log the data
|
||||
const eventData = {
|
||||
title: eventDraft.title,
|
||||
description: eventDraft.description,
|
||||
start: eventDraft.start || new Date().toISOString(),
|
||||
end: eventDraft.end || new Date().toISOString(),
|
||||
allDay: eventDraft.allDay
|
||||
};
|
||||
console.log('Saving event:', eventData);
|
||||
// Add actual implementation for saving the event
|
||||
handleDraftClose();
|
||||
}}
|
||||
onSave={() => saveEvent(eventDraft)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -230,8 +254,8 @@ const Home: React.FC = () => {
|
||||
</section>
|
||||
|
||||
{isTextboxOpen && (
|
||||
<div className={`textbox-overlay ${isClosing ? 'closing' : ''}`}>
|
||||
<div className={`textbox-container ${isClosing ? 'closing' : ''}`}>
|
||||
<div className={`textbox-overlay ${isClosing ? 'closing' : ''}`} onClick={handleCloseTextbox}>
|
||||
<div className={`textbox-container ${isClosing ? 'closing' : ''}`} onClick={(e) => e.stopPropagation()}>
|
||||
<button className="close-button" onClick={handleCloseTextbox}>×</button>
|
||||
<input
|
||||
ref={textInputRef}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user