Skip to content

Commit

Permalink
Merge pull request #36 from traP-jp/feat/backend-api-gateway
Browse files Browse the repository at this point in the history
feat: API呼び出しのラッパーを作成
  • Loading branch information
cp-20 authored Jun 16, 2024
2 parents 367a682 + 92e7958 commit 13f5ef5
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 39 deletions.
8 changes: 4 additions & 4 deletions frontend/src/assets/main.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
@import './base.css';

:root {
--accent-color: #FF922B;
--primary-text-color: #1111111;
--dimmed-text-color: #888888;
--dimmed-border-color: #DDDDDD;
--accent-color: #ff922b;
--primary-text-color: #1111111;
--dimmed-text-color: #888888;
--dimmed-border-color: #dddddd;
}
10 changes: 5 additions & 5 deletions frontend/src/components/Avatar.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<script setup lang="ts">
defineProps<{
name: string,
size: string
}>()
const endpoint = "https://q.trap.jp/api/v3"
name: string;
size: string;
}>();
const endpoint = 'https://q.trap.jp/api/v3';
</script>

<template>
<img class="avatar" :src="`${endpoint}/public/icon/${name}`" />
<img class="avatar" :src="`${endpoint}/public/icon/${name}`" />
</template>

<style scoped>
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/components/Button.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<script setup lang="ts">
defineProps<{
disabled: boolean
}>()
disabled: boolean;
}>();
</script>

<template>
<button type="button" :disabled="disabled"><slot /></button>
<button type="button" :disabled="disabled">
<slot />
</button>
</template>

<style lang="scss" scoped>
Expand All @@ -19,6 +21,7 @@ button {
text-align: center;
cursor: pointer;
background-color: var(--accent-color);
&:disabled {
background-color: var(--dimmed-border-color);
}
Expand Down
28 changes: 16 additions & 12 deletions frontend/src/components/NewPostSection.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
<script setup lang="ts">
import Button from "./Button.vue"
import Avatar from "./Avatar.vue"
import {ref, computed} from "vue"
import Button from './Button.vue';
import Avatar from './Avatar.vue';
import { ref, computed } from 'vue';
defineProps<{
name: string,
}>()
name: string;
}>();
const inputContent = ref('')
const inputContent = ref('');
const canPost = computed(() => {
return (inputContent.value.length != 0) && (inputContent.value.length <= 280)
})
return inputContent.value.length != 0 && inputContent.value.length <= 280;
});
</script>

<template>
Expand All @@ -20,9 +19,15 @@ const canPost = computed(() => {
<Avatar :name="name" size="48px"></Avatar>
</div>
<div class="new-post-input-section">
<textarea type="text" placeholder="投稿する内容を入力(投稿時に自動で変換されます)" v-model="inputContent"/>
<textarea
type="text"
placeholder="投稿する内容を入力(投稿時に自動で変換されます)"
v-model="inputContent"
/>
<div class="post-footer">
<span :class="!canPost ? 'post-charcount-warn' : undefined">{{ inputContent.length }}/280文字</span>
<span :class="!canPost ? 'post-charcount-warn' : undefined"
>{{ inputContent.length }}/280文字</span
>
<span class="post-button">
<Button :disabled="!canPost">投稿する</Button>
</span>
Expand Down Expand Up @@ -70,5 +75,4 @@ const canPost = computed(() => {
}
}
}
</style>
28 changes: 14 additions & 14 deletions frontend/src/components/Post.vue
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
<script setup lang="ts">
import Avatar from "@/components/Avatar.vue";
import Avatar from '@/components/Avatar.vue';
import 'moment/dist/locale/ja';
import moment from 'moment-timezone';
import {ref} from "vue";
import {reactionIcons} from "@/features/reactions";
import { ref } from 'vue';
import { reactionIcons } from '@/features/reactions';
const props = defineProps<{
name: string,
date: Date,
content: string,
reactions: { id: number, count: number, clicked: boolean }[],
}>()
name: string;
date: Date;
content: string;
reactions: { id: number; count: number; clicked: boolean }[];
}>();
function getDateText() {
return moment(props.date).fromNow();
}
const dateText = ref(getDateText());
</script>

<template>
<div class="post">
<div class="post-author-icon">
<Avatar size="48px" :name="name"/>
<Avatar size="48px" :name="name" />
</div>
<div class="post-content">
<div class="post-header">
Expand All @@ -35,10 +34,11 @@ const dateText = ref(getDateText());
</div>
<div class="post-reactions">
<div
v-for="reaction in reactions"
:key="reaction.id"
class="post-reaction"
:class="reaction.clicked ? ['clicked'] : undefined">
v-for="reaction in reactions"
:key="reaction.id"
class="post-reaction"
:class="reaction.clicked ? ['clicked'] : undefined"
>
<span class="post-reaction-icon">{{ reactionIcons[reaction.id] }}</span>
<span class="post-reaction-count">{{ reaction.count }}</span>
</div>
Expand Down
203 changes: 203 additions & 0 deletions frontend/src/features/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

export const fetchApi = async (
method: HttpMethod,
path: string,
option?: { parameters?: Record<string, string | undefined>; body?: Record<string, unknown> },
) => {
const bodyObj = option?.body && {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(option?.body),
};
const parameterStr = option?.parameters
? new URLSearchParams(JSON.parse(JSON.stringify(option?.parameters))).toString()
: '';
const res = await fetch(`/api${path}?${parameterStr}`, {
method,
...bodyObj,
});
const data = await res.json();
return data;
};

export type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;

export type CreatedPost = {
/**
* 変換元のメッセージ
*/
original_message: string;
/**
* 変換後のメッセージ
*/
converted_message: string;
/**
* UUID
*/
id: string;
/**
* 投稿時刻
*/
created_at: string;
/**
* リプライのとき、親のID。そうでなければ自身のID
*/
parent_id: string;
/**
* リプライのとき、そのおおもとのID。そうでなければ自身のID
*/
root_id: string;
};

export type Post = Omit<CreatedPost, 'parent_id'> & {
/**
* 投稿したユーザー名
*/
user_name: string;
/**
* リアクションのリスト
*/
reactions: Reaction[];
/**
* 自分がリアクションしたリアクションIDのリスト
*/
my_reactions: number[];
};
export type PostWithoutParents = Omit<Post, 'root_id'>;
export type PostDetail = PostWithoutParents & {
/**
* 全ての祖先投稿で、古い順
*/
ancestors: Array<{
post: Omit<PostWithoutParents, 'parent_id' | 'root_id'>;
children_count: number;
}>;
/**
* 1個下の子投稿で、新しい順
*/
children: Array<{
post: Omit<PostWithoutParents, 'parent_id' | 'root_id'>;
children_count: number;
}>;
};

export type Reaction = {
/**
* リアクションID
*/
id: number;
/**
* カウント
*/
count: number;
};
export type ReactionDetail = {
/**
* リアクションID
*/
id: number;
/**
* リアクションしたユーザーのID
*/
users: string[];
};

export type CreatePostBody = {
/**
* メッセージ
*/
message: string;
/**
* リプライのとき、親のID。そうでなければundefined
*/
parent_id?: string;
};
export type CreatePostResponse = CreatedPost;
export const createPost = async (body: CreatePostBody): Promise<CreatePostResponse> => {
return fetchApi('POST', '/posts', { body });
};

export type GetPostsParameters = {
/**
* 取得件数。デフォルト30
*/
limit?: number;
/**
* このIDの投稿より後に投稿されたものを取得する。指定されない場合は、最新のものからlimit件取得する
*/
after?: string;
/**
* リポストのやつを含むかどうか。デフォルトはfalse
*/
repost?: boolean;
};
export type GetPostsResponse = Array<
Expand<
Post & {
/**
* リポストの場合はリポストしたユーザーの名前
*/
repost_user?: string;
}
>
>;
export const getPosts = async ({
limit,
after,
repost,
}: GetPostsParameters): Promise<CreatePostResponse[]> => {
return fetchApi('GET', '/posts', {
parameters: { limit: limit?.toString() ?? '30', after, repost: repost?.toString() ?? 'false' },
});
};

type GetPostResponse = Expand<PostDetail>;
export const getPost = async (postId: string): Promise<GetPostResponse> => {
return fetchApi('GET', `/posts/${postId}`);
};

export type PostReactionResponse = Reaction[];
export const postReaction = async (postId: string, reactionId: number) => {
return fetchApi('POST', `/posts/${postId}/reactions/${reactionId}`);
};

export type DeleteReactionResponse = Reaction[];
export const deleteReaction = async (postId: string, reactionId: number) => {
return fetchApi('DELETE', `/posts/${postId}/reactions/${reactionId}`);
};

export type GetReactionsResponse = ReactionDetail[];
export const getReactions = async (postId: string) => {
return fetchApi('GET', `/posts/${postId}/reactions`);
};

export type GetTrendResponse = Array<Post>;
export const getTrend = async (reactionId: number) => {
return fetchApi('GET', '/trend', { parameters: { reaction_id: reactionId.toString() } });
};

export type GetUserResponse = {
/**
* ユーザーID
*/
user_name: string;
/**
* 投稿数
*/
post_count: number;
/**
* リアクションした数
*/
reaction_count: number;
/**
* リアクションされた数
*/
get_reaction_count: number;
/**
* 投稿のリスト
*/
posts: Post[];
};
export const getUser = async (userName: string) => {
return fetchApi('GET', `/user/${userName}`);
};
2 changes: 1 addition & 1 deletion frontend/src/features/reactions.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const reactionIcons = ['❤️', '🔥', '💧', '😢', '🤔'];
export const reactionIcons = ['❤️', '🔥', '💧', '😢', '🤔'];
19 changes: 19 additions & 0 deletions frontend/src/features/useFetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ref, type UnwrapRef } from 'vue';

export const useFetcher = <T>(fetcher: () => Promise<UnwrapRef<T>>) => {
const data = ref<T | undefined>(undefined);
const loading = ref(true);
const error = ref<Error | undefined>(undefined);

fetcher()
.then((fetchedData) => {
data.value = fetchedData;
loading.value = false;
})
.catch((e) => {
error.value = e;
loading.value = false;
});

return { data, loading, error };
};

0 comments on commit 13f5ef5

Please sign in to comment.