feat(web): Memory (#2759)

* Add on this day

* add query for x year

* dev: add query

* dev: front end

* dev: styling

* styling

* more styling

* add new page

* navigating

* navigate back and forth

* styling

* show gallery

* fix test

* fix test

* show previous and next title

* fix test

* show up down scrolling button

* more styling

* styling

* fix app bar

* fix height of next/previous

* autoplay

* auto play

* refactor

* refactor

* refactor

* show date

* Navigate

* finish

* pr feedback
This commit is contained in:
Alex
2023-06-14 20:47:18 -05:00
committed by GitHub
parent 408fa45c51
commit 43ec0b77a0
27 changed files with 1033 additions and 10 deletions

View File

@@ -1534,6 +1534,50 @@
]
}
},
"/asset/memory-lane": {
"get": {
"operationId": "getMemoryLane",
"parameters": [
{
"name": "timezone",
"required": true,
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MemoryLaneResponseDto"
}
}
}
}
}
},
"tags": [
"Asset"
],
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
]
}
},
"/asset/search": {
"post": {
"operationId": "searchAsset",
@@ -5709,6 +5753,24 @@
"lon"
]
},
"MemoryLaneResponseDto": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"assets": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AssetResponseDto"
}
}
},
"required": [
"title",
"assets"
]
},
"OAuthCallbackDto": {
"type": "object",
"properties": {

View File

@@ -42,6 +42,7 @@ export enum WithProperty {
export const IAssetRepository = 'IAssetRepository';
export interface IAssetRepository {
getByDate(ownerId: string, date: Date): Promise<AssetEntity[]>;
getByIds(ids: string[]): Promise<AssetEntity[]>;
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
getWith(pagination: PaginationOptions, property: WithProperty): Paginated<AssetEntity>;

View File

@@ -2,7 +2,9 @@ import { Inject } from '@nestjs/common';
import { AuthUserDto } from '../auth';
import { IAssetRepository } from './asset.repository';
import { MapMarkerDto } from './dto/map-marker.dto';
import { MapMarkerResponseDto } from './response-dto';
import { MapMarkerResponseDto, mapAsset } from './response-dto';
import { MemoryLaneResponseDto } from './response-dto/memory-lane-response.dto';
import { DateTime } from 'luxon';
export class AssetService {
constructor(@Inject(IAssetRepository) private assetRepository: IAssetRepository) {}
@@ -10,4 +12,31 @@ export class AssetService {
getMapMarkers(authUser: AuthUserDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
return this.assetRepository.getMapMarkers(authUser.id, options);
}
async getMemoryLane(authUser: AuthUserDto, timezone: string): Promise<MemoryLaneResponseDto[]> {
const result: MemoryLaneResponseDto[] = [];
const luxonDate = DateTime.fromISO(new Date().toISOString(), { zone: timezone });
const today = new Date(luxonDate.year, luxonDate.month - 1, luxonDate.day);
const years = Array.from({ length: 30 }, (_, i) => {
const year = today.getFullYear() - i - 1;
return new Date(year, today.getMonth(), today.getDate());
});
for (const year of years) {
const assets = await this.assetRepository.getByDate(authUser.id, year);
if (assets.length > 0) {
const yearGap = today.getFullYear() - year.getFullYear();
const memory = new MemoryLaneResponseDto();
memory.title = `${yearGap} year${yearGap > 1 && 's'} since...`;
memory.assets = assets.map((a) => mapAsset(a));
result.push(memory);
}
}
return result;
}
}

View File

@@ -0,0 +1,7 @@
import { AssetResponseDto } from './asset-response.dto';
export class MemoryLaneResponseDto {
title!: string;
assets!: AssetResponseDto[];
}

View File

@@ -5,6 +5,7 @@ import { ApiTags } from '@nestjs/swagger';
import { GetAuthUser } from '../decorators/auth-user.decorator';
import { Authenticated } from '../decorators/authenticated.decorator';
import { UseValidation } from '../decorators/use-validation.decorator';
import { MemoryLaneResponseDto } from '@app/domain/asset/response-dto/memory-lane-response.dto';
@ApiTags('Asset')
@Controller('asset')
@@ -17,4 +18,12 @@ export class AssetController {
getMapMarkers(@GetAuthUser() authUser: AuthUserDto, @Query() options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
return this.service.getMapMarkers(authUser, options);
}
@Get('memory-lane')
getMemoryLane(
@GetAuthUser() authUser: AuthUserDto,
@Query('timezone') timezone: string,
): Promise<MemoryLaneResponseDto[]> {
return this.service.getMemoryLane(authUser, timezone);
}
}

View File

@@ -20,6 +20,40 @@ import { paginate } from '../utils/pagination.util';
export class AssetRepository implements IAssetRepository {
constructor(@InjectRepository(AssetEntity) private repository: Repository<AssetEntity>) {}
getByDate(ownerId: string, date: Date): Promise<AssetEntity[]> {
// For reference of a correct approach althought slower
// let builder = this.repository
// .createQueryBuilder('asset')
// .leftJoin('asset.exifInfo', 'exifInfo')
// .where('asset.ownerId = :ownerId', { ownerId })
// .andWhere(
// `coalesce(date_trunc('day', asset."fileCreatedAt", "exifInfo"."timeZone") at TIME ZONE "exifInfo"."timeZone", date_trunc('day', asset."fileCreatedAt")) IN (:date)`,
// { date },
// )
// .andWhere('asset.isVisible = true')
// .andWhere('asset.isArchived = false')
// .orderBy('asset.fileCreatedAt', 'DESC');
// return builder.getMany();
const tomorrow = new Date(date.getTime() + 24 * 60 * 60 * 1000);
return this.repository.find({
where: {
ownerId,
isVisible: true,
isArchived: false,
fileCreatedAt: OptionalBetween(date, tomorrow),
},
relations: {
exifInfo: true,
},
order: {
fileCreatedAt: 'DESC',
},
});
}
getByIds(ids: string[]): Promise<AssetEntity[]> {
return this.repository.find({
where: { id: In(ids) },

View File

@@ -2,6 +2,7 @@ import { IAssetRepository } from '@app/domain';
export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
return {
getByDate: jest.fn(),
getByIds: jest.fn().mockResolvedValue([]),
getWithout: jest.fn(),
getWith: jest.fn(),