BLOG-126 Post management (list and create) #139
@ -1,15 +1,15 @@
|
||||
import { AuthViewModel } from '$lib/auth/adapter/presenter/authViewModel';
|
||||
import { UserViewModel } from '$lib/auth/adapter/presenter/userViewModel';
|
||||
import type { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase';
|
||||
import { StateFactory, type AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
||||
import { AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
||||
import type { BaseStore } from '$lib/common/adapter/presenter/baseStore';
|
||||
import { captureException } from '@sentry/sveltekit';
|
||||
import { get, writable } from 'svelte/store';
|
||||
|
||||
export type AuthState = AsyncState<AuthViewModel>;
|
||||
export type AuthEvent = CurrentUserLoadedEvent;
|
||||
|
||||
export class AuthBloc {
|
||||
private readonly state = writable<AuthState>(StateFactory.idle());
|
||||
export class AuthLoadedStore implements BaseStore<AuthState> {
|
||||
private readonly state = writable<AuthState>(AsyncState.idle<AuthViewModel>(null));
|
||||
|
||||
constructor(private readonly getCurrentUserUseCase: GetCurrentUserUseCase) {}
|
||||
|
||||
@ -17,24 +17,21 @@ export class AuthBloc {
|
||||
return this.state.subscribe;
|
||||
}
|
||||
|
||||
async dispatch(event: AuthEvent): Promise<AuthState> {
|
||||
switch (event.event) {
|
||||
case AuthEventType.CurrentUserLoadedEvent:
|
||||
return this.loadCurrentUser();
|
||||
}
|
||||
get trigger() {
|
||||
return () => this.loadCurrentUser();
|
||||
}
|
||||
|
||||
private async loadCurrentUser(): Promise<AuthState> {
|
||||
this.state.set(StateFactory.loading(get(this.state).data));
|
||||
this.state.set(AsyncState.loading(get(this.state).data));
|
||||
|
||||
let result: AuthState;
|
||||
try {
|
||||
const user = await this.getCurrentUserUseCase.execute();
|
||||
const userViewModel = user ? UserViewModel.fromEntity(user) : null;
|
||||
const authViewModel = AuthViewModel.fromEntity(userViewModel);
|
||||
result = StateFactory.success(authViewModel);
|
||||
result = AsyncState.success(authViewModel);
|
||||
} catch (e) {
|
||||
result = StateFactory.error(e);
|
||||
result = AsyncState.error(e, get(this.state).data);
|
||||
captureException(e);
|
||||
}
|
||||
|
||||
@ -42,11 +39,3 @@ export class AuthBloc {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export enum AuthEventType {
|
||||
CurrentUserLoadedEvent,
|
||||
}
|
||||
|
||||
interface CurrentUserLoadedEvent {
|
||||
event: AuthEventType.CurrentUserLoadedEvent;
|
||||
}
|
@ -1,60 +1,110 @@
|
||||
export enum StatusType {
|
||||
Idle,
|
||||
Loading,
|
||||
Success,
|
||||
Error,
|
||||
enum AsyncStateStatus {
|
||||
Idle = 'idle',
|
||||
Loading = 'loading',
|
||||
Success = 'success',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
export type AsyncState<T> = IdleState<T> | LoadingState<T> | SuccessState<T> | ErrorState<T>;
|
||||
export abstract class AsyncState<T> {
|
||||
abstract readonly status: AsyncStateStatus;
|
||||
abstract readonly data: T | null;
|
||||
abstract readonly error: Error | null;
|
||||
|
||||
interface IdleState<T> {
|
||||
status: StatusType.Idle;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
interface LoadingState<T> {
|
||||
status: StatusType.Loading;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
interface SuccessState<T> {
|
||||
status: StatusType.Success;
|
||||
data: T;
|
||||
}
|
||||
|
||||
interface ErrorState<T> {
|
||||
status: StatusType.Error;
|
||||
data?: T;
|
||||
error: Error;
|
||||
}
|
||||
|
||||
export abstract class StateFactory {
|
||||
static idle<T>(data?: T): AsyncState<T> {
|
||||
return {
|
||||
status: StatusType.Idle,
|
||||
data,
|
||||
};
|
||||
static idle<T>(data: T | null): IdleState<T> {
|
||||
return new IdleState(data);
|
||||
}
|
||||
|
||||
static loading<T>(data?: T): AsyncState<T> {
|
||||
return {
|
||||
status: StatusType.Loading,
|
||||
data,
|
||||
};
|
||||
static loading<T>(data: T | null): LoadingState<T> {
|
||||
return new LoadingState(data);
|
||||
}
|
||||
|
||||
static success<T>(data: T): AsyncState<T> {
|
||||
return {
|
||||
status: StatusType.Success,
|
||||
data,
|
||||
};
|
||||
static success<T>(data: T): SuccessState<T> {
|
||||
return new SuccessState(data);
|
||||
}
|
||||
|
||||
static error<T>(error: unknown, data?: T): AsyncState<T> {
|
||||
return {
|
||||
status: StatusType.Error,
|
||||
error: error instanceof Error ? error : new Error('Unknown error'),
|
||||
data,
|
||||
};
|
||||
static error<T>(error: unknown, data: T | null): ErrorState<T> {
|
||||
const errorInstance = error instanceof Error ? error : new Error(String(error));
|
||||
return new ErrorState(errorInstance, data);
|
||||
}
|
||||
|
||||
isIdle(): this is IdleState<T> {
|
||||
return this.status === AsyncStateStatus.Idle;
|
||||
}
|
||||
|
||||
isLoading(): this is LoadingState<T> {
|
||||
return this.status === AsyncStateStatus.Loading;
|
||||
}
|
||||
|
||||
isSuccess(): this is SuccessState<T> {
|
||||
return this.status === AsyncStateStatus.Success;
|
||||
}
|
||||
|
||||
isError(): this is ErrorState<T> {
|
||||
return this.status === AsyncStateStatus.Error;
|
||||
}
|
||||
}
|
||||
|
||||
class IdleState<T> extends AsyncState<T> {
|
||||
readonly status = AsyncStateStatus.Idle;
|
||||
readonly data: T | null;
|
||||
readonly error = null;
|
||||
|
||||
constructor(data: T | null) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
toLoading(): LoadingState<T> {
|
||||
return new LoadingState(this.data);
|
||||
}
|
||||
}
|
||||
|
||||
class LoadingState<T> extends AsyncState<T> {
|
||||
readonly status = AsyncStateStatus.Loading;
|
||||
readonly data: T | null;
|
||||
readonly error = null;
|
||||
|
||||
constructor(data: T | null) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
toSuccess(data: T): SuccessState<T> {
|
||||
return new SuccessState(data);
|
||||
}
|
||||
|
||||
toError(error: Error): ErrorState<T> {
|
||||
return new ErrorState(error, this.data);
|
||||
}
|
||||
}
|
||||
|
||||
class SuccessState<T> extends AsyncState<T> {
|
||||
readonly status = AsyncStateStatus.Success;
|
||||
readonly data: T;
|
||||
readonly error = null;
|
||||
|
||||
constructor(data: T) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
toLoading(): LoadingState<T> {
|
||||
return new LoadingState(this.data);
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorState<T> extends AsyncState<T> {
|
||||
readonly status = AsyncStateStatus.Error;
|
||||
readonly data: T | null;
|
||||
readonly error: Error;
|
||||
|
||||
constructor(error: Error, data: T | null) {
|
||||
super();
|
||||
this.error = error;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
toLoading(): LoadingState<T> {
|
||||
return new LoadingState(this.data);
|
||||
}
|
||||
}
|
||||
|
7
frontend/src/lib/common/adapter/presenter/baseStore.ts
Normal file
7
frontend/src/lib/common/adapter/presenter/baseStore.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
||||
import type { Readable } from 'svelte/store';
|
||||
|
||||
export interface BaseStore<T extends AsyncState<unknown>, U = void> {
|
||||
get subscribe(): Readable<T>['subscribe'];
|
||||
get trigger(): (arg: U) => Promise<T>;
|
||||
}
|
@ -1,20 +1,21 @@
|
||||
import type { AuthApiService } from '$lib/auth/adapter/gateway/authApiService';
|
||||
import { AuthRepositoryImpl } from '$lib/auth/adapter/gateway/authRepositoryImpl';
|
||||
import { AuthBloc } from '$lib/auth/adapter/presenter/authBloc';
|
||||
import { AuthLoadedStore } from '$lib/auth/adapter/presenter/authLoadedStore';
|
||||
import type { AuthRepository } from '$lib/auth/application/gateway/authRepository';
|
||||
import { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase';
|
||||
import { AuthApiServiceImpl } from '$lib/auth/framework/api/authApiServiceImpl';
|
||||
import type { ImageApiService } from '$lib/image/adapter/gateway/imageApiService';
|
||||
import { ImageRepositoryImpl } from '$lib/image/adapter/gateway/imageRepositoryImpl';
|
||||
import { ImageBloc } from '$lib/image/adapter/presenter/imageBloc';
|
||||
import { ImageUploadedStore } from '$lib/image/adapter/presenter/ImageUploadedStore';
|
||||
import type { ImageRepository } from '$lib/image/application/gateway/imageRepository';
|
||||
import { UploadImageUseCase } from '$lib/image/application/useCase/uploadImageUseCase';
|
||||
import { ImageApiServiceImpl } from '$lib/image/framework/api/imageApiServiceImpl';
|
||||
import type { PostApiService } from '$lib/post/adapter/gateway/postApiService';
|
||||
import { PostRepositoryImpl } from '$lib/post/adapter/gateway/postRepositoryImpl';
|
||||
import { PostBloc } from '$lib/post/adapter/presenter/postBloc';
|
||||
import { PostCreatedStore } from '$lib/post/adapter/presenter/postCreatedStore';
|
||||
import type { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel';
|
||||
import { PostListBloc } from '$lib/post/adapter/presenter/postListBloc';
|
||||
import { PostsListedStore } from '$lib/post/adapter/presenter/postsListedStore';
|
||||
import { PostLoadedStore } from '$lib/post/adapter/presenter/PostLoadedStore';
|
||||
import type { PostViewModel } from '$lib/post/adapter/presenter/postViewModel';
|
||||
import type { PostRepository } from '$lib/post/application/gateway/postRepository';
|
||||
import { CreatePostUseCase } from '$lib/post/application/useCase/createPostUseCase';
|
||||
@ -31,20 +32,24 @@ export class Container {
|
||||
this.useCases = new UseCases(repositories);
|
||||
}
|
||||
|
||||
createAuthBloc(): AuthBloc {
|
||||
return new AuthBloc(this.useCases.getCurrentUserUseCase);
|
||||
createAuthLoadedStore(): AuthLoadedStore {
|
||||
return new AuthLoadedStore(this.useCases.getCurrentUserUseCase);
|
||||
}
|
||||
|
||||
createImageBloc(): ImageBloc {
|
||||
return new ImageBloc(this.useCases.uploadImageUseCase);
|
||||
createImageUploadedStore(): ImageUploadedStore {
|
||||
return new ImageUploadedStore(this.useCases.uploadImageUseCase);
|
||||
}
|
||||
|
||||
createPostListBloc(initialData?: readonly PostInfoViewModel[]): PostListBloc {
|
||||
return new PostListBloc(this.useCases.getAllPostsUseCase, initialData);
|
||||
createPostsListedStore(initialData?: readonly PostInfoViewModel[]): PostsListedStore {
|
||||
return new PostsListedStore(this.useCases.getAllPostsUseCase, initialData);
|
||||
}
|
||||
|
||||
createPostBloc(initialData?: PostViewModel): PostBloc {
|
||||
return new PostBloc(this.useCases.getPostUseCase, this.useCases.createPostUseCase, initialData);
|
||||
createPostLoadedStore(initialData?: PostViewModel): PostLoadedStore {
|
||||
return new PostLoadedStore(this.useCases.getPostUseCase, initialData);
|
||||
}
|
||||
|
||||
createPostCreatedStore(initialData?: PostViewModel): PostCreatedStore {
|
||||
return new PostCreatedStore(this.useCases.createPostUseCase, initialData);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { StateFactory, type AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
||||
import { AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
||||
import { ImageInfoViewModel } from '$lib/image/adapter/presenter/imageInfoViewModel';
|
||||
import type { UploadImageUseCase } from '$lib/image/application/useCase/uploadImageUseCase';
|
||||
import { captureException } from '@sentry/sveltekit';
|
||||
import { get, writable } from 'svelte/store';
|
||||
|
||||
export type ImageInfoState = AsyncState<ImageInfoViewModel>;
|
||||
export type ImageEvent = ImageUploadedEvent;
|
||||
|
||||
export class ImageBloc {
|
||||
private readonly state = writable<ImageInfoState>(StateFactory.idle());
|
||||
export class ImageUploadedStore {
|
||||
private readonly state = writable<ImageInfoState>(AsyncState.idle<ImageInfoViewModel>(null));
|
||||
|
||||
constructor(private readonly uploadImageUseCase: UploadImageUseCase) {}
|
||||
|
||||
@ -16,35 +15,23 @@ export class ImageBloc {
|
||||
return this.state.subscribe;
|
||||
}
|
||||
|
||||
async dispatch(event: ImageEvent): Promise<ImageInfoState> {
|
||||
switch (event.event) {
|
||||
case ImageEventType.ImageUploadedEvent:
|
||||
return this.uploadImage(event.file);
|
||||
}
|
||||
get trigger() {
|
||||
return (file: File) => this.uploadImage(file);
|
||||
}
|
||||
|
||||
private async uploadImage(file: File): Promise<ImageInfoState> {
|
||||
this.state.set(StateFactory.loading(get(this.state).data));
|
||||
this.state.set(AsyncState.loading(get(this.state).data));
|
||||
|
||||
let result: ImageInfoState;
|
||||
try {
|
||||
const imageInfo = await this.uploadImageUseCase.execute(file);
|
||||
const imageInfoViewModel = ImageInfoViewModel.fromEntity(imageInfo);
|
||||
result = StateFactory.success(imageInfoViewModel);
|
||||
result = AsyncState.success(imageInfoViewModel);
|
||||
} catch (e) {
|
||||
result = StateFactory.error(e);
|
||||
result = AsyncState.error(e, get(this.state).data);
|
||||
captureException(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export enum ImageEventType {
|
||||
ImageUploadedEvent,
|
||||
}
|
||||
|
||||
interface ImageUploadedEvent {
|
||||
event: ImageEventType.ImageUploadedEvent;
|
||||
file: File;
|
||||
}
|
@ -1,19 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import UploadImageDialoag from '$lib/image/framework/ui/UploadImageDialoag.svelte';
|
||||
import { ImageBloc, ImageEventType } from '$lib/image/adapter/presenter/imageBloc';
|
||||
import { StatusType } from '$lib/common/adapter/presenter/asyncState';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { ImageUploadedStore } from '$lib/image/adapter/presenter/ImageUploadedStore';
|
||||
|
||||
const imageBloc = getContext<ImageBloc>(ImageBloc.name);
|
||||
const state = $derived($imageBloc);
|
||||
|
||||
const isLoading = $derived(state.status === StatusType.Loading);
|
||||
const store = getContext<ImageUploadedStore>(ImageUploadedStore.name);
|
||||
const state = $derived($store);
|
||||
const { trigger: uploadImage } = store;
|
||||
|
||||
async function onUploadImageDialogSubmit(file: File) {
|
||||
const state = await imageBloc.dispatch({ event: ImageEventType.ImageUploadedEvent, file });
|
||||
const state = await uploadImage(file);
|
||||
|
||||
if (state.status === StatusType.Success) {
|
||||
if (state.isSuccess()) {
|
||||
const imageInfo = state.data;
|
||||
console.log('Image URL', imageInfo.url.href);
|
||||
|
||||
@ -30,7 +28,7 @@
|
||||
? 'The URL is copied to clipboard'
|
||||
: 'The URL is printed in console',
|
||||
});
|
||||
} else if (state.status === StatusType.Error) {
|
||||
} else if (state.isError()) {
|
||||
toast.error('Failed to upload image', {
|
||||
description: state.error?.message ?? 'Unknown error',
|
||||
});
|
||||
@ -41,7 +39,7 @@
|
||||
<div class="dashboard-container mb-10">
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<h1 class="py-16 text-5xl font-bold text-gray-800">Image</h1>
|
||||
<UploadImageDialoag disabled={isLoading} onSubmit={onUploadImageDialogSubmit} />
|
||||
<UploadImageDialoag disabled={state.isLoading()} onSubmit={onUploadImageDialogSubmit} />
|
||||
</div>
|
||||
<p>Gallery is currently unavailable.</p>
|
||||
</div>
|
||||
|
@ -36,9 +36,9 @@
|
||||
}
|
||||
|
||||
await uploadImage(file);
|
||||
close();
|
||||
files = undefined;
|
||||
fileInputErrorMessage = null;
|
||||
open = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -1,89 +0,0 @@
|
||||
import { StateFactory, type AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
||||
import { PostViewModel } from '$lib/post/adapter/presenter/postViewModel';
|
||||
import type { CreatePostParams } from '$lib/post/application/gateway/postRepository';
|
||||
import type { CreatePostUseCase } from '$lib/post/application/useCase/createPostUseCase';
|
||||
import type { GetPostUseCase } from '$lib/post/application/useCase/getPostUseCase';
|
||||
import { captureException } from '@sentry/sveltekit';
|
||||
import { get, writable } from 'svelte/store';
|
||||
|
||||
export type PostState = AsyncState<PostViewModel>;
|
||||
export type PostEvent = PostLoadedEvent | PostCreatedEvent;
|
||||
|
||||
export class PostBloc {
|
||||
private readonly state = writable<PostState>(StateFactory.idle());
|
||||
|
||||
constructor(
|
||||
private readonly getPostUseCase: GetPostUseCase,
|
||||
private readonly createPostUseCase: CreatePostUseCase,
|
||||
initialData?: PostViewModel
|
||||
) {
|
||||
this.state.set(StateFactory.idle(initialData));
|
||||
}
|
||||
|
||||
get subscribe() {
|
||||
return this.state.subscribe;
|
||||
}
|
||||
|
||||
async dispatch(event: PostEvent): Promise<PostState> {
|
||||
switch (event.event) {
|
||||
case PostEventType.PostLoadedEvent:
|
||||
return this.loadPost(event.id);
|
||||
case PostEventType.PostCreatedEvent:
|
||||
return this.createPost(event.params);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadPost(id: string): Promise<PostState> {
|
||||
this.state.set(StateFactory.loading(get(this.state).data));
|
||||
|
||||
let result: PostState;
|
||||
try {
|
||||
const post = await this.getPostUseCase.execute(id);
|
||||
if (!post) {
|
||||
result = StateFactory.error(new Error('Post not found'));
|
||||
this.state.set(result);
|
||||
return result;
|
||||
}
|
||||
const postViewModel = PostViewModel.fromEntity(post);
|
||||
result = StateFactory.success(postViewModel);
|
||||
} catch (e) {
|
||||
result = StateFactory.error(e);
|
||||
captureException(e);
|
||||
}
|
||||
|
||||
this.state.set(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async createPost(params: CreatePostParams): Promise<PostState> {
|
||||
this.state.set(StateFactory.loading(get(this.state).data));
|
||||
|
||||
let result: PostState;
|
||||
try {
|
||||
const post = await this.createPostUseCase.execute(params);
|
||||
const postViewModel = PostViewModel.fromEntity(post);
|
||||
result = StateFactory.success(postViewModel);
|
||||
} catch (e) {
|
||||
result = StateFactory.error(e);
|
||||
captureException(e);
|
||||
}
|
||||
|
||||
this.state.set(result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export enum PostEventType {
|
||||
PostLoadedEvent,
|
||||
PostCreatedEvent,
|
||||
}
|
||||
|
||||
interface PostLoadedEvent {
|
||||
event: PostEventType.PostLoadedEvent;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface PostCreatedEvent {
|
||||
event: PostEventType.PostCreatedEvent;
|
||||
params: CreatePostParams;
|
||||
}
|
47
frontend/src/lib/post/adapter/presenter/postCreatedStore.ts
Normal file
47
frontend/src/lib/post/adapter/presenter/postCreatedStore.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
||||
import type { BaseStore } from '$lib/common/adapter/presenter/baseStore';
|
||||
import { PostViewModel } from '$lib/post/adapter/presenter/postViewModel';
|
||||
import type { CreatePostParams } from '$lib/post/application/gateway/postRepository';
|
||||
import type { CreatePostUseCase } from '$lib/post/application/useCase/createPostUseCase';
|
||||
import { captureException } from '@sentry/sveltekit';
|
||||
import { get, writable } from 'svelte/store';
|
||||
|
||||
type PostState = AsyncState<PostViewModel>;
|
||||
|
||||
export class PostCreatedStore implements BaseStore<PostState, CreatePostParams> {
|
||||
private readonly state = writable<PostState>(AsyncState.idle<PostViewModel>(null));
|
||||
|
||||
constructor(
|
||||
private readonly createPostUseCase: CreatePostUseCase,
|
||||
initialData?: PostViewModel
|
||||
) {
|
||||
if (initialData) {
|
||||
this.state.set(AsyncState.idle(initialData));
|
||||
}
|
||||
}
|
||||
|
||||
get subscribe() {
|
||||
return this.state.subscribe;
|
||||
}
|
||||
|
||||
get trigger() {
|
||||
return (params: CreatePostParams) => this.createPost(params);
|
||||
}
|
||||
|
||||
private async createPost(params: CreatePostParams): Promise<PostState> {
|
||||
this.state.set(AsyncState.loading(get(this.state).data));
|
||||
|
||||
let result: PostState;
|
||||
try {
|
||||
const post = await this.createPostUseCase.execute(params);
|
||||
const postViewModel = PostViewModel.fromEntity(post);
|
||||
result = AsyncState.success(postViewModel);
|
||||
} catch (e) {
|
||||
result = AsyncState.error(e, get(this.state).data);
|
||||
captureException(e);
|
||||
}
|
||||
|
||||
this.state.set(result);
|
||||
return result;
|
||||
}
|
||||
}
|
51
frontend/src/lib/post/adapter/presenter/postLoadedStore.ts
Normal file
51
frontend/src/lib/post/adapter/presenter/postLoadedStore.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
||||
import type { BaseStore } from '$lib/common/adapter/presenter/baseStore';
|
||||
import { PostViewModel } from '$lib/post/adapter/presenter/postViewModel';
|
||||
import type { GetPostUseCase } from '$lib/post/application/useCase/getPostUseCase';
|
||||
import { captureException } from '@sentry/sveltekit';
|
||||
import { get, writable } from 'svelte/store';
|
||||
|
||||
export type PostState = AsyncState<PostViewModel>;
|
||||
|
||||
export class PostLoadedStore implements BaseStore<PostState, string> {
|
||||
private readonly state = writable<PostState>(AsyncState.idle<PostViewModel>(null));
|
||||
|
||||
constructor(
|
||||
private readonly getPostUseCase: GetPostUseCase,
|
||||
initialData?: PostViewModel
|
||||
) {
|
||||
if (initialData) {
|
||||
this.state.set(AsyncState.idle(initialData));
|
||||
}
|
||||
}
|
||||
|
||||
get subscribe() {
|
||||
return this.state.subscribe;
|
||||
}
|
||||
|
||||
get trigger() {
|
||||
return (id: string) => this.loadPost(id);
|
||||
}
|
||||
|
||||
private async loadPost(id: string): Promise<PostState> {
|
||||
this.state.set(AsyncState.loading(get(this.state).data));
|
||||
|
||||
let result: PostState;
|
||||
try {
|
||||
const post = await this.getPostUseCase.execute(id);
|
||||
if (!post) {
|
||||
result = AsyncState.error(new Error('Post not found'), get(this.state).data);
|
||||
this.state.set(result);
|
||||
return result;
|
||||
}
|
||||
const postViewModel = PostViewModel.fromEntity(post);
|
||||
result = AsyncState.success(postViewModel);
|
||||
} catch (e) {
|
||||
result = AsyncState.error(e, get(this.state).data);
|
||||
captureException(e);
|
||||
}
|
||||
|
||||
this.state.set(result);
|
||||
return result;
|
||||
}
|
||||
}
|
@ -1,52 +1,42 @@
|
||||
import {
|
||||
StateFactory,
|
||||
StatusType,
|
||||
type AsyncState,
|
||||
} from '$lib/common/adapter/presenter/asyncState';
|
||||
import { AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
||||
import type { BaseStore } from '$lib/common/adapter/presenter/baseStore';
|
||||
import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel';
|
||||
import type { GetAllPostsUseCase } from '$lib/post/application/useCase/getAllPostsUseCase';
|
||||
import { captureException } from '@sentry/sveltekit';
|
||||
import { get, writable } from 'svelte/store';
|
||||
|
||||
export type PostListState = AsyncState<readonly PostInfoViewModel[]>;
|
||||
export type PostListEvent = PostListLoadedEvent;
|
||||
|
||||
export class PostListBloc {
|
||||
private readonly state = writable<PostListState>({
|
||||
status: StatusType.Idle,
|
||||
});
|
||||
export class PostsListedStore implements BaseStore<PostListState> {
|
||||
private readonly state = writable<PostListState>(AsyncState.idle([]));
|
||||
|
||||
constructor(
|
||||
private readonly getAllPostsUseCase: GetAllPostsUseCase,
|
||||
initialData?: readonly PostInfoViewModel[]
|
||||
) {
|
||||
this.state.set({
|
||||
status: StatusType.Idle,
|
||||
data: initialData,
|
||||
});
|
||||
if (initialData) {
|
||||
this.state.set(AsyncState.idle(initialData));
|
||||
}
|
||||
}
|
||||
|
||||
get subscribe() {
|
||||
return this.state.subscribe;
|
||||
}
|
||||
|
||||
async dispatch(event: PostListEvent): Promise<PostListState> {
|
||||
switch (event.event) {
|
||||
case PostListEventType.PostListLoadedEvent:
|
||||
return this.loadPosts();
|
||||
}
|
||||
get trigger() {
|
||||
return () => this.loadPosts();
|
||||
}
|
||||
|
||||
private async loadPosts(): Promise<PostListState> {
|
||||
this.state.set(StateFactory.loading(get(this.state).data));
|
||||
this.state.set(AsyncState.loading(get(this.state).data));
|
||||
|
||||
let result: PostListState;
|
||||
try {
|
||||
const posts = await this.getAllPostsUseCase.execute();
|
||||
const postViewModels = posts.map((post) => PostInfoViewModel.fromEntity(post));
|
||||
result = StateFactory.success(postViewModels);
|
||||
result = AsyncState.success(postViewModels);
|
||||
} catch (e) {
|
||||
result = StateFactory.error(e);
|
||||
result = AsyncState.error(e, get(this.state).data);
|
||||
captureException(e);
|
||||
}
|
||||
|
||||
@ -54,11 +44,3 @@ export class PostListBloc {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export enum PostListEventType {
|
||||
PostListLoadedEvent,
|
||||
}
|
||||
|
||||
export interface PostListLoadedEvent {
|
||||
event: PostListEventType.PostListLoadedEvent;
|
||||
}
|
@ -1,22 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { PostBloc, PostEventType } from '$lib/post/adapter/presenter/postBloc';
|
||||
import PostContentHeader from '$lib/post/framework/ui/PostContentHeader.svelte';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
import markdownit from 'markdown-it';
|
||||
import SafeHtml from '$lib/common/framework/ui/SafeHtml.svelte';
|
||||
import generateTitle from '$lib/common/framework/ui/generateTitle';
|
||||
import StructuredData from '$lib/post/framework/ui/StructuredData.svelte';
|
||||
import { PostLoadedStore } from '$lib/post/adapter/presenter/PostLoadedStore';
|
||||
|
||||
const { id }: { id: string } = $props();
|
||||
|
||||
const postBloc = getContext<PostBloc>(PostBloc.name);
|
||||
const state = $derived($postBloc);
|
||||
const store = getContext<PostLoadedStore>(PostLoadedStore.name);
|
||||
const state = $derived($store);
|
||||
const { trigger: loadPost } = store;
|
||||
|
||||
const md = markdownit();
|
||||
const parsedContent = $derived(state.data?.content ? md.render(state.data.content) : '');
|
||||
const postInfo = $derived(state.data?.info);
|
||||
|
||||
onMount(() => postBloc.dispatch({ event: PostEventType.PostLoadedEvent, id: id }));
|
||||
onMount(() => loadPost(id));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
@ -1,25 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { StatusType } from '$lib/common/adapter/presenter/asyncState';
|
||||
import { PostBloc, PostEventType } from '$lib/post/adapter/presenter/postBloc';
|
||||
import { PostCreatedStore } from '$lib/post/adapter/presenter/postCreatedStore';
|
||||
import CreatePostDialog, {
|
||||
type CreatePostDialogFormParams,
|
||||
} from '$lib/post/framework/ui/CreatePostDialog.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const postBloc = getContext<PostBloc>(PostBloc.name);
|
||||
const state = $derived($postBloc);
|
||||
|
||||
const isLoading = $derived(state.status === StatusType.Loading);
|
||||
const store = getContext<PostCreatedStore>(PostCreatedStore.name);
|
||||
const state = $derived($store);
|
||||
const { trigger: createPost } = store;
|
||||
|
||||
async function onCreatePostDialogSubmit(params: CreatePostDialogFormParams) {
|
||||
const state = await postBloc.dispatch({ event: PostEventType.PostCreatedEvent, params });
|
||||
const state = await createPost(params);
|
||||
|
||||
if (state.status === StatusType.Success) {
|
||||
if (state.isSuccess()) {
|
||||
toast.success(`Post created successfully with ID: ${state.data.id}`);
|
||||
} else if (state.status === StatusType.Error) {
|
||||
} else if (state.isError()) {
|
||||
toast.error('Failed to create post', {
|
||||
description: state.error?.message ?? 'Unknown error',
|
||||
description: state.error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -28,7 +26,7 @@
|
||||
<div class="dashboard-container mb-10">
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<h1 class="py-16 text-5xl font-bold text-gray-800">Post</h1>
|
||||
<CreatePostDialog disabled={isLoading} onSubmit={onCreatePostDialogSubmit} />
|
||||
<CreatePostDialog disabled={state.isLoading()} onSubmit={onCreatePostDialogSubmit} />
|
||||
</div>
|
||||
<p></p>
|
||||
</div>
|
||||
|
@ -1,13 +1,14 @@
|
||||
<script lang="ts">
|
||||
import generateTitle from '$lib/common/framework/ui/generateTitle';
|
||||
import { PostListBloc, PostListEventType } from '$lib/post/adapter/presenter/postListBloc';
|
||||
import { PostsListedStore } from '$lib/post/adapter/presenter/postsListedStore';
|
||||
import PostPreview from '$lib/post/framework/ui/PostPreview.svelte';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
|
||||
const postListBloc = getContext<PostListBloc>(PostListBloc.name);
|
||||
const state = $derived($postListBloc);
|
||||
const store = getContext<PostsListedStore>(PostsListedStore.name);
|
||||
const state = $derived($store);
|
||||
const { trigger: loadPosts } = store;
|
||||
|
||||
onMount(() => postListBloc.dispatch({ event: PostListEventType.PostListLoadedEvent }));
|
||||
onMount(() => loadPosts());
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
@ -1,42 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { AuthBloc, AuthEventType } from '$lib/auth/adapter/presenter/authBloc';
|
||||
import { getContext, onMount, setContext } from 'svelte';
|
||||
import type { LayoutProps } from './$types';
|
||||
import { StatusType } from '$lib/common/adapter/presenter/asyncState';
|
||||
import ErrorPage from '$lib/common/framework/ui/ErrorPage.svelte';
|
||||
import DashboardNavbar from '$lib/dashboard/framework/ui/DashboardNavbar.svelte';
|
||||
import type { DashboardLink } from '$lib/dashboard/framework/ui/dashboardLink';
|
||||
import { Container } from '$lib/container';
|
||||
import { AuthLoadedStore } from '$lib/auth/adapter/presenter/authLoadedStore';
|
||||
|
||||
const { children }: LayoutProps = $props();
|
||||
const container = getContext<Container>(Container.name);
|
||||
const store = container.createAuthLoadedStore();
|
||||
setContext(AuthLoadedStore.name, store);
|
||||
|
||||
const authBloc = container.createAuthBloc();
|
||||
setContext(AuthBloc.name, authBloc);
|
||||
const state = $derived($store);
|
||||
const { trigger: loadAuthentication } = store;
|
||||
|
||||
onMount(() => authBloc.dispatch({ event: AuthEventType.CurrentUserLoadedEvent }));
|
||||
|
||||
const authState = $derived($authBloc);
|
||||
const isLoading = $derived(
|
||||
authState.status === StatusType.Loading || authState.status === StatusType.Idle
|
||||
);
|
||||
const isAuthenticated = $derived(
|
||||
authState.status === StatusType.Success && authState.data.isAuthenticated
|
||||
);
|
||||
const isAuthenticated = $derived(state.isSuccess() && state.data.isAuthenticated);
|
||||
|
||||
const links: DashboardLink[] = [
|
||||
{ label: 'Post', href: '/dashboard/post' },
|
||||
{ label: 'Label', href: '/dashboard/label' },
|
||||
{ label: 'Image', href: '/dashboard/image' },
|
||||
];
|
||||
|
||||
onMount(() => loadAuthentication());
|
||||
</script>
|
||||
|
||||
{#if isLoading}
|
||||
{#if state.isIdle() || state.isLoading()}
|
||||
<div></div>
|
||||
{:else if !isAuthenticated}
|
||||
<ErrorPage />
|
||||
{:else}
|
||||
<div class="grid min-h-content-height grid-cols-[auto_1fr]">
|
||||
<div class="min-h-content-height grid grid-cols-[auto_1fr]">
|
||||
<DashboardNavbar {links} />
|
||||
{@render children()}
|
||||
</div>
|
||||
|
@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/container';
|
||||
import { ImageBloc } from '$lib/image/adapter/presenter/imageBloc';
|
||||
import { ImageUploadedStore } from '$lib/image/adapter/presenter/ImageUploadedStore';
|
||||
import ImageManagementPage from '$lib/image/framework/ui/ImageManagementPage.svelte';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
|
||||
const container = getContext<Container>(Container.name);
|
||||
const imageBloc = container.createImageBloc();
|
||||
setContext(ImageBloc.name, imageBloc);
|
||||
const store = container.createImageUploadedStore();
|
||||
setContext(ImageUploadedStore.name, store);
|
||||
</script>
|
||||
|
||||
<ImageManagementPage />
|
||||
|
@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/container';
|
||||
import { PostBloc } from '$lib/post/adapter/presenter/postBloc';
|
||||
import { PostCreatedStore } from '$lib/post/adapter/presenter/postCreatedStore';
|
||||
import PostManagementPage from '$lib/post/framework/ui/PostManagementPage.svelte';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
|
||||
const container = getContext<Container>(Container.name);
|
||||
const postBloc = container.createPostBloc();
|
||||
setContext(PostBloc.name, postBloc);
|
||||
const store = container.createPostCreatedStore();
|
||||
setContext(PostCreatedStore.name, store);
|
||||
</script>
|
||||
|
||||
<PostManagementPage />
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { PostListEventType } from '$lib/post/adapter/presenter/postListBloc';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
const { container } = locals;
|
||||
const postListBloc = container.createPostListBloc();
|
||||
const store = container.createPostsListedStore();
|
||||
const { trigger: loadPosts } = store;
|
||||
|
||||
const state = await postListBloc.dispatch({ event: PostListEventType.PostListLoadedEvent });
|
||||
const state = await loadPosts();
|
||||
|
||||
return {
|
||||
dehydratedData: state.data?.map((post) => post.dehydrate()),
|
||||
|
@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { PostListBloc } from '$lib/post/adapter/presenter/postListBloc';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import type { PageProps } from './$types';
|
||||
import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel';
|
||||
import PostOverallPage from '$lib/post/framework/ui/PostOverallPage.svelte';
|
||||
import { Container } from '$lib/container';
|
||||
import { PostsListedStore } from '$lib/post/adapter/presenter/postsListedStore';
|
||||
|
||||
const { data }: PageProps = $props();
|
||||
const container = getContext<Container>(Container.name);
|
||||
|
||||
const initialData = data.dehydratedData?.map((post) => PostInfoViewModel.rehydrate(post));
|
||||
const postListBloc = container.createPostListBloc(initialData);
|
||||
setContext(PostListBloc.name, postListBloc);
|
||||
const store = container.createPostsListedStore(initialData);
|
||||
setContext(PostsListedStore.name, store);
|
||||
</script>
|
||||
|
||||
<PostOverallPage />
|
||||
|
@ -1,15 +1,12 @@
|
||||
import { PostEventType } from '$lib/post/adapter/presenter/postBloc';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals, params }) => {
|
||||
const { container } = locals;
|
||||
const postBloc = container.createPostBloc();
|
||||
const store = container.createPostLoadedStore();
|
||||
const { trigger: loadPost } = store;
|
||||
|
||||
const state = await postBloc.dispatch({
|
||||
event: PostEventType.PostLoadedEvent,
|
||||
id: params.id,
|
||||
});
|
||||
const state = await loadPost(params.id);
|
||||
if (!state.data) {
|
||||
error(404, { message: 'Post not found' });
|
||||
}
|
||||
|
@ -1,18 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { PostBloc } from '$lib/post/adapter/presenter/postBloc';
|
||||
import { PostViewModel } from '$lib/post/adapter/presenter/postViewModel';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import type { PageProps } from './$types';
|
||||
import PostContentPage from '$lib/post/framework/ui/PostContentPage.svelte';
|
||||
import { Container } from '$lib/container';
|
||||
import { PostLoadedStore } from '$lib/post/adapter/presenter/PostLoadedStore';
|
||||
|
||||
const { data, params }: PageProps = $props();
|
||||
const { id } = params;
|
||||
const container = getContext<Container>(Container.name);
|
||||
|
||||
const initialData = PostViewModel.rehydrate(data.dehydratedData!);
|
||||
const postBloc = container.createPostBloc(initialData);
|
||||
setContext(PostBloc.name, postBloc);
|
||||
const store = container.createPostLoadedStore(initialData);
|
||||
setContext(PostLoadedStore.name, store);
|
||||
</script>
|
||||
|
||||
<PostContentPage {id} />
|
||||
|
Loading…
x
Reference in New Issue
Block a user