From d7d8e59bc09dd03b3d952ae0ee97979ed0d7674c Mon Sep 17 00:00:00 2001
From: shio <85730998+dino3616@users.noreply.github.com>
Date: Sun, 25 Feb 2024 15:17:25 +0900
Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20=E6=89=80=E6=9C=89=E8=80=85?=
 =?UTF-8?q?=E3=82=92=E5=8C=BF=E5=90=8D=E5=8C=96=E3=81=A7=E3=81=8D=E3=82=8B?=
 =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../user/controller/user-mutation.resolver.ts |  14 ++
 .../user/use-case/impl/user.use-case.ts       |   9 +
 .../src/module/user/use-case/user.use-case.ts |   1 +
 apps/locker-dashboard/graphql.schema.json     |  49 ++++
 apps/website/graphql.schema.json              |  49 ++++
 .../user-action-status-list.presenter.tsx     |  91 ++++----
 apps/website/src/common/model/user.ts         |   4 +-
 .../infra/graphql/document/create-user.gql    |   1 +
 .../src/infra/graphql/document/find-user.gql  |   1 +
 .../document/fragment/user-public-meta.gql    |   1 +
 .../document/relate-fingerprint-with-user.gql |   1 +
 .../graphql/document/report-lost-item.gql     |   4 +-
 .../document/update-user-disclosure.gql       |  12 +
 .../user-dropdown-menu.presenter.tsx          | 211 ++++++++++--------
 .../pinned-task-section.presenter.tsx         |  16 +-
 .../website/src/use-case/create-user/index.ts |   3 +-
 .../use-case/find-similar-lost-item/index.ts  |   1 +
 .../use-case/find-user-lost-items/index.ts    |  36 +--
 apps/website/src/use-case/find-user/index.ts  |   1 +
 .../relate-fingerprint-with-user/index.ts     |   1 +
 .../use-case/update-user-disclosure/index.ts  |  37 +++
 .../component/information-popover/index.ts    |   1 +
 .../information-popover.presenter.tsx         |  18 ++
 packages/core/component/switch/index.ts       |   1 +
 .../component/switch/switch.presenter.tsx     |  25 +++
 packages/core/icon/information-icon/index.ts  |   1 +
 .../information-icon.presenter.tsx            |   6 +
 .../information-icon.story.tsx                |  13 ++
 .../user-avatar-placeholder-icon/index.ts     |   1 +
 ...user-avatar-placeholder-icon.presenter.tsx |   6 +
 .../user-avatar-placeholder-icon.story.tsx    |  13 ++
 packages/core/package.json                    |   1 +
 pnpm-lock.yaml                                |  30 +++
 33 files changed, 499 insertions(+), 160 deletions(-)
 create mode 100644 apps/website/src/infra/graphql/document/update-user-disclosure.gql
 create mode 100644 apps/website/src/use-case/update-user-disclosure/index.ts
 create mode 100644 packages/core/component/information-popover/index.ts
 create mode 100644 packages/core/component/information-popover/information-popover.presenter.tsx
 create mode 100644 packages/core/component/switch/index.ts
 create mode 100644 packages/core/component/switch/switch.presenter.tsx
 create mode 100644 packages/core/icon/information-icon/index.ts
 create mode 100644 packages/core/icon/information-icon/information-icon.presenter.tsx
 create mode 100644 packages/core/icon/information-icon/information-icon.story.tsx
 create mode 100644 packages/core/icon/user-avatar-placeholder-icon/index.ts
 create mode 100644 packages/core/icon/user-avatar-placeholder-icon/user-avatar-placeholder-icon.presenter.tsx
 create mode 100644 packages/core/icon/user-avatar-placeholder-icon/user-avatar-placeholder-icon.story.tsx

diff --git a/apps/api/src/module/user/controller/user-mutation.resolver.ts b/apps/api/src/module/user/controller/user-mutation.resolver.ts
index d515f60..cb40f13 100644
--- a/apps/api/src/module/user/controller/user-mutation.resolver.ts
+++ b/apps/api/src/module/user/controller/user-mutation.resolver.ts
@@ -32,6 +32,20 @@ export class UserMutation {
     return createdUser;
   }
 
+  @Mutation(() => UserObject)
+  async updateUserDisclosure(
+    @Args('where', { type: () => UserWhereAuthIdInput }, ValidationPipe)
+    where: UserWhereAuthIdInput,
+    @Args('isDiscloseAsOwner', { type: () => Boolean }, ValidationPipe)
+    isDiscloseAsOwner: boolean,
+  ): Promise<User> {
+    this.logger.log(`${this.updateUserDisclosure.name} called`);
+
+    const updatedUser = await this.userUseCase.updateUserDisclosure(where.authId, isDiscloseAsOwner);
+
+    return updatedUser;
+  }
+
   @Mutation(() => UserObject)
   async relateFingerprintWithUser(
     @Args('where', { type: () => UserWhereAuthIdInput }, ValidationPipe)
diff --git a/apps/api/src/module/user/use-case/impl/user.use-case.ts b/apps/api/src/module/user/use-case/impl/user.use-case.ts
index 728909a..cda7932 100644
--- a/apps/api/src/module/user/use-case/impl/user.use-case.ts
+++ b/apps/api/src/module/user/use-case/impl/user.use-case.ts
@@ -33,6 +33,15 @@ export class UserUseCase implements UserUseCaseInterface {
     return createdUser;
   }
 
+  async updateUserDisclosure(
+    authId: Parameters<UserUseCaseInterface['updateUserDisclosure']>[0],
+    isDiscloseAsOwner: Parameters<UserUseCaseInterface['updateUserDisclosure']>[1],
+  ): Promise<User> {
+    const updatedUser = await this.userRepository.updateByAuthId(authId, { isDiscloseAsOwner });
+
+    return updatedUser;
+  }
+
   async relateFingerprintWithUser(
     authId: Parameters<UserUseCaseInterface['relateFingerprintWithUser']>[0],
     hashedFingerprintId: Parameters<UserUseCaseInterface['relateFingerprintWithUser']>[1],
diff --git a/apps/api/src/module/user/use-case/user.use-case.ts b/apps/api/src/module/user/use-case/user.use-case.ts
index 1e94649..c01417c 100644
--- a/apps/api/src/module/user/use-case/user.use-case.ts
+++ b/apps/api/src/module/user/use-case/user.use-case.ts
@@ -4,5 +4,6 @@ export interface UserUseCaseInterface {
   findUser(authId: User['authId']): Promise<User | null>;
   findUserByHashedFingerprintId(hashedFingerprintId: NonNullable<User['hashedFingerprintId']>): Promise<User | null>;
   createUser(user: Omit<User, 'id' | 'hashedFingerprintId' | 'lostAndFoundState' | 'isDiscloseAsOwner' | 'createdAt' | 'isOnTheWay'>): Promise<User>;
+  updateUserDisclosure(authId: User['authId'], isDiscloseAsOwner: User['isDiscloseAsOwner']): Promise<User>;
   relateFingerprintWithUser(authId: User['authId'], hashedFingerprintId: NonNullable<User['hashedFingerprintId']>): Promise<User>;
 }
diff --git a/apps/locker-dashboard/graphql.schema.json b/apps/locker-dashboard/graphql.schema.json
index 69b6ba2..82fdec9 100644
--- a/apps/locker-dashboard/graphql.schema.json
+++ b/apps/locker-dashboard/graphql.schema.json
@@ -767,6 +767,55 @@
             },
             "isDeprecated": false,
             "deprecationReason": null
+          },
+          {
+            "name": "updateUserDisclosure",
+            "description": null,
+            "args": [
+              {
+                "name": "isDiscloseAsOwner",
+                "description": null,
+                "type": {
+                  "kind": "NON_NULL",
+                  "name": null,
+                  "ofType": {
+                    "kind": "SCALAR",
+                    "name": "Boolean",
+                    "ofType": null
+                  }
+                },
+                "defaultValue": null,
+                "isDeprecated": false,
+                "deprecationReason": null
+              },
+              {
+                "name": "where",
+                "description": null,
+                "type": {
+                  "kind": "NON_NULL",
+                  "name": null,
+                  "ofType": {
+                    "kind": "INPUT_OBJECT",
+                    "name": "UserWhereAuthIdInput",
+                    "ofType": null
+                  }
+                },
+                "defaultValue": null,
+                "isDeprecated": false,
+                "deprecationReason": null
+              }
+            ],
+            "type": {
+              "kind": "NON_NULL",
+              "name": null,
+              "ofType": {
+                "kind": "OBJECT",
+                "name": "User",
+                "ofType": null
+              }
+            },
+            "isDeprecated": false,
+            "deprecationReason": null
           }
         ],
         "inputFields": null,
diff --git a/apps/website/graphql.schema.json b/apps/website/graphql.schema.json
index 69b6ba2..82fdec9 100644
--- a/apps/website/graphql.schema.json
+++ b/apps/website/graphql.schema.json
@@ -767,6 +767,55 @@
             },
             "isDeprecated": false,
             "deprecationReason": null
+          },
+          {
+            "name": "updateUserDisclosure",
+            "description": null,
+            "args": [
+              {
+                "name": "isDiscloseAsOwner",
+                "description": null,
+                "type": {
+                  "kind": "NON_NULL",
+                  "name": null,
+                  "ofType": {
+                    "kind": "SCALAR",
+                    "name": "Boolean",
+                    "ofType": null
+                  }
+                },
+                "defaultValue": null,
+                "isDeprecated": false,
+                "deprecationReason": null
+              },
+              {
+                "name": "where",
+                "description": null,
+                "type": {
+                  "kind": "NON_NULL",
+                  "name": null,
+                  "ofType": {
+                    "kind": "INPUT_OBJECT",
+                    "name": "UserWhereAuthIdInput",
+                    "ofType": null
+                  }
+                },
+                "defaultValue": null,
+                "isDeprecated": false,
+                "deprecationReason": null
+              }
+            ],
+            "type": {
+              "kind": "NON_NULL",
+              "name": null,
+              "ofType": {
+                "kind": "OBJECT",
+                "name": "User",
+                "ofType": null
+              }
+            },
+            "isDeprecated": false,
+            "deprecationReason": null
           }
         ],
         "inputFields": null,
diff --git a/apps/website/src/common/component/user-action-status-list/user-action-status-list.presenter.tsx b/apps/website/src/common/component/user-action-status-list/user-action-status-list.presenter.tsx
index bde69c7..57a7cae 100644
--- a/apps/website/src/common/component/user-action-status-list/user-action-status-list.presenter.tsx
+++ b/apps/website/src/common/component/user-action-status-list/user-action-status-list.presenter.tsx
@@ -1,5 +1,6 @@
 import { Image } from '@lockerai/core/component/image';
 import { DotIcon } from '@lockerai/core/icon/dot-icon';
+import { UserAvatarPlaceholderIcon } from '@lockerai/core/icon/user-avatar-placeholder-icon';
 import { formatDate } from '@lockerai/core/util/format-date';
 import type { ComponentPropsWithoutRef, ReactNode } from 'react';
 import type { LostItem } from '#website/common/model/lost-item';
@@ -12,40 +13,14 @@ type UserActionStatusListProps = Omit<ComponentPropsWithoutRef<'div'>, 'children
   lostItem: LostItem;
 };
 
-export const UserActionStatusList = ({ user, reporter, owner, lostItem, ...props }: UserActionStatusListProps): ReactNode => (
-  <div className="flex flex-col gap-4" {...props}>
-    <div className="flex items-center gap-3">
-      <Image
-        src={reporter.avatarUrl}
-        alt=""
-        width={36}
-        height={36}
-        priority
-        skeleton={{
-          className: 'rounded-full',
-        }}
-        className="h-9 w-9"
-      />
-      <div className="flex flex-col gap-1">
-        <p className="text-base font-bold text-sage-12">
-          {reporter.name}
-          {reporter.id === user?.id ? <span className="text-sage-11"> (You)</span> : null}
-        </p>
-        <div className="flex flex-col gap-1 tablet:flex-row tablet:items-center">
-          <p className="text-sm text-sage-11 tablet:text-base">{formatDate(lostItem.reportedAt, 'MMM. dd, yyyy HH:mm')} reported</p>
-          {lostItem.deliveredAt && (
-            <>
-              <DotIcon className="hidden h-4 w-4 fill-sage-11 tablet:inline" />
-              <p className="text-sm text-sage-11 tablet:text-base">{formatDate(lostItem.deliveredAt, 'MMM. dd, yyyy HH:mm')} delivered</p>
-            </>
-          )}
-        </div>
-      </div>
-    </div>
-    {owner && lostItem.ownedAt && (
-      <div className="flex items-center gap-3" {...props}>
+export const UserActionStatusList = ({ user, reporter, owner, lostItem, ...props }: UserActionStatusListProps): ReactNode => {
+  const isOwnerDisclose = (user && owner && (user.id === owner.id || owner.isDiscloseAsOwner)) ?? undefined;
+
+  return (
+    <div className="flex flex-col gap-4" {...props}>
+      <div className="flex items-center gap-3">
         <Image
-          src={owner.avatarUrl}
+          src={reporter.avatarUrl}
           alt=""
           width={36}
           height={36}
@@ -57,20 +32,54 @@ export const UserActionStatusList = ({ user, reporter, owner, lostItem, ...props
         />
         <div className="flex flex-col gap-1">
           <p className="text-base font-bold text-sage-12">
-            {owner.name}
-            {owner.id === user?.id ? <span className="text-sage-11"> (You)</span> : null}
+            {reporter.name}
+            {reporter.id === user?.id ? <span className="text-sage-11"> (You)</span> : null}
           </p>
           <div className="flex flex-col gap-1 tablet:flex-row tablet:items-center">
-            <p className="text-sm text-sage-11 tablet:text-base">{formatDate(lostItem.ownedAt, 'MMM. dd, yyyy HH:mm')} owned</p>
-            {lostItem.retrievedAt && (
+            <p className="text-sm text-sage-11 tablet:text-base">{formatDate(lostItem.reportedAt, 'MMM. dd, yyyy HH:mm')} reported</p>
+            {lostItem.deliveredAt && (
               <>
                 <DotIcon className="hidden h-4 w-4 fill-sage-11 tablet:inline" />
-                <p className="text-sm text-sage-11 tablet:text-base">{formatDate(lostItem.retrievedAt, 'MMM. dd, yyyy HH:mm')} retrieved</p>
+                <p className="text-sm text-sage-11 tablet:text-base">{formatDate(lostItem.deliveredAt, 'MMM. dd, yyyy HH:mm')} delivered</p>
               </>
             )}
           </div>
         </div>
       </div>
-    )}
-  </div>
-);
+      {owner && lostItem.ownedAt && (
+        <div className="flex items-center gap-3" {...props}>
+          {isOwnerDisclose ? (
+            <Image
+              src={owner.avatarUrl}
+              alt=""
+              width={36}
+              height={36}
+              priority
+              skeleton={{
+                className: 'rounded-full',
+              }}
+              className="h-9 w-9"
+            />
+          ) : (
+            <UserAvatarPlaceholderIcon className="h-9 w-9 fill-sage-11" />
+          )}
+          <div className="flex flex-col gap-1">
+            <p className="text-base font-bold text-sage-12">
+              {isOwnerDisclose ? owner.name : 'Anonymous'}
+              {owner.id === user?.id ? <span className="text-sage-11"> (You)</span> : null}
+            </p>
+            <div className="flex flex-col gap-1 tablet:flex-row tablet:items-center">
+              <p className="text-sm text-sage-11 tablet:text-base">{formatDate(lostItem.ownedAt, 'MMM. dd, yyyy HH:mm')} owned</p>
+              {lostItem.retrievedAt && (
+                <>
+                  <DotIcon className="hidden h-4 w-4 fill-sage-11 tablet:inline" />
+                  <p className="text-sm text-sage-11 tablet:text-base">{formatDate(lostItem.retrievedAt, 'MMM. dd, yyyy HH:mm')} retrieved</p>
+                </>
+              )}
+            </div>
+          </div>
+        </div>
+      )}
+    </div>
+  );
+};
diff --git a/apps/website/src/common/model/user.ts b/apps/website/src/common/model/user.ts
index 5a86563..edb40fb 100644
--- a/apps/website/src/common/model/user.ts
+++ b/apps/website/src/common/model/user.ts
@@ -7,10 +7,11 @@ export type User = {
   email: string;
   lostAndFoundState: LostAndFoundState;
   avatarUrl: string;
+  isDiscloseAsOwner: boolean;
   createdAt: Date;
 };
 
-export type UserPublicMeta = Pick<User, 'id' | 'name' | 'avatarUrl'>;
+export type UserPublicMeta = Pick<User, 'id' | 'name' | 'avatarUrl' | 'isDiscloseAsOwner'>;
 
 export const mockUser = (user: Partial<User> = {}): User => ({
   id: 'e069eeb2-a239-44c7-9870-acc1af492264',
@@ -19,6 +20,7 @@ export const mockUser = (user: Partial<User> = {}): User => ({
   email: 'example@example.com',
   lostAndFoundState: 'NONE',
   avatarUrl: 'https://avatars.githubusercontent.com/u/1',
+  isDiscloseAsOwner: true,
   createdAt: new Date(0),
   ...user,
 });
diff --git a/apps/website/src/infra/graphql/document/create-user.gql b/apps/website/src/infra/graphql/document/create-user.gql
index 0991262..1b59e8c 100644
--- a/apps/website/src/infra/graphql/document/create-user.gql
+++ b/apps/website/src/infra/graphql/document/create-user.gql
@@ -5,6 +5,7 @@ mutation CreateUser($user: UserCreateInput!) {
     createdAt
     email
     id
+    isDiscloseAsOwner
     lostAndFoundState
     name
   }
diff --git a/apps/website/src/infra/graphql/document/find-user.gql b/apps/website/src/infra/graphql/document/find-user.gql
index 3a7d60f..751115d 100644
--- a/apps/website/src/infra/graphql/document/find-user.gql
+++ b/apps/website/src/infra/graphql/document/find-user.gql
@@ -5,6 +5,7 @@ query FindUser($where: UserWhereAuthIdInput!) {
     createdAt
     email
     id
+    isDiscloseAsOwner
     lostAndFoundState
     name
   }
diff --git a/apps/website/src/infra/graphql/document/fragment/user-public-meta.gql b/apps/website/src/infra/graphql/document/fragment/user-public-meta.gql
index b134653..7c509cd 100644
--- a/apps/website/src/infra/graphql/document/fragment/user-public-meta.gql
+++ b/apps/website/src/infra/graphql/document/fragment/user-public-meta.gql
@@ -1,5 +1,6 @@
 fragment UserPublicMeta on User {
   avatarUrl
   id
+  isDiscloseAsOwner
   name
 }
diff --git a/apps/website/src/infra/graphql/document/relate-fingerprint-with-user.gql b/apps/website/src/infra/graphql/document/relate-fingerprint-with-user.gql
index 0286fad..5f64509 100644
--- a/apps/website/src/infra/graphql/document/relate-fingerprint-with-user.gql
+++ b/apps/website/src/infra/graphql/document/relate-fingerprint-with-user.gql
@@ -5,6 +5,7 @@ mutation RelateFingerprintWithUser($hashedFingerprintId: String!, $where: UserWh
     createdAt
     email
     id
+    isDiscloseAsOwner
     lostAndFoundState
     name
   }
diff --git a/apps/website/src/infra/graphql/document/report-lost-item.gql b/apps/website/src/infra/graphql/document/report-lost-item.gql
index 1b13e71..5ea4e91 100644
--- a/apps/website/src/infra/graphql/document/report-lost-item.gql
+++ b/apps/website/src/infra/graphql/document/report-lost-item.gql
@@ -7,9 +7,7 @@ mutation ReportLostItem($imageFiles: [Upload!]!, $lostItem: LostItemCreateInput!
     ownedAt
     reportedAt
     reporter {
-      id
-      name
-      lostAndFoundState
+      ...UserPublicMeta
     }
     retrievedAt
     title
diff --git a/apps/website/src/infra/graphql/document/update-user-disclosure.gql b/apps/website/src/infra/graphql/document/update-user-disclosure.gql
new file mode 100644
index 0000000..dd409ce
--- /dev/null
+++ b/apps/website/src/infra/graphql/document/update-user-disclosure.gql
@@ -0,0 +1,12 @@
+mutation UpdateUserDisclosure($isDiscloseAsOwner: Boolean!, $where: UserWhereAuthIdInput!) {
+  updateUserDisclosure(isDiscloseAsOwner: $isDiscloseAsOwner, where: $where) {
+    authId
+    avatarUrl
+    createdAt
+    email
+    id
+    isDiscloseAsOwner
+    lostAndFoundState
+    name
+  }
+}
diff --git a/apps/website/src/layout/global/header/component/user-dropdown-menu/user-dropdown-menu.presenter.tsx b/apps/website/src/layout/global/header/component/user-dropdown-menu/user-dropdown-menu.presenter.tsx
index 73d7c16..3b3d04a 100644
--- a/apps/website/src/layout/global/header/component/user-dropdown-menu/user-dropdown-menu.presenter.tsx
+++ b/apps/website/src/layout/global/header/component/user-dropdown-menu/user-dropdown-menu.presenter.tsx
@@ -1,5 +1,6 @@
 'use client';
 
+import { InformationPopover } from '#core/component/information-popover';
 import {
   DropdownMenu,
   DropdownMenuContent,
@@ -11,6 +12,8 @@ import {
 } from '@lockerai/core/component/dropdown-menu';
 import { Image } from '@lockerai/core/component/image';
 import { Link } from '@lockerai/core/component/link';
+import { toast } from '@lockerai/core/component/sonner';
+import { Switch } from '@lockerai/core/component/switch';
 import { Tag } from '@lockerai/core/component/tag';
 import { DashboardIcon } from '@lockerai/core/icon/dashboard-icon';
 import { ExternalLinkIcon } from '@lockerai/core/icon/external-link-icon';
@@ -20,103 +23,131 @@ import { SearchIcon } from '@lockerai/core/icon/search-icon';
 import { SignOutIcon } from '@lockerai/core/icon/sign-out-icon';
 import { UploadIcon } from '@lockerai/core/icon/upload-icon';
 import { cn } from '@lockerai/tailwind';
-import type { ComponentPropsWithoutRef, ReactNode } from 'react';
+import { type ComponentPropsWithoutRef, type ReactNode, useState } from 'react';
 import type { User } from '#website/common/model/user';
+import { updateUserDisclosureUseCase } from '#website/use-case/update-user-disclosure';
 
 type UserDropdownMenuProps = Omit<ComponentPropsWithoutRef<'button'>, 'children' | 'className'> & {
   user: User;
   signOut: () => Promise<void>;
 };
 
-export const UserDropdownMenu = ({ user, signOut, ...props }: UserDropdownMenuProps): ReactNode => (
-  <DropdownMenu>
-    <DropdownMenuTrigger asChild>
-      <button className="relative h-10 w-10 rounded-full drop-shadow-md" {...props}>
-        <Image
-          aria-hidden
-          src={user.avatarUrl}
-          alt=""
-          width={40}
-          height={40}
-          skeleton={{
-            className: cn('absolute inset-0 overflow-visible before:hidden'),
-          }}
-          className="absolute inset-0 h-full w-full rounded-full blur-sm"
-        />
-        <Image
-          src={user.avatarUrl}
-          alt="Your avatar image."
-          width={40}
-          height={40}
-          skeleton={{
-            className: 'rounded-full',
-          }}
-          className="h-10 w-10"
-        />
-      </button>
-    </DropdownMenuTrigger>
-    <DropdownMenuContent align="end">
-      <DropdownMenuLabel className="flex items-center gap-6">
-        <div className="flex flex-col gap-1">
-          <p className="text-base font-bold text-sage-12">{user.name}</p>
-          <p className="text-sm text-sage-11">{user.email}</p>
-        </div>
-        {user.lostAndFoundState !== 'NONE' ? (
-          <Tag variant={{ color: user.lostAndFoundState === 'DELIVERING' ? 'purple' : 'cyan' }}>{user.lostAndFoundState.toLowerCase()}</Tag>
-        ) : null}
-      </DropdownMenuLabel>
-      <DropdownMenuSeparator />
-      <DropdownMenuGroup>
-        <DropdownMenuItem asChild>
-          <Link href="/dashboard">
-            <DashboardIcon className="h-4 w-4 fill-sage-11 transition group-hover:fill-sage-12" />
-            Dashboard
-          </Link>
-        </DropdownMenuItem>
-        <DropdownMenuItem asChild>
-          <Link href="/search">
-            <SearchIcon className="h-4 w-4 fill-sage-11 transition group-hover:fill-sage-12" />
-            Search lost item
-          </Link>
-        </DropdownMenuItem>
-        <DropdownMenuItem asChild>
-          <Link href="/report">
-            <UploadIcon className="h-4 w-4 stroke-sage-11 transition group-hover:stroke-sage-12" />
-            Report dropped lost item
-          </Link>
-        </DropdownMenuItem>
-      </DropdownMenuGroup>
-      <DropdownMenuSeparator />
-      <DropdownMenuGroup>
-        <DropdownMenuItem asChild>
-          <Link href="https://github.com/nitic-pbl-p8/lockerai" external>
-            <GithubIcon className="h-4 w-4 fill-sage-11 transition group-hover:fill-sage-12" />
-            GitHub
-            <ExternalLinkIcon className="ml-auto h-4 w-4 stroke-sage-11 transition group-hover:stroke-sage-12" />
-          </Link>
-        </DropdownMenuItem>
+export const UserDropdownMenu = ({ user, signOut, ...props }: UserDropdownMenuProps): ReactNode => {
+  const [loading, setLoading] = useState(false);
+
+  return (
+    <DropdownMenu>
+      <DropdownMenuTrigger asChild>
+        <button className="relative h-10 w-10 rounded-full drop-shadow-md" {...props}>
+          <Image
+            aria-hidden
+            src={user.avatarUrl}
+            alt=""
+            width={40}
+            height={40}
+            skeleton={{
+              className: cn('absolute inset-0 overflow-visible before:hidden'),
+            }}
+            className="absolute inset-0 h-full w-full rounded-full blur-sm"
+          />
+          <Image
+            src={user.avatarUrl}
+            alt="Your avatar image."
+            width={40}
+            height={40}
+            skeleton={{
+              className: 'rounded-full',
+            }}
+            className="h-10 w-10"
+          />
+        </button>
+      </DropdownMenuTrigger>
+      <DropdownMenuContent align="end">
+        <DropdownMenuLabel className="flex items-center gap-6">
+          <div className="flex flex-col gap-1">
+            <p className="text-base font-bold text-sage-12">{user.name}</p>
+            <p className="text-sm text-sage-11">{user.email}</p>
+          </div>
+          {user.lostAndFoundState !== 'NONE' ? (
+            <Tag variant={{ color: user.lostAndFoundState === 'DELIVERING' ? 'purple' : 'cyan' }}>{user.lostAndFoundState.toLowerCase()}</Tag>
+          ) : null}
+        </DropdownMenuLabel>
+        <DropdownMenuSeparator />
+        <DropdownMenuGroup>
+          <DropdownMenuItem asChild>
+            <Link href="/dashboard">
+              <DashboardIcon className="h-4 w-4 fill-sage-11 transition group-hover:fill-sage-12" />
+              Dashboard
+            </Link>
+          </DropdownMenuItem>
+          <DropdownMenuItem asChild>
+            <Link href="/search">
+              <SearchIcon className="h-4 w-4 fill-sage-11 transition group-hover:fill-sage-12" />
+              Search lost item
+            </Link>
+          </DropdownMenuItem>
+          <DropdownMenuItem asChild>
+            <Link href="/report">
+              <UploadIcon className="h-4 w-4 stroke-sage-11 transition group-hover:stroke-sage-12" />
+              Report dropped lost item
+            </Link>
+          </DropdownMenuItem>
+        </DropdownMenuGroup>
+        <DropdownMenuSeparator />
+        <DropdownMenuGroup>
+          <DropdownMenuItem asChild>
+            <Link href="https://github.com/nitic-pbl-p8/lockerai" external>
+              <GithubIcon className="h-4 w-4 fill-sage-11 transition group-hover:fill-sage-12" />
+              GitHub
+              <ExternalLinkIcon className="ml-auto h-4 w-4 stroke-sage-11 transition group-hover:stroke-sage-12" />
+            </Link>
+          </DropdownMenuItem>
+          <DropdownMenuItem asChild>
+            <Link
+              href="https://www.figma.com/file/xNKAhniAfPPTsL987xRCVe/website?type=design&node-id=20%3A35&mode=design&t=oAlQP6Jqqk0ZcqOy-1"
+              external
+            >
+              <FigmaIcon className="h-4 w-4 fill-sage-11 transition group-hover:fill-sage-12" />
+              Figma
+              <ExternalLinkIcon className="ml-auto h-4 w-4 stroke-sage-11 transition group-hover:stroke-sage-12" />
+            </Link>
+          </DropdownMenuItem>
+        </DropdownMenuGroup>
+        <DropdownMenuSeparator />
+        <DropdownMenuGroup>
+          <DropdownMenuLabel className="flex items-center justify-between gap-6 px-3 py-2">
+            <div className="flex items-center gap-3">
+              <p className="text-base font-bold text-sage-11">Disclosure</p>
+              <InformationPopover description="If on, your name and email will be disclosed to the user who reported your lost item. If it is off, it will not be disclosed and will be anonymized." />
+            </div>
+            <Switch
+              disabled={loading}
+              defaultChecked={user.isDiscloseAsOwner}
+              onCheckedChange={async (checked) => {
+                setLoading(true);
+
+                await updateUserDisclosureUseCase(user.authId, checked);
+                toast.success('Disclosure has been updated', {
+                  description: `Your name and email address will now be ${checked ? 'disclosed' : 'hidden'} to the user who reported your lost item.`,
+                });
+
+                setLoading(false);
+              }}
+            />
+          </DropdownMenuLabel>
+        </DropdownMenuGroup>
+        <DropdownMenuSeparator />
         <DropdownMenuItem asChild>
-          <Link
-            href="https://www.figma.com/file/xNKAhniAfPPTsL987xRCVe/website?type=design&node-id=20%3A35&mode=design&t=oAlQP6Jqqk0ZcqOy-1"
-            external
+          <button
+            onClick={async () => {
+              await signOut();
+            }}
           >
-            <FigmaIcon className="h-4 w-4 fill-sage-11 transition group-hover:fill-sage-12" />
-            Figma
-            <ExternalLinkIcon className="ml-auto h-4 w-4 stroke-sage-11 transition group-hover:stroke-sage-12" />
-          </Link>
+            <SignOutIcon className="h-4 w-4 stroke-sage-11 transition group-hover:stroke-sage-12" />
+            Sign out
+          </button>
         </DropdownMenuItem>
-      </DropdownMenuGroup>
-      <DropdownMenuSeparator />
-      <DropdownMenuItem asChild>
-        <button
-          onClick={async () => {
-            await signOut();
-          }}
-        >
-          <SignOutIcon className="h-4 w-4 stroke-sage-11 transition group-hover:stroke-sage-12" />
-          Sign out
-        </button>
-      </DropdownMenuItem>
-    </DropdownMenuContent>
-  </DropdownMenu>
-);
+      </DropdownMenuContent>
+    </DropdownMenu>
+  );
+};
diff --git a/apps/website/src/module/dashboard/pinned-task-section/pinned-task-section.presenter.tsx b/apps/website/src/module/dashboard/pinned-task-section/pinned-task-section.presenter.tsx
index dd37581..9d7ea92 100644
--- a/apps/website/src/module/dashboard/pinned-task-section/pinned-task-section.presenter.tsx
+++ b/apps/website/src/module/dashboard/pinned-task-section/pinned-task-section.presenter.tsx
@@ -50,9 +50,9 @@ export const PinnedTaskSection = ({ user, currentTargetLostItem, variant, ...pro
   const { beacon, heading } = pinnedTaskSectionVariant({ ...variant });
 
   return (
-    <section className="flex flex-col items-center gap-10 px-6 py-10 tablet:px-16 tablet:py-12" {...props}>
-      <div className="flex flex-col items-center gap-3 tablet:gap-5">
-        <h1 className="flex w-fit flex-col items-center gap-6 tablet:flex-row">
+    <section className="flex flex-col items-center gap-10 px-6 py-10 laptop:px-16 laptop:py-12" {...props}>
+      <div className="flex flex-col items-center gap-3 laptop:gap-5">
+        <h1 className="flex w-fit flex-col items-center gap-6 laptop:flex-row">
           <span className={cn('relative h-6 w-6')}>
             <span
               className={cn(
@@ -62,16 +62,16 @@ export const PinnedTaskSection = ({ user, currentTargetLostItem, variant, ...pro
               )}
             />
           </span>
-          <span className="text-center text-4xl font-bold text-sage-12 tablet:text-5xl">
+          <span className="text-center text-4xl font-bold text-sage-12 laptop:text-5xl">
             You are currently <span className={heading()}>{user.lostAndFoundState.toLowerCase()}</span>
           </span>
         </h1>
-        <p className="max-w-[820px] text-xl text-sage-11 tablet:text-2xl">
+        <p className="max-w-[820px] text-xl text-sage-11 laptop:text-2xl">
           You are in the process of {user.lostAndFoundState.toLowerCase()} a lost item. Please go to the nearest locker and{' '}
           {user.lostAndFoundState === 'DELIVERING' ? 'put in' : 'take out'} the lost item.
         </p>
       </div>
-      <div className="flex flex-col items-center gap-10 tablet:flex-row">
+      <div className="flex flex-col items-center gap-10 laptop:flex-row">
         <figure className="shrink-0">
           <Image
             src={currentTargetLostItem.lostItem.imageUrls[0]!}
@@ -86,8 +86,8 @@ export const PinnedTaskSection = ({ user, currentTargetLostItem, variant, ...pro
         </figure>
         <div className="flex w-fit shrink flex-col gap-7">
           <hgroup className="flex flex-col gap-2">
-            <h2 className="text-2xl font-bold text-sage-12 tablet:text-3xl">{currentTargetLostItem.lostItem.title}</h2>
-            <p className="text-base text-sage-11 tablet:text-lg">{currentTargetLostItem.lostItem.description}</p>
+            <h2 className="text-2xl font-bold text-sage-12 laptop:text-3xl">{currentTargetLostItem.lostItem.title}</h2>
+            <p className="text-base text-sage-11 laptop:text-lg">{currentTargetLostItem.lostItem.description}</p>
           </hgroup>
           <UserActionStatusList
             user={user}
diff --git a/apps/website/src/use-case/create-user/index.ts b/apps/website/src/use-case/create-user/index.ts
index 54470e3..c66f5b5 100644
--- a/apps/website/src/use-case/create-user/index.ts
+++ b/apps/website/src/use-case/create-user/index.ts
@@ -2,7 +2,7 @@ import type { User } from '#website/common/model/user';
 import { CreateUserDocument, type CreateUserMutation, type CreateUserMutationVariables } from '#website/infra/graphql/generated/graphql';
 import { urqlClient } from '#website/infra/urql';
 
-type CreateUserUseCaseInput = Omit<User, 'id' | 'lostAndFoundState' | 'createdAt'>;
+type CreateUserUseCaseInput = Omit<User, 'id' | 'lostAndFoundState' | 'isDiscloseAsOwner' | 'createdAt'>;
 
 type CreateUserUseCase = (user: CreateUserUseCaseInput) => Promise<User>;
 
@@ -26,6 +26,7 @@ export const createUserUseCase: CreateUserUseCase = async ({ authId, name, email
     email: data.createUser.email,
     lostAndFoundState: data.createUser.lostAndFoundState,
     avatarUrl: data.createUser.avatarUrl,
+    isDiscloseAsOwner: data.createUser.isDiscloseAsOwner,
     createdAt: data.createUser.createdAt,
   };
 };
diff --git a/apps/website/src/use-case/find-similar-lost-item/index.ts b/apps/website/src/use-case/find-similar-lost-item/index.ts
index e3dc4af..2f1ccef 100644
--- a/apps/website/src/use-case/find-similar-lost-item/index.ts
+++ b/apps/website/src/use-case/find-similar-lost-item/index.ts
@@ -50,6 +50,7 @@ export const findSimilarLostItemUseCase: FindSimilarLostItemUseCase = async (des
     id: data.findSimilarLostItem.reporter.id,
     name: data.findSimilarLostItem.reporter.name,
     avatarUrl: data.findSimilarLostItem.reporter.avatarUrl,
+    isDiscloseAsOwner: data.findSimilarLostItem.reporter.isDiscloseAsOwner,
   };
 
   return {
diff --git a/apps/website/src/use-case/find-user-lost-items/index.ts b/apps/website/src/use-case/find-user-lost-items/index.ts
index 2cae3cc..541d58f 100644
--- a/apps/website/src/use-case/find-user-lost-items/index.ts
+++ b/apps/website/src/use-case/find-user-lost-items/index.ts
@@ -47,21 +47,23 @@ export const findUserLostItemsUseCase: FindUserLostItemsUseCase = async (authId)
         title: data.findUser!.reportedLostItems[0]!.title,
         description: data.findUser!.reportedLostItems[0]!.description,
         imageUrls: data.findUser!.reportedLostItems[0]!.imageUrls,
-        reportedAt: new Date(data.findUser!.reportedLostItems[0]!.reportedAt),
-        ownedAt: data.findUser!.reportedLostItems[0]!.ownedAt ? new Date(data.findUser!.reportedLostItems[0]!.ownedAt) : null,
-        deliveredAt: data.findUser!.reportedLostItems[0]!.deliveredAt ? new Date(data.findUser!.reportedLostItems[0]!.deliveredAt) : null,
-        retrievedAt: data.findUser!.reportedLostItems[0]!.retrievedAt ? new Date(data.findUser!.reportedLostItems[0]!.retrievedAt) : null,
+        reportedAt: data.findUser!.reportedLostItems[0]!.reportedAt,
+        ownedAt: data.findUser!.reportedLostItems[0]!.ownedAt ? data.findUser!.reportedLostItems[0]!.ownedAt : null,
+        deliveredAt: data.findUser!.reportedLostItems[0]!.deliveredAt ? data.findUser!.reportedLostItems[0]!.deliveredAt : null,
+        retrievedAt: data.findUser!.reportedLostItems[0]!.retrievedAt ? data.findUser!.reportedLostItems[0]!.retrievedAt : null,
       },
       reporter: {
         id: data.findUser!.reportedLostItems[0]!.reporter.id,
         name: data.findUser!.reportedLostItems[0]!.reporter.name,
         avatarUrl: data.findUser!.reportedLostItems[0]!.reporter.avatarUrl,
+        isDiscloseAsOwner: data.findUser!.reportedLostItems[0]!.reporter.isDiscloseAsOwner,
       },
       owner: data.findUser!.reportedLostItems[0]!.owner
         ? {
             id: data.findUser!.reportedLostItems[0]!.owner.id,
             name: data.findUser!.reportedLostItems[0]!.owner.name,
             avatarUrl: data.findUser!.reportedLostItems[0]!.owner.avatarUrl,
+            isDiscloseAsOwner: data.findUser!.reportedLostItems[0]!.owner.isDiscloseAsOwner,
           }
         : null,
     }))
@@ -71,21 +73,23 @@ export const findUserLostItemsUseCase: FindUserLostItemsUseCase = async (authId)
         title: data.findUser!.ownedLostItems[0]!.title,
         description: data.findUser!.ownedLostItems[0]!.description,
         imageUrls: data.findUser!.ownedLostItems[0]!.imageUrls,
-        reportedAt: new Date(data.findUser!.ownedLostItems[0]!.reportedAt),
-        ownedAt: data.findUser!.ownedLostItems[0]!.ownedAt ? new Date(data.findUser!.ownedLostItems[0]!.ownedAt) : null,
-        deliveredAt: data.findUser!.ownedLostItems[0]!.deliveredAt ? new Date(data.findUser!.ownedLostItems[0]!.deliveredAt) : null,
-        retrievedAt: data.findUser!.ownedLostItems[0]!.retrievedAt ? new Date(data.findUser!.ownedLostItems[0]!.retrievedAt) : null,
+        reportedAt: data.findUser!.ownedLostItems[0]!.reportedAt,
+        ownedAt: data.findUser!.ownedLostItems[0]!.ownedAt ? data.findUser!.ownedLostItems[0]!.ownedAt : null,
+        deliveredAt: data.findUser!.ownedLostItems[0]!.deliveredAt ? data.findUser!.ownedLostItems[0]!.deliveredAt : null,
+        retrievedAt: data.findUser!.ownedLostItems[0]!.retrievedAt ? data.findUser!.ownedLostItems[0]!.retrievedAt : null,
       },
       reporter: {
         id: data.findUser!.ownedLostItems[0]!.reporter.id,
         name: data.findUser!.ownedLostItems[0]!.reporter.name,
         avatarUrl: data.findUser!.ownedLostItems[0]!.reporter.avatarUrl,
+        isDiscloseAsOwner: data.findUser!.ownedLostItems[0]!.reporter.isDiscloseAsOwner,
       },
       owner: data.findUser!.ownedLostItems[0]!.owner
         ? {
             id: data.findUser!.ownedLostItems[0]!.owner.id,
             name: data.findUser!.ownedLostItems[0]!.owner.name,
             avatarUrl: data.findUser!.ownedLostItems[0]!.owner.avatarUrl,
+            isDiscloseAsOwner: data.findUser!.ownedLostItems[0]!.owner.isDiscloseAsOwner,
           }
         : null,
     }))
@@ -96,10 +100,10 @@ export const findUserLostItemsUseCase: FindUserLostItemsUseCase = async (authId)
     title: reportedLostItem.title,
     description: reportedLostItem.description,
     imageUrls: reportedLostItem.imageUrls,
-    reportedAt: new Date(reportedLostItem.reportedAt),
-    ownedAt: reportedLostItem.ownedAt ? new Date(reportedLostItem.ownedAt) : null,
-    deliveredAt: reportedLostItem.deliveredAt ? new Date(reportedLostItem.deliveredAt) : null,
-    retrievedAt: reportedLostItem.retrievedAt ? new Date(reportedLostItem.retrievedAt) : null,
+    reportedAt: reportedLostItem.reportedAt,
+    ownedAt: reportedLostItem.ownedAt ? reportedLostItem.ownedAt : null,
+    deliveredAt: reportedLostItem.deliveredAt ? reportedLostItem.deliveredAt : null,
+    retrievedAt: reportedLostItem.retrievedAt ? reportedLostItem.retrievedAt : null,
   }));
 
   const ownedLostItems: LostItem[] = data.findUser.ownedLostItems.map((ownedLostItem) => ({
@@ -107,10 +111,10 @@ export const findUserLostItemsUseCase: FindUserLostItemsUseCase = async (authId)
     title: ownedLostItem.title,
     description: ownedLostItem.description,
     imageUrls: ownedLostItem.imageUrls,
-    reportedAt: new Date(ownedLostItem.reportedAt),
-    ownedAt: ownedLostItem.ownedAt ? new Date(ownedLostItem.ownedAt) : null,
-    deliveredAt: ownedLostItem.deliveredAt ? new Date(ownedLostItem.deliveredAt) : null,
-    retrievedAt: ownedLostItem.retrievedAt ? new Date(ownedLostItem.retrievedAt) : null,
+    reportedAt: ownedLostItem.reportedAt,
+    ownedAt: ownedLostItem.ownedAt ? ownedLostItem.ownedAt : null,
+    deliveredAt: ownedLostItem.deliveredAt ? ownedLostItem.deliveredAt : null,
+    retrievedAt: ownedLostItem.retrievedAt ? ownedLostItem.retrievedAt : null,
   }));
 
   return {
diff --git a/apps/website/src/use-case/find-user/index.ts b/apps/website/src/use-case/find-user/index.ts
index 930cffc..86ea6ee 100644
--- a/apps/website/src/use-case/find-user/index.ts
+++ b/apps/website/src/use-case/find-user/index.ts
@@ -29,6 +29,7 @@ export const findUserUseCase: FindUserUseCase = async (authId) => {
     email: data.findUser.email,
     lostAndFoundState: data.findUser.lostAndFoundState,
     avatarUrl: data.findUser.avatarUrl,
+    isDiscloseAsOwner: data.findUser.isDiscloseAsOwner,
     createdAt: data.findUser.createdAt,
   };
 };
diff --git a/apps/website/src/use-case/relate-fingerprint-with-user/index.ts b/apps/website/src/use-case/relate-fingerprint-with-user/index.ts
index 2890bc9..77a4cfe 100644
--- a/apps/website/src/use-case/relate-fingerprint-with-user/index.ts
+++ b/apps/website/src/use-case/relate-fingerprint-with-user/index.ts
@@ -29,6 +29,7 @@ export const relateFingerprintWithUserUseCase: RelateFingerprintWithUserUseCase
     email: data.relateFingerprintWithUser.email,
     lostAndFoundState: data.relateFingerprintWithUser.lostAndFoundState,
     avatarUrl: data.relateFingerprintWithUser.avatarUrl,
+    isDiscloseAsOwner: data.relateFingerprintWithUser.isDiscloseAsOwner,
     createdAt: data.relateFingerprintWithUser.createdAt,
   };
 };
diff --git a/apps/website/src/use-case/update-user-disclosure/index.ts b/apps/website/src/use-case/update-user-disclosure/index.ts
new file mode 100644
index 0000000..29006e0
--- /dev/null
+++ b/apps/website/src/use-case/update-user-disclosure/index.ts
@@ -0,0 +1,37 @@
+'use server';
+
+import type { User } from '#website/common/model/user';
+import {
+  UpdateUserDisclosureDocument,
+  type UpdateUserDisclosureMutation,
+  type UpdateUserDisclosureMutationVariables,
+} from '#website/infra/graphql/generated/graphql';
+import { urqlClient } from '#website/infra/urql';
+
+type UpdateUserDisclosureUseCase = (authId: User['authId'], isDiscloseAsOwner: boolean) => Promise<User>;
+
+export const updateUserDisclosureUseCase: UpdateUserDisclosureUseCase = async (authId, isDiscloseAsOwner) => {
+  const { data, error } = await urqlClient.mutation<UpdateUserDisclosureMutation, UpdateUserDisclosureMutationVariables>(
+    UpdateUserDisclosureDocument,
+    {
+      where: {
+        authId,
+      },
+      isDiscloseAsOwner,
+    },
+  );
+  if (!data || error) {
+    throw error || new Error('Failed to update user disclosure.');
+  }
+
+  return {
+    id: data.updateUserDisclosure.id,
+    authId: data.updateUserDisclosure.authId,
+    name: data.updateUserDisclosure.name,
+    email: data.updateUserDisclosure.email,
+    lostAndFoundState: data.updateUserDisclosure.lostAndFoundState,
+    avatarUrl: data.updateUserDisclosure.avatarUrl,
+    isDiscloseAsOwner: data.updateUserDisclosure.isDiscloseAsOwner,
+    createdAt: data.updateUserDisclosure.createdAt,
+  };
+};
diff --git a/packages/core/component/information-popover/index.ts b/packages/core/component/information-popover/index.ts
new file mode 100644
index 0000000..83701ae
--- /dev/null
+++ b/packages/core/component/information-popover/index.ts
@@ -0,0 +1 @@
+export { InformationPopover } from './information-popover.presenter';
diff --git a/packages/core/component/information-popover/information-popover.presenter.tsx b/packages/core/component/information-popover/information-popover.presenter.tsx
new file mode 100644
index 0000000..42b563d
--- /dev/null
+++ b/packages/core/component/information-popover/information-popover.presenter.tsx
@@ -0,0 +1,18 @@
+import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
+import { Popover, PopoverContent, PopoverTrigger } from '#core/component/popover';
+import { InformationIcon } from '#core/icon/information-icon';
+
+type InformationPopoverProps = Omit<ComponentPropsWithoutRef<typeof Popover>, 'children'> & {
+  description: string;
+};
+
+export const InformationPopover = ({ description, ...props }: InformationPopoverProps): ReactNode => (
+  <Popover {...props}>
+    <PopoverTrigger className="transition hover:opacity-80">
+      <InformationIcon className="h-3 w-3 fill-sage-11" />
+    </PopoverTrigger>
+    <PopoverContent>
+      <p className="text-sm text-sage-11">{description}</p>
+    </PopoverContent>
+  </Popover>
+);
diff --git a/packages/core/component/switch/index.ts b/packages/core/component/switch/index.ts
new file mode 100644
index 0000000..05aaf3f
--- /dev/null
+++ b/packages/core/component/switch/index.ts
@@ -0,0 +1 @@
+export { Switch } from './switch.presenter';
diff --git a/packages/core/component/switch/switch.presenter.tsx b/packages/core/component/switch/switch.presenter.tsx
new file mode 100644
index 0000000..b3235a6
--- /dev/null
+++ b/packages/core/component/switch/switch.presenter.tsx
@@ -0,0 +1,25 @@
+'use client';
+
+import { cn } from '@lockerai/tailwind';
+import * as RadixUiSwitch from '@radix-ui/react-switch';
+import { type ComponentPropsWithoutRef, type ElementRef, forwardRef } from 'react';
+
+type SelectContentProps = Omit<ComponentPropsWithoutRef<typeof RadixUiSwitch.Root>, 'className'>;
+
+export const Switch = forwardRef<ElementRef<typeof RadixUiSwitch.Root>, Omit<SelectContentProps, 'ref'>>(({ ...props }, ref) => (
+  <RadixUiSwitch.Root
+    className={cn(
+      'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-4 border-transparent shadow-inner transition disabled:cursor-not-allowed disabled:opacity-50 rdx-state-checked:bg-green-11 rdx-state-unchecked:bg-sage-11',
+    )}
+    {...props}
+    ref={ref}
+  >
+    <RadixUiSwitch.Thumb
+      className={cn(
+        'pointer-events-none block h-4 w-4 rounded-full bg-sage-3 shadow-lg ring-0 transition rdx-state-checked:translate-x-5 rdx-state-unchecked:translate-x-0',
+      )}
+    />
+  </RadixUiSwitch.Root>
+));
+
+Switch.displayName = RadixUiSwitch.Root.displayName;
diff --git a/packages/core/icon/information-icon/index.ts b/packages/core/icon/information-icon/index.ts
new file mode 100644
index 0000000..7ef7c74
--- /dev/null
+++ b/packages/core/icon/information-icon/index.ts
@@ -0,0 +1 @@
+export { InformationIcon } from './information-icon.presenter';
diff --git a/packages/core/icon/information-icon/information-icon.presenter.tsx b/packages/core/icon/information-icon/information-icon.presenter.tsx
new file mode 100644
index 0000000..af3f109
--- /dev/null
+++ b/packages/core/icon/information-icon/information-icon.presenter.tsx
@@ -0,0 +1,6 @@
+import type { ComponentPropsWithoutRef, ReactNode } from 'react';
+import { BsInfoCircleFill } from 'react-icons/bs';
+
+type InformationIconProps = Omit<ComponentPropsWithoutRef<'svg'>, 'children'>;
+
+export const InformationIcon = ({ ...props }: InformationIconProps): ReactNode => <BsInfoCircleFill {...props} />;
diff --git a/packages/core/icon/information-icon/information-icon.story.tsx b/packages/core/icon/information-icon/information-icon.story.tsx
new file mode 100644
index 0000000..d632eb9
--- /dev/null
+++ b/packages/core/icon/information-icon/information-icon.story.tsx
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { InformationIcon } from './information-icon.presenter';
+
+type Story = StoryObj<typeof InformationIcon>;
+
+const meta = {
+  component: InformationIcon,
+  argTypes: {},
+} satisfies Meta<typeof InformationIcon>;
+
+export default meta;
+
+export const Default: Story = {};
diff --git a/packages/core/icon/user-avatar-placeholder-icon/index.ts b/packages/core/icon/user-avatar-placeholder-icon/index.ts
new file mode 100644
index 0000000..d412b2f
--- /dev/null
+++ b/packages/core/icon/user-avatar-placeholder-icon/index.ts
@@ -0,0 +1 @@
+export { UserAvatarPlaceholderIcon } from './user-avatar-placeholder-icon.presenter';
diff --git a/packages/core/icon/user-avatar-placeholder-icon/user-avatar-placeholder-icon.presenter.tsx b/packages/core/icon/user-avatar-placeholder-icon/user-avatar-placeholder-icon.presenter.tsx
new file mode 100644
index 0000000..624e6fb
--- /dev/null
+++ b/packages/core/icon/user-avatar-placeholder-icon/user-avatar-placeholder-icon.presenter.tsx
@@ -0,0 +1,6 @@
+import type { ComponentPropsWithoutRef, ReactNode } from 'react';
+import { FaCircleUser } from 'react-icons/fa6';
+
+type UserAvatarPlaceholderIconProps = Omit<ComponentPropsWithoutRef<'svg'>, 'children'>;
+
+export const UserAvatarPlaceholderIcon = ({ ...props }: UserAvatarPlaceholderIconProps): ReactNode => <FaCircleUser {...props} />;
diff --git a/packages/core/icon/user-avatar-placeholder-icon/user-avatar-placeholder-icon.story.tsx b/packages/core/icon/user-avatar-placeholder-icon/user-avatar-placeholder-icon.story.tsx
new file mode 100644
index 0000000..cd02275
--- /dev/null
+++ b/packages/core/icon/user-avatar-placeholder-icon/user-avatar-placeholder-icon.story.tsx
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { UserAvatarPlaceholderIcon } from './user-avatar-placeholder-icon.presenter';
+
+type Story = StoryObj<typeof UserAvatarPlaceholderIcon>;
+
+const meta = {
+  component: UserAvatarPlaceholderIcon,
+  argTypes: {},
+} satisfies Meta<typeof UserAvatarPlaceholderIcon>;
+
+export default meta;
+
+export const Default: Story = {};
diff --git a/packages/core/package.json b/packages/core/package.json
index 2921e6d..18d93f4 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -20,6 +20,7 @@
     "@radix-ui/react-dropdown-menu": "2.0.6",
     "@radix-ui/react-popover": "1.0.7",
     "@radix-ui/react-select": "2.0.0",
+    "@radix-ui/react-switch": "1.0.3",
     "date-fns": "2.30.0",
     "date-fns-tz": "2.0.0",
     "dotenv": "16.3.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 671fe73..b423416 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -506,6 +506,9 @@ importers:
       '@radix-ui/react-select':
         specifier: 2.0.0
         version: 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-switch':
+        specifier: 1.0.3
+        version: 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0)
       date-fns:
         specifier: 2.30.0
         version: 2.30.0
@@ -5648,6 +5651,33 @@ packages:
       '@types/react': 18.2.47
       react: 18.2.0
 
+  /@radix-ui/react-switch@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.8
+      '@radix-ui/primitive': 1.0.1
+      '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.47)(react@18.2.0)
+      '@radix-ui/react-context': 1.0.1(@types/react@18.2.47)(react@18.2.0)
+      '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.47)(react@18.2.0)
+      '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.47)(react@18.2.0)
+      '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.47)(react@18.2.0)
+      '@types/react': 18.2.47
+      '@types/react-dom': 18.2.18
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: false
+
   /@radix-ui/react-toggle-group@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0):
     resolution: {integrity: sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==}
     peerDependencies: