Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into AB#308-prepare-nuxt-e…
Browse files Browse the repository at this point in the history
…ndpoint-for-fetching-post-data

* origin/main:
  ♻️ use logger instance
  ✨ load more messages mechanism
  🐛 fix footer
  🐛 fix multiple tweaks
  ✨ notification component
  ✨ notification util
  ✨ partial prepare
  ♻️ added small changes
  ♻️ small changes to user settings component
  ♻️ add service to component
  ✨ add user settings component
AdamPrisen committed Apr 20, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents 5f9e734 + aa5dbe5 commit 9616a77
Showing 28 changed files with 416 additions and 58 deletions.
19 changes: 18 additions & 1 deletion app.vue
Original file line number Diff line number Diff line change
@@ -2,12 +2,29 @@
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<div class="fixed bottom-0 right-0 p-4 z-50 gap-2 flex flex-col">
<PopupMessage
v-for="(notification, id) in notifications"
:key="id"
:title="notification.title"
:message="notification.content"
:type="notification.type"
@close="clearNotification(id)"
/>
</div>
</template>
<script setup lang="ts">
import { useTokenStore } from '~/store';
import { useTokenStore, useNotificationStore } from '~/store';
import { MOCKED_POSTS } from '~/mocks';
const tokenStore = useTokenStore();
const notificationStore = useNotificationStore();
const notifications = computed(() => notificationStore.items);
const clearNotification = (id: string) => {
notificationStore.remove(id);
};
onMounted(() => {
// Mock posts
63 changes: 63 additions & 0 deletions components/PopupMessage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<template>
<div
class="bg-slate-800 border border-white rounded-lg shadow-lg p-4 flex flex-col items-start gap-2"
:class="`bg-${type}-100 border-${type}-200`"
>
<p :class="[color]">{{ title }}</p>
<span class="flex justify-between gap-2">
<div class="flex items-center gap-2">
<Icon :name="icon" :class="[color]" />
<span>{{ message }}</span>
</div>
<button class="text-gray-500 hover:text-gray-800" @click="$emit('close')">
<Icon name="mdi:close" />
</button>
</span>
</div>
</template>

<script lang="ts" setup>
defineEmits(['close']);
const props = defineProps({
title: {
type: String,
required: true,
},
message: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
});
const icon = computed(() => {
switch (props.type) {
case 'success':
return 'mdi:check-circle';
case 'error':
return 'mdi:alert-circle';
case 'warning':
return 'mdi:alert-circle';
case 'info':
return 'mdi:information';
}
return 'mdi:alert-circle';
});
const color = computed(() => {
switch (props.type) {
case 'success':
return 'text-pink-500';
case 'error':
return 'text-red-500';
case 'warning':
return 'text-yellow-500';
case 'info':
return 'text-white';
}
return 'text-white';
});
</script>
2 changes: 1 addition & 1 deletion components/SignTest.vue
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ const logger = useLogger('sign-test::');
const { $web3 } = useNuxtApp();
const accountStore = useAccountStore();
const account = computed(() => accountStore.account);
const account = computed(() => accountStore.address);
const message = ref('');
const signature = ref('');
25 changes: 25 additions & 0 deletions components/chat/ChatMember.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<template>
<div class="flex items-center w-full gap-2">
<img :src="profilePicture" alt="avatar" class="w-8 h-8 rounded-full" />
<p class="overflow-hidden text-ellipsis cursor-pointer" @click="$emit('copy')">
{{ member.username ?? member.address }}
</p>
</div>
</template>

<script lang="ts" setup>
import type { PropType } from 'vue';
import type { UserResponseDTO } from '~/types/dtos';
defineEmits(['copy']);
const props = defineProps({
member: {
type: Object as PropType<UserResponseDTO>,
required: true,
},
});
const profilePicture =
props.member.profilePicture ?? `https://gravatar.com/avatar/${props.member.address}?s=200&d=identicon&r=x`;
</script>
5 changes: 4 additions & 1 deletion components/chat/CreateChatModal.vue
Original file line number Diff line number Diff line change
@@ -46,7 +46,7 @@
</template>

<script lang="ts" setup>
import { useChatStore } from '~/store';
import { useChatStore, useNotificationStore } from '~/store';
const emit = defineEmits<{
(e: 'close'): void;
@@ -88,13 +88,16 @@ const canCreateChat = computed(() => {
const signal = useSignalR();
const chatStore = useChatStore();
const router = useRouter();
const notificationStore = useNotificationStore();
const createChat = async () => {
if (!canCreateChat.value) return;
const addresses = users.value.map((u) => u.address);
const newChat = await signal.createChat(addresses, chatName.value);
chatStore.addChat(newChat);
emit('close');
notificationStore.addNotification('Chat created!', `Chat ${chatName.value} created`);
router.push(`/chat/${newChat.id}`);
};
</script>
15 changes: 12 additions & 3 deletions components/chat/MessageRow.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="wrapper" :class="[mine ? 'justify-start' : 'justify-end']">
<div class="w-min min-w-40" :class="[mine ? 'my-message' : 'their-message']">
<div class="wrapper" :class="[mine ? 'justify-end' : 'justify-start']">
<div class="min-w-40" :class="[mine ? 'my-message' : 'their-message']">
<p>{{ message.content }}</p>
<sub class="w-full text-end block my-2">{{ formattedDate }}</sub>
</div>
@@ -22,7 +22,16 @@ const props = defineProps({
const date = new Date(props.message.createdAt);
const formattedDate = `${date.getHours()}:${date.getMinutes()}`;
const minutes = date.getMinutes();
const formattedMinutes = minutes < 10 ? `0${minutes}` : minutes;
const hours = date.getHours();
const formattedHours = hours < 10 ? `0${hours}` : hours;
const formattedTime = `${formattedHours}:${formattedMinutes}`;
const dateString = date.toDateString();
const isBeforeToday = new Date().toDateString() !== dateString;
const formattedDate = isBeforeToday ? `${dateString} ${formattedTime}` : formattedTime;
</script>

<style>
1 change: 1 addition & 0 deletions components/chat/MessageSender.vue
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@ const message = ref('');
const signal = useSignalR();
const sendMessage = async () => {
if (!message.value) return;
try {
const newMess = await signal.sendMessage(props.chat.id, message.value);
emit('message', newMess);
19 changes: 18 additions & 1 deletion components/controls/ConfirmButton.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
<template>
<button class="w-full border-2 p-2 rounded-md bg-primary" @click="$emit('click')">{{ text }}</button>
<button
class="w-full border-2 p-2 rounded-md bg-primary"
:class="{
'bg-pink-500': primary && !disabled,
'bg-gray-500 cursor-not-allowed': disabled,
}"
@click="!disabled && $emit('click')"
>
{{ text }}
</button>
</template>

<script lang="ts" setup>
@@ -13,6 +22,14 @@ defineProps({
type: String,
default: 'primary',
},
primary: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
});
</script>

23 changes: 21 additions & 2 deletions components/controls/SearchBar.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
<template>
<div class="wrapper">
<Icon name="mdi:search" size="24" />
<input class="address-input" placeholder="Search" type="text" />
<Icon
name="mdi:search"
:class="{
'text-gray-200 cursor-not-allowed': !validAddress,
'text-pink-300 hover:text-pink-500 cursor-pointer': validAddress,
}"
size="24"
@click="go"
/>
<input v-model="text" class="address-input" placeholder="Search" type="text" @keyup.enter="go" />
</div>
</template>
<script setup lang="ts">
const text = ref('');
const router = useRouter();
const validAddress = computed(() => text.value.match(/(\b0x[a-fA-F0-9]{40}\b)/g));
const go = () => {
if (!validAddress.value) return;
router.push(`/profile/${text.value}`);
};
</script>
<style scoped>
.wrapper {
@apply flex items-center gap-2 relative justify-center w-full px-2 h-10 border-2 border-pink-300 rounded-md bg-transparent text-pink-300;
2 changes: 1 addition & 1 deletion components/controls/TextInput.vue
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
<input
v-model="model"
:readonly="readonly"
class="border-2 rounded-md p-2"
class="border-2 rounded-md p-2 bg-transparent"
type="text"
name="message"
:placeholder="placeholder ?? undefined"
6 changes: 3 additions & 3 deletions components/layout/LeftSideContent.vue
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ const links = computed<Link[]>(() =>
? [
{
name: 'Home',
link: '/',
link: '/home',
icon: 'mdi:home-outline',
},
{
@@ -53,7 +53,7 @@ const links = computed<Link[]>(() =>
},
{
name: 'Settings',
link: '/settings',
link: '/profile/settings',
icon: 'mdi:cog-outline',
},
]
@@ -68,7 +68,7 @@ const links = computed<Link[]>(() =>
</script>
<style>
.left-nav {
@apply flex flex-col w-full p-2 h-full items-start bg-slate-900;
@apply flex flex-col w-full p-2 items-start bg-slate-900;
#navigation-links {
@apply w-full flex flex-col gap-4 p-2;
7 changes: 2 additions & 5 deletions components/layout/NavigationComponent.vue
Original file line number Diff line number Diff line change
@@ -50,7 +50,7 @@ const logger = useLogger('wallet::');
const accountStore = useAccountStore();
const account = computed(() => accountStore.account);
const account = computed(() => accountStore.address);
const availableAccounts = ref<string[]>([]);
@@ -84,12 +84,9 @@ onMounted(async () => {
logger.info('Requesting accounts');
await window.ethereum.request({ method: 'eth_requestAccounts' });
availableAccounts.value = (await $web3?.eth.getAccounts()) ?? [];
logger.info('Available accounts:', availableAccounts.value);
if (!account.value) {
return;
}
logger.info('Account connected:', account);
logger.info('Refresh tokens');
const { accessToken, refreshToken } = await $fetch('/api/auth/refresh', {
@@ -122,7 +119,7 @@ const connectWallet = async () => {
if (!address) {
return;
}
accountStore.setAccount(address);
accountStore.setAddress(address);
try {
logger.info('Requesting nonce');
const query = new URLSearchParams({ address });
22 changes: 20 additions & 2 deletions components/layout/RightSideChat.vue
Original file line number Diff line number Diff line change
@@ -13,6 +13,12 @@
<span v-if="expanded">Toggle chats</span>
</span>
<ul v-if="expanded" id="side-items" :class="{ 'items-start': expanded, 'items-center': !expanded }">
<template v-if="currentChat">
<li class="side-item text-xl">Members</li>
<li v-for="member in currentChat.users" :key="member.address" class="side-item">
<chat-member :member="member" @copy="copyAddress(member.address)" />
</li>
</template>
<li v-if="hasInvitations" class="side-item text-xl border-b border-pink-500">Invitations</li>
<li
v-for="invitation in invitations"
@@ -60,8 +66,9 @@ import { HubConnectionState } from '@microsoft/signalr';
import ChatRow from '~/components/chat/ChatRow.vue';
import CreateChatModal from '~/components/chat/CreateChatModal.vue';
import ChatMember from '~/components/chat/ChatMember.vue';
import { useAccountStore, useChatStore } from '~/store';
import { useAccountStore, useChatStore, useNotificationStore } from '~/store';
const expanded = ref(true);
@@ -140,6 +147,11 @@ const leaveChat = async (id: number) => {
}
};
const currentChat = computed(() => {
if (!route.params.id) return null;
return chats.value.find((c) => c.id === Number(route.params.id)) ?? null;
});
onMounted(async () => {
// Load chats
const dto = await $fetch('/api/chat', {
@@ -167,10 +179,16 @@ onMounted(async () => {
}
}
});
const notificationStore = useNotificationStore();
const copyAddress = (address: string) => {
navigator.clipboard.writeText(address);
notificationStore.addNotification('Address copied!', 'Address copied to clipboard', 'success');
};
</script>
<style scoped>
.right-nav {
@apply flex flex-col w-full p-4 h-full items-center bg-slate-900;
@apply flex flex-col w-full p-4 items-center bg-slate-900;
#side-items {
@apply w-full flex flex-col gap-4;
90 changes: 90 additions & 0 deletions components/user/UserSettings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<template>
<div class="flex flex-col justify-start items-center gap-2 mt-4 w-full h-screen">
<h1 class="text-2xl font-bold">User Settings</h1>
<div class="flex flex-col rounded-md bg-black border border-pink-500 w-full m-2 p-4 gap-4">
<span class="flex justify-between items-center w-full">
<span class="flex flex-col gap-1">
<p class="font-semibold">Profile picture</p>
<img v-if="profilePicture" :src="profilePicture" alt="Profile picture" class="border-2 p-2" />
<span v-else class="border-2 p-2">No image</span>
</span>
<span class="w-1/3">
<input ref="fileInput" type="file" accept="image/*" style="display: none" @change="openFileInput" />
<ConfirmButton text="Change photo" @click="openFileInput" />
</span>
</span>
<hr />
<span class="flex justify-between items-center w-full">
<span class="flex flex-col gap-1">
<p class="font-semibold">Username</p>
<TextInput v-model="editedUsername" :placeholder="username" />
</span>
</span>
<hr />
<span class="flex justify-between items-center w-full">
<span class="flex flex-col gap-1">
<p class="font-semibold">Address</p>
<p>{{ address }}</p>
</span>
<span class="w-1/3"></span>
</span>
<span class="flex justify-end gap-4">
<ConfirmButton primary :disabled="!canSave" text="Save changes" @click="saveChanges" />
<ConfirmButton text="Exit" @click="exit" />
</span>
</div>
</div>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import ConfirmButton from '~/components/controls/ConfirmButton.vue';
import TextInput from '~/components/controls/TextInput.vue';
import { useAccountStore } from '~/store';
const accountStore = useAccountStore();
const username = computed(() => accountStore.username ?? '');
const address = computed(() => accountStore.address);
const editedUsername = ref(username.value);
const profilePicture = ref('');
const fileInputRef = ref<HTMLInputElement | null>(null);
const openFileInput = () => {
if (fileInputRef.value) {
fileInputRef.value.click();
}
};
const saveChanges = async () => {
const fd = new FormData();
if (fileInputRef.value?.files?.length) {
fd.append('profilePicture', fileInputRef.value.files[0]);
}
fd.append('username', editedUsername.value);
try {
await $fetch(`/api/user`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${accountStore.accessToken}`,
},
body: fd,
});
} catch (error) {
throw new Error('Error saving changes: ' + error);
}
};
const router = useRouter();
const exit = () => {
router.push('/');
};
const canSave = computed(() => {
return !!(editedUsername.value !== username.value || profilePicture.value);
});
</script>

<style></style>
4 changes: 2 additions & 2 deletions layouts/chat.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="h-screen">
<div class="min-h-screen">
<header><navigation-component /></header>
<main class="flex h-full">
<left-side-content />
@@ -8,7 +8,7 @@
</div>
<right-side-chat />
</main>
<footer>
<footer class="sticky">
<footer-component />
</footer>
</div>
34 changes: 30 additions & 4 deletions pages/chat/[id].vue
Original file line number Diff line number Diff line change
@@ -5,7 +5,15 @@
<template v-if="chat">
<chat-header :chat="chat" />
<invite-to-chat v-if="isChatAdmin" />
<div class="w-full flex flex-col justify-end gap-2">
<div id="chat" class="w-full flex flex-col justify-end gap-2">
<button
v-if="canLoadMore"
class="flex w-full text-pink-500 border justify-center rounded border-pink-500"
@click="loadMore"
>
Load more
</button>

<message-row v-for="message in messages" :key="message.id" :message="message" :mine="isMine(message)" />
<message-sender :chat="chat" @message="saveMessage" @error="(e) => (error = e)" />
</div>
@@ -34,6 +42,7 @@ const error = ref<Error | null>(null);
const route = useRoute();
const router = useRouter();
const canLoadMore = ref(true);
const chatStore = useChatStore();
const accountStore = useAccountStore();
@@ -50,20 +59,37 @@ if (!chat.value) {
const messages = computed(() => chatStore.getMessages(Number(route.params.id)));
const isMine = (message: ChatMessageResponseDTO) => {
return message.sender.address === accountStore.account;
return message.sender.address === accountStore.address;
};
const isChatAdmin = computed(() => {
return chat.value && chat.value.admin.address === accountStore.account;
return chat.value && chat.value.admin.address === accountStore.address;
});
const pageNumber = ref(1);
const loadMore = async () => {
pageNumber.value++;
const params: URLSearchParams = new URLSearchParams({
pageNumber: String(pageNumber.value),
pageSize: '20',
});
const newMessages = await $fetch(`/api/chat/${route.params.id}/messages?${params.toString()}`, {
headers: {
Authorization: `Bearer ${accountStore.accessToken}`,
},
});
if (newMessages.length === 0) {
canLoadMore.value = false;
}
chatStore.addMessages(Number(route.params.id), newMessages);
};
onMounted(async () => {
// Load messages
const params: URLSearchParams = new URLSearchParams({
pageNumber: String(pageNumber.value),
pageSize: '10',
pageSize: '20',
});
const messages = await $fetch(`/api/chat/${route.params.id}/messages?${params.toString()}`, {
headers: {
15 changes: 15 additions & 0 deletions pages/profile/[address].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<template>
<div>
<h1>User profile</h1>
<h2>{{ address }}</h2>
</div>
</template>

<script lang="ts" setup>
import { computed } from 'vue';
const route = useRoute();
const address = computed(() => route.params.address);
</script>

<style></style>
14 changes: 14 additions & 0 deletions pages/profile/settings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<template>
<div>
<UserSettingsComponent :access-token="accessToken" />
</div>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import UserSettingsComponent from '~/components/user/UserSettings.vue';
const accessToken = ref<string | null>(null);
</script>

<style></style>
8 changes: 4 additions & 4 deletions pages/profile/setup.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div class="flex flex-col justify-start items-center gap-2 mt-4 w-full h-screen">
<h1 class="text-2xl font-bold">Setup your profile</h1>
<div class="flex flex-col rounded-md bg-white w-full m-2 p-4 gap-4">
<div class="flex flex-col rounded-md bg-slate-800 w-full m-2 p-4 gap-4">
<span class="flex justify-between items-center w-full">
<span class="flex flex-col gap-1">
<p class="font-semibold">Profile picture</p>
@@ -43,11 +43,11 @@ definePageMeta({
});
const accountStore = useAccountStore();
const address = computed(() => accountStore.account);
const alias = ref(accountStore.alias);
const address = computed(() => accountStore.address);
const alias = ref(accountStore.username);
const saveChanges = () => {
accountStore.setAlias(alias.value);
accountStore.setUsername(alias.value);
};
const r = useRouter();
5 changes: 0 additions & 5 deletions pages/settings.vue

This file was deleted.

14 changes: 8 additions & 6 deletions server/api/user/index.put.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useUserService } from '~/server/services/user.service';
import { AuthenticatedUser } from '~/types/auth';
import { UserUpdateDTO, UserResponseDTO } from '~/types/dtos/user';

type Body = AuthenticatedUser & UserUpdateDTO & UserResponseDTO;

export default defineEventHandler(async (event) => {
const { jwt, username, profilePicture } = await readBody<Body>(event);
const jwt = getHeader(event, 'Authorization')?.split('Bearer ')[1];
if (!jwt) {
throw createError({
message: 'Unauthorized',
});
}
const fd = await readFormData(event);
const service = useUserService(jwt);
return await service.updateUser(username, profilePicture);
return await service.updateUser(fd);
});
12 changes: 6 additions & 6 deletions server/services/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { useApi } from '../utils/api';
import type { UserUpdateDTO, UserResponseDTO } from '~/types/dtos/user';
import type { UserResponseDTO } from '~/types/dtos/user';

export function useUserService(token: string) {
const updateUser = async (username: string, profilePicture: string): Promise<UserResponseDTO> => {
const resp = await useApi<UserResponseDTO, UserUpdateDTO>(`user`, token, {
const updateUser = async (fd: FormData): Promise<UserResponseDTO> => {
const resp = await useApi<UserResponseDTO, FormData>(`users`, token, {
method: 'PUT',
body: {
username,
profilePicture,
body: fd,
headers: {
'Content-Type': 'multipart/form-data',
},
});
return resp;
6 changes: 4 additions & 2 deletions server/utils/api.ts
Original file line number Diff line number Diff line change
@@ -35,17 +35,19 @@ export const useApi = async <T = undefined, B extends FetchOptions<'json'>['body
headers,
});
} catch (error) {
const logger = useLogger('API');
if (error instanceof FetchError) {
if (error.statusCode) {
logger.error(error.statusCode, error.message);
throw ServerError.fromCode(error.statusCode, error.message);
} else {
logger.error('Unavailable', error.message);
throw ServerError.unavailable();
}
} else {
// This is a generic error, we don't know what happened.

// eslint-disable-next-line no-console
console.error(error);
logger.error('Internal error', error);
throw ServerError.internalServerError('Something went wrong');
}
}
16 changes: 8 additions & 8 deletions store/account.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
type AccountStoreState = {
account: string | null;
address: string | null;
accessToken: string | null;
refreshToken: string | null;
alias: string | null;
username: string | null;
};

export const useAccountStore = defineStore('account', {
persist: true,
state: (): AccountStoreState => ({
account: null,
alias: null,
address: null,
username: null,
accessToken: null,
refreshToken: null,
}),

actions: {
setAccount(account: string | null) {
this.account = account;
setAddress(address: string | null) {
this.address = address;
},
setAlias(alias: string | null) {
this.alias = alias;
setUsername(username: string | null) {
this.username = username;
},
setToken(accessToken: string | null, refreshToken: string | null) {
this.accessToken = accessToken;
6 changes: 6 additions & 0 deletions store/chat.ts
Original file line number Diff line number Diff line change
@@ -39,6 +39,12 @@ export const useChatStore = defineStore({
setMessages(chatId: number, messages: ChatMessageResponseDTO[]) {
this.messages[chatId] = messages.reverse();
},
addMessages(chatId: number, messages: ChatMessageResponseDTO[]) {
if (!this.messages[chatId]) {
this.messages[chatId] = [];
}
this.messages[chatId].unshift(...messages);
},
addMessage(chatId: number, message: ChatMessageResponseDTO) {
if (!this.messages[chatId]) {
this.messages[chatId] = [];
1 change: 1 addition & 0 deletions store/index.ts
Original file line number Diff line number Diff line change
@@ -2,3 +2,4 @@ export { useAccountStore } from './account';
export { useUsersStore } from './users';
export { useChatStore } from './chat';
export { useTokenStore } from './token';
export { useNotificationStore } from './notification';
38 changes: 38 additions & 0 deletions store/notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { defineStore } from 'pinia';

type NotificationType = 'info' | 'success' | 'error';

export interface Notification {
title: string;
content: string;
type: NotificationType;
}

type StoreState = {
items: Record<string, Notification>;
};

export const useNotificationStore = defineStore({
id: 'notification',
state: (): StoreState => ({
items: {},
}),
actions: {
addNotification(title: string, content: string, type: NotificationType = 'success', timeout = 50000) {
const id = Math.random().toString(36).substring(7);
this.items[id] = {
title,
content,
type,
};
setTimeout(() => {
if (this.items[id]) {
delete this.items[id];
}
}, timeout);
},
remove(id: string) {
delete this.items[id];
},
},
});
2 changes: 1 addition & 1 deletion types/dtos/user.ts
Original file line number Diff line number Diff line change
@@ -5,6 +5,6 @@ export interface UserUpdateDTO {

export interface UserResponseDTO {
address: string;
username: string;
username?: string;
profilePicture: string;
}

0 comments on commit 9616a77

Please sign in to comment.