Skip to content

Commit a53e18d

Browse files
committed
✨ Trip archive feature, 💄 Trip mobile UI menu
1 parent 7b9d614 commit a53e18d

File tree

6 files changed

+157
-33
lines changed

6 files changed

+157
-33
lines changed

backend/trip/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.0.0"
1+
__version__ = "1.1.0"

backend/trip/routers/trips.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ def update_trip(
7676
db_trip = session.get(Trip, trip_id)
7777
verify_exists_and_owns(current_user, db_trip)
7878

79+
if db_trip.archived and (trip.archived is not False):
80+
raise HTTPException(status_code=400, detail="Bad request")
81+
7982
trip_data = trip.model_dump(exclude_unset=True)
8083
if trip_data.get("image"):
8184
try:
@@ -136,6 +139,9 @@ def delete_trip(
136139
db_trip = session.get(Trip, trip_id)
137140
verify_exists_and_owns(current_user, db_trip)
138141

142+
if db_trip.archived:
143+
raise HTTPException(status_code=400, detail="Bad request")
144+
139145
if db_trip.image:
140146
try:
141147
remove_image(db_trip.image.filename)
@@ -161,6 +167,9 @@ def create_tripday(
161167
db_trip = session.get(Trip, trip_id)
162168
verify_exists_and_owns(current_user, db_trip)
163169

170+
if db_trip.archived:
171+
raise HTTPException(status_code=400, detail="Bad request")
172+
164173
new_day = TripDay(label=td.label, trip_id=trip_id, user=current_user)
165174

166175
session.add(new_day)
@@ -180,6 +189,9 @@ def update_tripday(
180189
db_trip = session.get(Trip, trip_id)
181190
verify_exists_and_owns(current_user, db_trip)
182191

192+
if db_trip.archived:
193+
raise HTTPException(status_code=400, detail="Bad request")
194+
183195
db_day = session.get(TripDay, day_id)
184196
verify_exists_and_owns(current_user, db_day)
185197
if db_day.trip_id != trip_id:
@@ -205,6 +217,9 @@ def delete_tripday(
205217
db_trip = session.get(Trip, trip_id)
206218
verify_exists_and_owns(current_user, db_trip)
207219

220+
if db_trip.archived:
221+
raise HTTPException(status_code=400, detail="Bad request")
222+
208223
db_day = session.get(TripDay, day_id)
209224
verify_exists_and_owns(current_user, db_day)
210225
if db_day.trip_id != trip_id:
@@ -226,6 +241,9 @@ def create_tripitem(
226241
db_trip = session.get(Trip, trip_id)
227242
verify_exists_and_owns(current_user, db_trip)
228243

244+
if db_trip.archived:
245+
raise HTTPException(status_code=400, detail="Bad request")
246+
229247
db_day = session.get(TripDay, day_id)
230248
if db_day.trip_id != trip_id:
231249
raise HTTPException(status_code=400, detail="Bad request")
@@ -265,6 +283,9 @@ def update_tripitem(
265283
db_trip = session.get(Trip, trip_id)
266284
verify_exists_and_owns(current_user, db_trip)
267285

286+
if db_trip.archived:
287+
raise HTTPException(status_code=400, detail="Bad request")
288+
268289
db_day = session.get(TripDay, day_id)
269290
if db_day.trip_id != trip_id:
270291
raise HTTPException(status_code=400, detail="Bad request")
@@ -303,6 +324,9 @@ def delete_tripitem(
303324
db_trip = session.get(Trip, trip_id)
304325
verify_exists_and_owns(current_user, db_trip)
305326

327+
if db_trip.archived:
328+
raise HTTPException(status_code=400, detail="Bad request")
329+
306330
db_day = session.get(TripDay, day_id)
307331
if db_day.trip_id != trip_id:
308332
raise HTTPException(status_code=400, detail="Bad request")

src/src/app/components/trip/trip.component.html

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,38 @@ <h1 class="font-medium tracking-tight text-2xl truncate">{{ trip?.name }}</h1>
1212
<img src="favicon.png" class="size-20">
1313
<div class="flex gap-2 items-center text-xs text-gray-500"><i class="pi pi-github"></i>itskovacs/trip</div>
1414
</div>
15-
<div class="flex flex-col md:flex-row items-center gap-2 print:hidden">
16-
<div>
15+
<div class="flex items-center gap-2 print:hidden">
16+
@if (!trip?.archived) {
17+
<div class="hidden md:flex items-center gap-2">
18+
<p-button text (click)="toggleArchiveTrip()" icon="pi pi-box" severity="warn" />
19+
<div class="border-l border-solid border-gray-700 h-4"></div>
1720
<p-button text (click)="deleteTrip()" icon="pi pi-trash" severity="danger" />
1821
<p-button text (click)="editTrip()" icon="pi pi-pencil" />
1922
</div>
20-
<div>
21-
<span class="bg-gray-100 text-gray-800 text-xs md:text-sm font-medium me-2 px-2.5 py-0.5 rounded">{{ totalPrice
22-
|| '-' }} {{ currency$ | async }}</span>
23+
24+
<div class="flex md:hidden">
25+
<p-button (click)="menu.toggle($event)" severity="secondary" text icon="pi pi-ellipsis-h" />
26+
<p-menu #menu [model]="menuItems" [popup]="true" />
2327
</div>
28+
}
29+
30+
<span class="bg-gray-100 text-gray-800 text-xs md:text-sm font-medium me-2 px-2.5 py-0.5 rounded min-w-fit">{{
31+
totalPrice
32+
|| '-' }} {{ currency$ | async }}</span>
2433
</div>
2534
</div>
2635
</section>
2736

37+
@if (trip?.archived) {
38+
<div class="mx-auto p-4 my-4 w-fit max-w-[400px] text-center text-orange-800 rounded-lg bg-orange-50">
39+
<div class="flex items-center justify-between">
40+
<div class="font-semibold">Archived</div>
41+
<p-button text icon="pi pi-box" label="Restore" (click)="toggleArchiveTrip()" [size]="'small'" />
42+
</div>
43+
This Trip is archived, you cannot modify it.
44+
</div>
45+
}
46+
2847
<section class="p-4 print:px-1 grid md:grid-cols-3 gap-4 print:block">
2948
<div class="p-4 shadow rounded-md md:col-span-2 max-w-screen print:col-span-full">
3049
<div class="p-2 mb-2 flex justify-between items-center">
@@ -36,8 +55,8 @@ <h1 class="font-semibold tracking-tight text-xl">Plans</h1>
3655
<div class="flex items-center gap-2 print:hidden">
3756
<p-button icon="pi pi-print" (click)="printTable()" text />
3857
<div class="border-l border-solid border-gray-700 h-4"></div>
39-
<p-button icon="pi pi-ellipsis-v" (click)="addItems()" text />
40-
<p-button icon="pi pi-plus" (click)="addItem()" text />
58+
<p-button icon="pi pi-ellipsis-v" [disabled]="trip?.archived" (click)="addItems()" text />
59+
<p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="addItem()" text />
4160
</div>
4261
</div>
4362

@@ -104,7 +123,7 @@ <h2 class="mb-0 text-4xl text-center tracking-tight font-extrabold text-gray-900
104123
Add <i>Day</i> to your <i>Trip</i> to start organizing !
105124
</p>
106125

107-
<p-button styleClass="mt-4" label="Add" icon="pi pi-plus" (click)="addDay()" text />
126+
<p-button styleClass="mt-4" label="Add" icon="pi pi-plus" [disabled]="trip?.archived" (click)="addDay()" text />
108127
</div>
109128
</div>
110129
<div class="hidden print:block text-center text-sm text-gray-500 mt-4">
@@ -130,9 +149,10 @@ <h2 class="mb-0 text-4xl text-center tracking-tight font-extrabold text-gray-900
130149

131150
<h2 class="text-xl md:text-3xl font-semibold mb-0 truncate max-w-96 md:mx-auto">{{ selectedItem.text }}</h2>
132151
<div class="flex items-center gap-2">
133-
<p-button icon="pi pi-trash" severity="danger" (click)="deleteItem(selectedItem)" text />
134-
<p-button icon="pi pi-pencil" (click)="editItem(selectedItem)" text />
135-
<p-button icon="pi pi-times" (click)="selectedItem = undefined" text />
152+
<p-button icon="pi pi-trash" [disabled]="trip?.archived" severity="danger" (click)="deleteItem(selectedItem)"
153+
text />
154+
<p-button icon="pi pi-pencil" [disabled]="trip?.archived" (click)="editItem(selectedItem)" text />
155+
<p-button icon="pi pi-times" [disabled]="trip?.archived" (click)="selectedItem = undefined" text />
136156
</div>
137157
</div>
138158

@@ -201,7 +221,7 @@ <h1 class="font-semibold tracking-tight text-xl">Days</h1>
201221
<span class="text-xs text-gray-500 line-clamp-1">{{ trip?.name }} days</span>
202222
</div>
203223

204-
<p-button icon="pi pi-plus" (click)="addDay()" text />
224+
<p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="addDay()" text />
205225
</div>
206226

207227
<div class="max-h-[20vh] overflow-y-auto">
@@ -216,13 +236,13 @@ <h1 class="font-semibold tracking-tight text-xl">Days</h1>
216236
getDayStats(d).places }}</span>
217237
</div>
218238
<div class="hidden group-hover:flex gap-2 items-center">
219-
<p-button icon="pi pi-trash" severity="danger" (click)="deleteDay(d)" text />
220-
<p-button icon="pi pi-pencil" (click)="editDay(d)" label="Edit" text />
221-
<p-button icon="pi pi-plus" (click)="addItem(d.id)" label="Item" text />
239+
<p-button icon="pi pi-trash" severity="danger" [disabled]="trip?.archived" (click)="deleteDay(d)" text />
240+
<p-button icon="pi pi-pencil" [disabled]="trip?.archived" (click)="editDay(d)" label="Edit" text />
241+
<p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="addItem(d.id)" label="Item" text />
222242
</div>
223243
</div>
224244
} @empty {
225-
<p-button label="Add" icon="pi pi-plus" (click)="addDay()" text />
245+
<p-button label="Add" icon="pi pi-plus" [disabled]="trip?.archived" (click)="addDay()" text />
226246
}
227247
} @placeholder (minimum 0.4s) {
228248
<div class="h-16">
@@ -245,7 +265,7 @@ <h1 class="font-semibold tracking-tight text-xl">Places</h1>
245265
} @placeholder (minimum 0.4s) {
246266
<p-skeleton height="1.75rem" width="2.5rem" class="mr-1" />
247267
}
248-
<p-button icon="pi pi-plus" (click)="manageTripPlaces()" text />
268+
<p-button icon="pi pi-plus" [disabled]="trip?.archived" (click)="manageTripPlaces()" text />
249269
</div>
250270
</div>
251271

@@ -280,7 +300,7 @@ <h1 class="tracking-tight truncate">{{ p.name }}</h1>
280300
</div>
281301
</div>
282302
} @empty {
283-
<p-button label="Add" icon="pi pi-plus" (click)="manageTripPlaces()" text />
303+
<p-button label="Add" icon="pi pi-plus" [disabled]="trip?.archived" (click)="manageTripPlaces()" text />
284304
}
285305
} @placeholder (minimum 0.4s) {
286306
<div class="flex flex-col gap-4">

src/src/app/components/trip/trip.component.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import { YesNoModalComponent } from "../../modals/yes-no-modal/yes-no-modal.comp
2727
import { UtilsService } from "../../services/utils.service";
2828
import { TripCreateModalComponent } from "../../modals/trip-create-modal/trip-create-modal.component";
2929
import { AsyncPipe } from "@angular/common";
30+
import { MenuItem } from "primeng/api";
31+
import { MenuModule } from "primeng/menu";
3032

3133
interface PlaceWithUsage extends Place {
3234
placeUsage?: boolean;
@@ -38,6 +40,7 @@ interface PlaceWithUsage extends Place {
3840
imports: [
3941
FormsModule,
4042
SkeletonModule,
43+
MenuModule,
4144
ReactiveFormsModule,
4245
InputTextModule,
4346
AsyncPipe,
@@ -62,6 +65,7 @@ export class TripComponent implements AfterViewInit {
6265

6366
places: PlaceWithUsage[] = [];
6467
flattenedTripItems: FlattenedTripItem[] = [];
68+
menuItems: MenuItem[] = [];
6569

6670
constructor(
6771
private apiService: ApiService,
@@ -72,6 +76,38 @@ export class TripComponent implements AfterViewInit {
7276
) {
7377
this.currency$ = this.utilsService.currency$;
7478
this.statuses = this.utilsService.statuses;
79+
80+
this.menuItems = [
81+
{
82+
label: "Actions",
83+
items: [
84+
{
85+
label: "Edit",
86+
icon: "pi pi-pencil",
87+
iconClass: "text-blue-500!",
88+
command: () => {
89+
this.editTrip();
90+
},
91+
},
92+
{
93+
label: "Archive",
94+
icon: "pi pi-box",
95+
iconClass: "text-orange-500!",
96+
command: () => {
97+
this.toggleArchiveTrip();
98+
},
99+
},
100+
{
101+
label: "Delete",
102+
icon: "pi pi-trash",
103+
iconClass: "text-red-500!",
104+
command: () => {
105+
this.deleteTrip();
106+
},
107+
},
108+
],
109+
},
110+
];
75111
}
76112

77113
back() {
@@ -292,6 +328,33 @@ export class TripComponent implements AfterViewInit {
292328
});
293329
}
294330

331+
toggleArchiveTrip() {
332+
const currentArchiveStatus = this.trip?.archived;
333+
const modal = this.dialogService.open(YesNoModalComponent, {
334+
header: "Confirm Action",
335+
modal: true,
336+
closable: true,
337+
dismissableMask: true,
338+
breakpoints: {
339+
"640px": "90vw",
340+
},
341+
data: `${currentArchiveStatus ? "Restore" : "Archive"} ${this.trip?.name} ?${currentArchiveStatus ? "" : " This will make everything read-only."}`,
342+
});
343+
344+
modal.onClose.subscribe({
345+
next: (bool) => {
346+
if (bool)
347+
this.apiService
348+
.putTrip({ archived: !currentArchiveStatus }, this.trip?.id!)
349+
.subscribe({
350+
next: () => {
351+
this.trip!.archived = !currentArchiveStatus;
352+
},
353+
});
354+
},
355+
});
356+
}
357+
295358
addDay() {
296359
const modal: DynamicDialogRef = this.dialogService.open(
297360
TripCreateDayModalComponent,

src/src/app/components/trips/trips.component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
<div class="mt-10 grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
1313
@defer {
1414
@for (trip of trips; track trip.id) {
15-
<div class="group relative rounded-lg overflow-hidden shadow-lg cursor-pointer" (click)="viewTrip(trip.id)">
15+
<div class="group relative rounded-lg overflow-hidden shadow-lg cursor-pointer" [class.grayscale]="trip.archived"
16+
(click)="viewTrip(trip.id)">
1617
<img class="rounded-lg object-cover transform transition-transform duration-300 ease-in-out group-hover:scale-105"
1718
[src]="trip.image" />
1819

src/src/app/components/trips/trips.component.ts

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,33 +26,49 @@ export class TripsComponent {
2626
constructor(
2727
private apiService: ApiService,
2828
private dialogService: DialogService,
29-
private router: Router
29+
private router: Router,
3030
) {
3131
this.apiService.getTrips().subscribe({
32-
next: (trips) => (this.trips = trips),
32+
next: (trips) => {
33+
this.trips = trips;
34+
this.sortTrips();
35+
},
3336
});
3437
}
3538

3639
viewTrip(id: number) {
3740
this.router.navigateByUrl(`/trips/${id}`);
3841
}
3942

43+
sortTrips() {
44+
this.trips = this.trips.sort((a, b) => {
45+
if (!!a.archived !== !!b.archived) {
46+
return Number(!!a.archived) - Number(!!b.archived);
47+
}
48+
49+
return a.name.localeCompare(b.name);
50+
});
51+
}
52+
4053
gotoMap() {
4154
this.router.navigateByUrl("/");
4255
}
4356

4457
addTrip() {
45-
const modal: DynamicDialogRef = this.dialogService.open(TripCreateModalComponent, {
46-
header: "Create Place",
47-
modal: true,
48-
appendTo: "body",
49-
closable: true,
50-
dismissableMask: true,
51-
width: "30vw",
52-
breakpoints: {
53-
"640px": "90vw",
58+
const modal: DynamicDialogRef = this.dialogService.open(
59+
TripCreateModalComponent,
60+
{
61+
header: "Create Place",
62+
modal: true,
63+
appendTo: "body",
64+
closable: true,
65+
dismissableMask: true,
66+
width: "30vw",
67+
breakpoints: {
68+
"640px": "90vw",
69+
},
5470
},
55-
});
71+
);
5672

5773
modal.onClose.subscribe({
5874
next: (trip: TripBase | null) => {
@@ -61,7 +77,7 @@ export class TripsComponent {
6177
this.apiService.postTrip(trip).subscribe({
6278
next: (trip: TripBase) => {
6379
this.trips.push(trip);
64-
this.trips.sort((a, b) => a.name.localeCompare(b.name));
80+
this.sortTrips();
6581
},
6682
});
6783
},

0 commit comments

Comments
 (0)