Skip to content

Commit 469d434

Browse files
committed
feat: show events on map
1 parent 5e79bad commit 469d434

File tree

23 files changed

+520
-41
lines changed

23 files changed

+520
-41
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { LocationDTO } from "@/DTOs/location.dto";
2+
import { ApiProperty } from "@nestjs/swagger";
3+
4+
export class EventPublicDTO {
5+
@ApiProperty({
6+
type: "string",
7+
nullable: false,
8+
required: true,
9+
})
10+
startDate: string;
11+
12+
@ApiProperty({
13+
type: "string",
14+
nullable: false,
15+
required: true,
16+
})
17+
startTime: string;
18+
19+
@ApiProperty({
20+
type: "string",
21+
nullable: false,
22+
required: true,
23+
})
24+
endTime: string;
25+
26+
@ApiProperty({
27+
nullable: false,
28+
required: true,
29+
})
30+
venueWithArticleIfNeeded: string;
31+
32+
@ApiProperty({
33+
nullable: false,
34+
required: true,
35+
})
36+
address: string;
37+
38+
@ApiProperty({
39+
nullable: true,
40+
type: LocationDTO,
41+
description: "Location of event",
42+
})
43+
location: LocationDTO;
44+
}

backend/src/DTOs/new-event.dto.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { LocationDTO } from "@/DTOs/location.dto";
12
import { MultiLingStringDTO } from "@/DTOs/multi-ling-string.dto";
23
import { ApiProperty } from "@nestjs/swagger";
34

@@ -9,6 +10,13 @@ export class NewEventDTO {
910
})
1011
eventKey: string;
1112

13+
@ApiProperty({
14+
nullable: true,
15+
type: LocationDTO,
16+
description: "Location of event",
17+
})
18+
location: LocationDTO;
19+
1220
@ApiProperty({
1321
type: "string",
1422
format: "date-time",

backend/src/entities/event/event.controller.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { OnlyAdmin } from "@/auth/auth.guard";
2+
import { EventPublicDTO } from "@/DTOs/event-public.dto";
23
import { NewEventResponseDTO } from "@/DTOs/new-event-response.dto";
34
import { NewEventDTO } from "@/DTOs/new-event.dto";
45
import { NewTestEventDTO } from "@/DTOs/new-test-event.dto";
6+
import { ELanguage } from "@/types/user.types";
57
import {
68
Body,
79
Controller,
10+
Get,
811
Logger,
12+
Param,
913
Post,
1014
UsePipes,
1115
ValidationPipe,
@@ -14,6 +18,7 @@ import {
1418
ApiBody,
1519
ApiExcludeEndpoint,
1620
ApiOperation,
21+
ApiParam,
1722
ApiTags,
1823
} from "@nestjs/swagger";
1924
import { EventService } from "./event.service";
@@ -28,6 +33,26 @@ export class EventController {
2833

2934
constructor(private readonly eventService: EventService) {}
3035

36+
@Get(`events/:lang`)
37+
@ApiParam({
38+
name: "lang",
39+
type: "string",
40+
description: "Language to get event data in.",
41+
})
42+
@UsePipes(new ValidationPipe({ transform: true }))
43+
@ApiOperation({ summary: "Get upcoming and active events" })
44+
async getAllUpcomingEvents(
45+
@Param("lang") lang: ELanguage,
46+
): Promise<EventPublicDTO[]> {
47+
if (!lang) {
48+
this.logger.error(
49+
`Language not provided, MultiLingualStrings will be undefined!`,
50+
);
51+
}
52+
const events = await this.eventService.getAllUpcomingEvents();
53+
return events.map((event) => event.convertToPublicDTO(lang));
54+
}
55+
3156
@Post("admin/new-event")
3257
@OnlyAdmin()
3358
@ApiExcludeEndpoint()

backend/src/entities/event/event.entity.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,57 @@
1+
import { EventPublicDTO } from "@/DTOs/event-public.dto";
2+
import { BaseEntity } from "@/entities/base.entity";
13
import { MultilingualString } from "@/entities/multilingual-string/multilingual-string.entity";
24
import { TranslatableEntity } from "@/entities/translatable.entity";
3-
import { ChildEntity, Column } from "typeorm";
5+
import { ITranslatableEntityToDTOInterface } from "@/interfaces/IEntityToDTO.interface";
6+
import { ELanguage } from "@/types/user.types";
7+
import { formatMultiLanguageDateTimeStringsCET } from "@/utils/date.utils";
8+
import { getTypedCoordinatesFromPoint } from "@/utils/location.utils";
9+
import { Point } from "geojson";
10+
import { ChildEntity, Column, Index } from "typeorm";
411

512
@ChildEntity("Event")
6-
export class Event extends TranslatableEntity {
13+
export class Event
14+
extends TranslatableEntity
15+
implements BaseEntity, ITranslatableEntityToDTOInterface<EventPublicDTO>
16+
{
717
readonly LBL_VENUE_WITH_ARTICLE_IF_NEEDED = "venueWithArticleIfNeeded";
818
readonly LBL_ADDRESS = "address";
919

20+
public convertToPublicDTO(lang: ELanguage): EventPublicDTO {
21+
const { date: startDate, time: startTime } =
22+
formatMultiLanguageDateTimeStringsCET(
23+
this.eventStartDateTime,
24+
lang,
25+
);
26+
const { time: endTime } = formatMultiLanguageDateTimeStringsCET(
27+
this.eventEndDateTime,
28+
lang,
29+
);
30+
return {
31+
startDate,
32+
startTime,
33+
endTime,
34+
address: this.address.find((a) => a.language === lang)?.text,
35+
venueWithArticleIfNeeded: this.venueWithArticleIfNeeded.find(
36+
(a) => a.language === lang,
37+
)?.text,
38+
location: getTypedCoordinatesFromPoint(this.location),
39+
};
40+
}
41+
1042
/** @dev Used to re-address the event and avoiding duplicates */
1143
@Column({ nullable: false, unique: true })
1244
eventKey: string;
1345

46+
@Index({ spatial: true })
47+
@Column({
48+
type: "geography",
49+
spatialFeatureType: "Point",
50+
srid: 4326,
51+
nullable: false,
52+
})
53+
location: Point;
54+
1455
@Column({ nullable: false })
1556
eventStartDateTime: Date;
1657

backend/src/entities/event/event.service.ts

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { NotificationService } from "@/transient-services/notification/notificat
99
import { OfflineryNotification } from "@/types/notification-message.types";
1010
import { ELanguage } from "@/types/user.types";
1111
import { formatMultiLanguageDateTimeStringsCET } from "@/utils/date.utils";
12+
import { getPointFromTypedCoordinates } from "@/utils/location.utils";
1213
import { countExpoPushTicketStatuses } from "@/utils/misc.utils";
1314
import { MailerService } from "@nestjs-modules/mailer";
1415
import {
@@ -20,7 +21,7 @@ import {
2021
} from "@nestjs/common";
2122
import { InjectRepository } from "@nestjs/typeorm";
2223
import { I18nService } from "nestjs-i18n";
23-
import { Repository } from "typeorm";
24+
import { MoreThanOrEqual, Repository } from "typeorm";
2425
import { Event } from "./event.entity";
2526

2627
@Injectable()
@@ -39,6 +40,20 @@ export class EventService {
3940
private readonly mailService: MailerService,
4041
) {}
4142

43+
async getAllUpcomingEvents(): Promise<Event[]> {
44+
const now = new Date();
45+
46+
return this.eventRepository.find({
47+
where: {
48+
// @dev upcoming or currently active/running
49+
eventEndDateTime: MoreThanOrEqual(now),
50+
},
51+
order: {
52+
eventStartDateTime: "ASC",
53+
},
54+
});
55+
}
56+
4257
async createNewEvent(newEvent: NewEventDTO): Promise<NewEventResponseDTO> {
4358
let responseDTO: NewEventResponseDTO = {
4459
error: 0,
@@ -48,6 +63,36 @@ export class EventService {
4863
};
4964

5065
try {
66+
const newEventEntity: Event = this.eventRepository.create();
67+
await this.eventRepository.save(newEventEntity); // otherwise relationship constraints don't work
68+
newEventEntity.eventKey = newEvent.eventKey;
69+
newEventEntity.mapsLink = newEvent.mapsLink;
70+
newEventEntity.location = getPointFromTypedCoordinates(
71+
newEvent.location,
72+
);
73+
newEventEntity.venueWithArticleIfNeeded =
74+
await this.multilingualStringService.createTranslations(
75+
newEvent.venueWithArticleIfNeeded,
76+
newEventEntity.LBL_VENUE_WITH_ARTICLE_IF_NEEDED,
77+
newEventEntity.entityType,
78+
newEventEntity.id,
79+
);
80+
81+
newEventEntity.address =
82+
await this.multilingualStringService.createTranslations(
83+
newEvent.address,
84+
newEventEntity.LBL_ADDRESS,
85+
newEventEntity.entityType,
86+
newEventEntity.id,
87+
);
88+
newEventEntity.eventStartDateTime = newEvent.eventStartDateTime;
89+
newEventEntity.eventEndDateTime = newEvent.eventEndDateTime;
90+
await this.eventRepository.save(newEventEntity);
91+
92+
this.logger.debug(
93+
`Event ${newEventEntity.id} persisted. Notifying users now.`,
94+
);
95+
5196
const notifications: OfflineryNotification[] = [];
5297
const users = await this.userService.findAll(); // TODO: restrict by region and also only active accounts
5398

@@ -135,30 +180,6 @@ export class EventService {
135180
throw new InternalServerErrorException(err);
136181
}
137182

138-
const newEventEntity: Event = this.eventRepository.create();
139-
await this.eventRepository.save(newEventEntity); // otherwise relationship constraints don't work
140-
newEventEntity.eventKey = newEvent.eventKey;
141-
newEventEntity.venueWithArticleIfNeeded =
142-
await this.multilingualStringService.createTranslations(
143-
newEvent.venueWithArticleIfNeeded,
144-
newEventEntity.LBL_VENUE_WITH_ARTICLE_IF_NEEDED,
145-
newEventEntity.entityType,
146-
newEventEntity.id,
147-
);
148-
149-
newEventEntity.address =
150-
await this.multilingualStringService.createTranslations(
151-
newEvent.address,
152-
newEventEntity.LBL_ADDRESS,
153-
newEventEntity.entityType,
154-
newEventEntity.id,
155-
);
156-
newEventEntity.eventStartDateTime = newEvent.eventStartDateTime;
157-
newEventEntity.eventEndDateTime = newEvent.eventEndDateTime;
158-
await this.eventRepository.save(newEventEntity);
159-
160-
this.logger.debug(`Event ${newEventEntity.id} persisted.`);
161-
162183
return responseDTO;
163184
}
164185

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import { ELanguage } from "@/types/user.types";
2+
13
export interface IEntityToDTOInterface<T> {
24
convertToPublicDTO(): T;
35
}
6+
7+
export interface ITranslatableEntityToDTOInterface<T> {
8+
convertToPublicDTO(lang: ELanguage): T;
9+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class EventKey1738184967320 implements MigrationInterface {
4+
name = "EventKey1738184967320";
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(
8+
`ALTER TABLE "translatable_entity" ADD "eventKey" character varying`,
9+
);
10+
await queryRunner.query(
11+
`ALTER TABLE "translatable_entity" ADD CONSTRAINT "UQ_678d2f506dab1684c76207bac3e" UNIQUE ("eventKey")`,
12+
);
13+
}
14+
15+
public async down(queryRunner: QueryRunner): Promise<void> {
16+
await queryRunner.query(
17+
`ALTER TABLE "translatable_entity" DROP CONSTRAINT "UQ_678d2f506dab1684c76207bac3e"`,
18+
);
19+
await queryRunner.query(
20+
`ALTER TABLE "translatable_entity" DROP COLUMN "eventKey"`,
21+
);
22+
}
23+
}

backend/src/utils/date.utils.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,28 +32,28 @@ export const formatMultiLanguageDateTimeStringsCET = (
3232
) => {
3333
const locales =
3434
LOCALES_LANG_MAPPING[lang] ?? LOCALES_LANG_MAPPING[ELanguage.en];
35-
const cetTime = new Date(
36-
dateTime.toLocaleString(locales, { timeZone: DEFAULT_TIMEZONE_CET }),
37-
);
35+
36+
const cetTime = new Date(dateTime.getTime());
37+
cetTime.toLocaleString(locales, { timeZone: DEFAULT_TIMEZONE_CET });
3838

3939
// English formats
40-
const englishDateString = cetTime.toLocaleDateString(locales, {
40+
const dateString = cetTime.toLocaleDateString(locales, {
4141
timeZone: DEFAULT_TIMEZONE_CET,
4242
weekday: "long",
4343
year: "numeric",
4444
month: "long",
4545
day: "numeric",
4646
});
4747

48-
const englishTimeString = cetTime.toLocaleTimeString(locales, {
48+
const timeString = cetTime.toLocaleTimeString(locales, {
4949
timeZone: DEFAULT_TIMEZONE_CET,
5050
hour: "2-digit",
5151
minute: "2-digit",
5252
hour12: false,
5353
});
5454

5555
return {
56-
date: englishDateString,
57-
time: englishTimeString,
56+
date: dateString,
57+
time: timeString,
5858
};
5959
};

backend/src/utils/location.utils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,10 @@ export const getTypedCoordinatesFromPoint = (point: Point): LocationDTO => {
77
latitude: point.coordinates[1],
88
};
99
};
10+
11+
export const getPointFromTypedCoordinates = (location: LocationDTO): Point => {
12+
return {
13+
type: "Point",
14+
coordinates: [location.longitude, location.latitude],
15+
};
16+
};

backend/swagger.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)