Refactor avatar components across the application to use the new UserAvatar component, replacing instances of the Avatar component for improved consistency and maintainability.

This commit is contained in:
Rotimi Best
2026-03-28 23:22:55 +01:00
parent 935b1c493d
commit 2e38f7541b
18 changed files with 78 additions and 123 deletions
@@ -1,10 +1,9 @@
<script lang="ts">
import * as Table from '@cio/ui/base/table';
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
import * as Avatar from '@cio/ui/base/avatar';
import { Checkbox } from '@cio/ui/base/checkbox';
import { Badge } from '@cio/ui/base/badge';
import UserIcon from '@lucide/svelte/icons/user';
import { UserAvatar } from '@cio/ui/custom/user-avatar';
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
import type { OrganizationAudienceMember } from '$features/org/utils/types';
import { t } from '$lib/utils/functions/translations';
@@ -40,26 +39,12 @@
<Table.Cell>
{#if memberDetailHref}
<a href={memberDetailHref} class="ui:text-primary flex items-center gap-2 hover:underline">
<Avatar.Root class="h-5 w-5">
{#if row.avatarUrl}
<Avatar.Image src={row.avatarUrl} alt={row.name ? row.name : 'User'} />
{/if}
<Avatar.Fallback>
<UserIcon class="ui:size-3 ui:text-muted-foreground" />
</Avatar.Fallback>
</Avatar.Root>
<UserAvatar src={row.avatarUrl} alt={row.name ?? 'User'} class="h-5 w-5" />
{row.name}
</a>
{:else}
<span class="flex items-center gap-2">
<Avatar.Root class="h-5 w-5">
{#if row.avatarUrl}
<Avatar.Image src={row.avatarUrl} alt={row.name ? row.name : 'User'} />
{/if}
<Avatar.Fallback>
<UserIcon class="ui:size-3 ui:text-muted-foreground" />
</Avatar.Fallback>
</Avatar.Root>
<UserAvatar src={row.avatarUrl} alt={row.name ?? 'User'} class="h-5 w-5" />
{row.name}
</span>
{/if}
@@ -1,7 +1,6 @@
<script lang="ts">
import * as Avatar from '@cio/ui/base/avatar';
import TrashIcon from '@lucide/svelte/icons/trash';
import UserIcon from '@lucide/svelte/icons/user';
import { UserAvatar } from '@cio/ui/custom/user-avatar';
import { calDateDiff } from '$lib/utils/functions/date';
import { IconButton } from '@cio/ui/custom/icon-button';
import { Vote } from '$features/ui';
@@ -39,17 +38,7 @@
<div class="border-gray w-full rounded-lg border">
<header class="flex items-center justify-between p-2 leading-none">
<div class="flex items-center text-black">
<Avatar.Root class="size-7">
{#if comment.author?.avatarUrl}
<Avatar.Image
src={comment.author.avatarUrl}
alt={comment.author?.fullname ? comment.author.fullname : 'User'}
/>
{/if}
<Avatar.Fallback>
<UserIcon class="ui:size-3.5 ui:text-muted-foreground" />
</Avatar.Fallback>
</Avatar.Root>
<UserAvatar src={comment.author?.avatarUrl} alt={comment.author?.fullname ?? 'User'} class="size-7" />
<p class="ml-2 text-sm dark:text-white">{comment.author?.fullname}</p>
@@ -11,7 +11,7 @@
import { t } from '$lib/utils/functions/translations';
import EmptyState from './empty-state.svelte';
import * as Avatar from '@cio/ui/base/avatar';
import { UserAvatar } from '@cio/ui/custom/user-avatar';
import type { CourseAnalytics } from '$features/course/utils/types';
interface Props {
@@ -80,17 +80,11 @@
class="sticky left-0 z-10 min-w-[200px] bg-white px-4 py-3 transition-colors group-hover:bg-gray-50 dark:bg-neutral-800 dark:group-hover:bg-gray-700"
>
<div class="flex items-center gap-3">
<Avatar.Root class="size-8">
{#if student.profile.avatar_url}
<Avatar.Image
src={student.profile.avatar_url}
alt={student.profile.fullname ? student.profile.fullname : $t('analytics.student')}
/>
{/if}
<Avatar.Fallback>
<UserIcon class="ui:size-4 ui:text-muted-foreground" />
</Avatar.Fallback>
</Avatar.Root>
<UserAvatar
src={student.profile.avatar_url}
alt={student.profile.fullname ?? $t('analytics.student')}
class="size-8"
/>
<div class="min-w-0 flex-1">
<p class="truncate font-medium text-gray-900 dark:text-white">
{student.profile.fullname}
@@ -14,6 +14,7 @@
import * as Dialog from '@cio/ui/base/dialog';
import { TextareaField } from '@cio/ui/custom/textarea-field';
import { Button } from '@cio/ui/base/button';
import { UserAvatar } from '@cio/ui/custom/user-avatar';
// import { useCompletion } from 'ai/svelte';
// import { QUESTION_TYPE } from '$features/ui/question/constants';
@@ -330,10 +331,10 @@
</p>
{#if data.student}
<div class="flex flex-row items-center justify-center rounded-md bg-gray-100 p-[6px] dark:bg-neutral-700">
<img
alt={$t('course.navItem.submissions.grading_modal.student_avatar')}
class="flex h-5 w-5 rounded-full"
<UserAvatar
src={data.student.avatarUrl}
alt={$t('course.navItem.submissions.grading_modal.student_avatar')}
class="h-5 w-5"
/>
<p class="ml-2 line-clamp-1 text-sm font-semibold dark:text-white">
{data.student.fullname}
@@ -1,6 +1,7 @@
<script lang="ts">
import { Spinner } from '@cio/ui/base/spinner';
import { ExerciseQuestion } from '@cio/ui';
import { UserAvatar } from '@cio/ui/custom/user-avatar';
import { submissions } from './store';
import { questionnaire } from '../store';
@@ -17,9 +18,6 @@
let { isLoading = $bindable(false) }: Props = $props();
const defaultImg =
'https://images.unsplash.com/photo-1599566150163-29194dcaad36?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8YXZhdGFyc3xlbnwwfHwwfHx8Mg%3D%3D';
let studentSelected = $state(0);
const questionLabels = $derived(getExerciseQuestionLabels());
@@ -57,10 +55,10 @@
studentSelected == i ? 'border-primary-700 border-[3px]' : ''
}`}
>
<img
src={student.groupmember?.profile.avatarUrl ? student.groupmember.profile.avatarUrl : defaultImg}
<UserAvatar
src={student.groupmember?.profile.avatarUrl}
alt={$t('course.navItem.lessons.exercises.all_exercises.analytics.individual.student_avatar')}
class="m-1 max-h-10 w-10 rounded-full bg-white"
class="m-1 size-10"
/>
</div>
<p class="line-clamp-2 w-20 leading-4 whitespace-pre-wrap">
@@ -2,9 +2,7 @@
import { untrack } from 'svelte';
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
import UserIcon from '@lucide/svelte/icons/user';
import * as Avatar from '@cio/ui/base/avatar';
import { UserAvatar } from '@cio/ui/custom/user-avatar';
import { TextareaField } from '@cio/ui/custom/textarea-field';
import { DeleteModal } from '$features/ui';
import { Button } from '@cio/ui/base/button';
@@ -98,14 +96,7 @@
</div>
<div>
<div class="flex h-full items-start gap-3">
<Avatar.Root class="mt-2 size-6!">
{#if $profile.avatarUrl}
<Avatar.Image src={$profile.avatarUrl} alt={$profile.fullname ? $profile.fullname : 'User'} />
{/if}
<Avatar.Fallback>
<UserIcon class="ui:size-3 ui:text-muted-foreground" />
</Avatar.Fallback>
</Avatar.Root>
<UserAvatar src={$profile.avatarUrl} alt={$profile.fullname ?? 'User'} class="mt-2 size-6!" />
<div class="h-full w-full">
<TextareaField
label={$t('course.navItem.lessons.comments.text_area_title')}
@@ -125,14 +116,7 @@
<div class="my-5">
{#each comments as commentItem (commentItem.id)}
<div class="mt-2 flex items-start gap-3 pb-2">
<Avatar.Root class="mt-2 size-6!">
{#if commentItem.avatar}
<Avatar.Image src={commentItem.avatar} alt={commentItem.name ? commentItem.name : 'User'} />
{/if}
<Avatar.Fallback>
<UserIcon class="ui:size-3 ui:text-muted-foreground" />
</Avatar.Fallback>
</Avatar.Root>
<UserAvatar src={commentItem.avatar} alt={commentItem.name ?? 'User'} class="mt-2 size-6!" />
<div class="w-full rounded-md border px-3 py-2 dark:border-neutral-700">
<div class="flex items-center justify-between gap-2">
@@ -12,6 +12,7 @@
import { newsfeedApi } from '$features/course/api';
import { Button } from '@cio/ui/base/button';
import { UserAvatar } from '@cio/ui/custom/user-avatar';
import type { Feed } from '$features/course/utils/types';
import type { NewsfeedCommentsByFeedId } from '$features/course/api';
@@ -193,13 +194,7 @@
<!-- Input for new comment -->
<div class="flex items-center justify-between gap-2">
<div class="h-7 w-7">
<img
src={author.avatarUrl}
alt={$t('course.navItem.news_feed.user_avatar_alt')}
class="h-full w-full rounded-full object-cover"
/>
</div>
<UserAvatar src={author.avatarUrl} alt={$t('course.navItem.news_feed.user_avatar_alt')} class="size-7" />
<div class="flex-1">
<input
type="text"
@@ -1,11 +1,10 @@
<script lang="ts">
import { Empty } from '@cio/ui/custom/empty';
import UserXIcon from '@lucide/svelte/icons/user-x';
import * as Avatar from '@cio/ui/base/avatar';
import { UserAvatar } from '@cio/ui/custom/user-avatar';
import * as Table from '@cio/ui/base/table';
import * as Tooltip from '@cio/ui/base/tooltip';
import { t } from '$lib/utils/functions/translations';
import { shortenName } from '$lib/utils/functions/string';
import { calculateStudentAverage, type MarksPageData, type ExerciseInfo } from '$features/course/utils/marks-utils';
import TruncatedWithTooltip from '$features/course/components/truncated-with-tooltip.svelte';
@@ -70,15 +69,11 @@
class="sticky left-0 min-w-[200px] border-r border-gray-200 bg-gray-50 px-3 py-2 dark:border-neutral-700 dark:bg-neutral-800/80"
>
<div class="flex items-center gap-2">
<Avatar.Root class="size-6! shrink-0 rounded-full">
<Avatar.Image
src={student.profile?.avatarUrl ?? undefined}
alt={student.profile?.fullname ?? 'Student'}
/>
<Avatar.Fallback class="ui:bg-muted ui:text-muted-foreground rounded-full">
{shortenName(student.profile?.fullname) ?? 'S'}
</Avatar.Fallback>
</Avatar.Root>
<UserAvatar
src={student.profile?.avatarUrl}
alt={student.profile?.fullname ?? 'Student'}
class="size-6! shrink-0"
/>
<div class="min-w-0 flex-1">
<p class="ui:text-foreground truncate text-sm font-medium">
{student.profile?.fullname ?? '-'}
@@ -10,6 +10,7 @@
import { t } from '$lib/utils/functions/translations';
import { Chip } from '@cio/ui/custom/chip';
import { UserAvatar } from '@cio/ui/custom/user-avatar';
import MarkExerciseModal from '$features/course/components/exercise/mark-exercise-modal.svelte';
import { STATUS } from '$features/course/components/exercise/constants';
import { onMount } from 'svelte';
@@ -242,7 +243,7 @@
class="mb-2 flex w-full cursor-pointer items-center text-black"
href={`${page.url.pathname}?submissionId=${item.id}`}
>
<img alt="Student avatar" class="block h-6 w-6 rounded-full" src={item.student.avatarUrl} />
<UserAvatar src={item.student.avatarUrl} alt="Student avatar" class="h-6 w-6" />
<p class="ml-2 text-sm dark:text-white">
{item.student.username}
</p>
@@ -1,11 +1,10 @@
<script lang="ts">
import * as Avatar from '@cio/ui/base/avatar';
import { UserAvatar } from '@cio/ui/custom/user-avatar';
import { calDateDiff } from '$lib/utils/functions/date';
import { t } from '$lib/utils/functions/translations';
import type { UserAnalytics } from '$lib/utils/types/analytics';
import AlarmClockIcon from '@lucide/svelte/icons/alarm-clock';
import MailIcon from '@lucide/svelte/icons/mail';
import UserIcon from '@lucide/svelte/icons/user';
interface Props {
user: UserAnalytics['user'];
@@ -16,14 +15,7 @@
<div class="rounded-md border p-5">
<div class="flex w-full flex-col items-center justify-start gap-4 text-start md:flex-row">
<Avatar.Root class="h-16 w-16">
{#if user.avatarUrl}
<Avatar.Image src={user.avatarUrl} alt={user.fullName ? user.fullName : 'User'} />
{/if}
<Avatar.Fallback>
<UserIcon class="ui:size-8 ui:text-muted-foreground" />
</Avatar.Fallback>
</Avatar.Root>
<UserAvatar src={user.avatarUrl} alt={user.fullName ?? 'User'} class="h-16 w-16" />
<div class="flex flex-col space-y-2">
<p class="text-center text-2xl md:text-left dark:text-white">
{user.fullName}
@@ -21,7 +21,7 @@
if (item.id === 'profile') {
return {
...item,
is_completed: !$profile.avatarUrl?.includes('avatars/avatar.png')
is_completed: !$profile.avatarUrl?.includes('avatars/avatar.png') || !!$profile.avatarUrl
};
}
return item;
@@ -46,7 +46,7 @@
</Button>
</div>
{#each setupList as item}
{#each setupList as item (item.id)}
<div class="flex items-center gap-2 text-sm">
{#if item.is_completed}
<BadgeCheckIcon class="custom size-4 shrink-0 text-green-600! opacity-50" />
@@ -7,8 +7,8 @@
import BellPlusIcon from '@lucide/svelte/icons/bell-plus';
import BadgeHelpIcon from '@lucide/svelte/icons/badge-help';
import LogOutIcon from '@lucide/svelte/icons/log-out';
import * as Avatar from '@cio/ui/base/avatar';
import * as Sidebar from '@cio/ui/base/sidebar';
import { UserAvatar } from '@cio/ui/custom/user-avatar';
import { useSidebar } from '@cio/ui/base/sidebar';
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
import ThemeToggle from './theme-toggle.svelte';
@@ -25,10 +25,7 @@
-->
{#snippet avatarblock()}
<Avatar.Root>
<Avatar.Image src={$profile.avatarUrl} alt={$profile.fullname} />
<Avatar.Fallback class="rounded-lg">{$profile.fullname.slice(0, 2)}</Avatar.Fallback>
</Avatar.Root>
<UserAvatar src={$profile.avatarUrl} alt={$profile.fullname} />
<div class="grid flex-1 text-left text-sm leading-tight font-normal">
<span class="truncate">{$profile.fullname}</span>
<span class="truncate text-xs">{$profile.email}</span>
@@ -13,7 +13,7 @@
import { currentOrgPath } from '$lib/utils/store/org';
import { CreateCourseButton } from '$features/course/components';
import * as Avatar from '@cio/ui/base/avatar';
import { UserAvatar } from '@cio/ui/custom/user-avatar';
import { Progress } from '@cio/ui/base/progress';
import { Button } from '@cio/ui/base/button';
import { WelcomeModal } from '$features/onboarding/components';
@@ -146,14 +146,7 @@
{#each enrollments as enrollment, i (i)}
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<Avatar.Root class="h-6 w-6">
{#if enrollment.avatarUrl}
<Avatar.Image src={enrollment.avatarUrl} alt={enrollment.name ? enrollment.name : 'User'} />
{/if}
<Avatar.Fallback>
<UserIcon class="ui:size-3 ui:text-muted-foreground" />
</Avatar.Fallback>
</Avatar.Root>
<UserAvatar src={enrollment.avatarUrl} alt={enrollment.name ?? 'User'} class="h-6 w-6" />
<div class="min-h-[45px] space-y-1">
<p class="text-sm leading-none capitalize">{enrollment.name}</p>
@@ -31,7 +31,7 @@
if (item.id === 'profile') {
return {
...item,
is_completed: !$profile.avatarUrl?.includes('avatars/avatar.png')
is_completed: !$profile.avatarUrl?.includes('avatars/avatar.png') || !!$profile.avatarUrl
};
}
return item;
+11 -5
View File
@@ -288,9 +288,7 @@ export const profile = pgTable(
id: uuid().primaryKey().notNull(),
fullname: text().notNull(),
username: text().notNull(),
avatarUrl: text('avatar_url').default(
'https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/avatar.png'
),
avatarUrl: text('avatar_url'),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
email: varchar(),
@@ -591,6 +589,11 @@ export const course = pgTable(
isPublished: boolean('is_published').default(false),
isCertificateDownloadable: boolean('is_certificate_downloadable').default(false),
certificateTheme: text('certificate_theme'),
certificationDeadline: timestamp('certification_deadline', { withTimezone: true, mode: 'string' }),
certificationThreshold: integer('certification_threshold').default(100),
certificationRequiredExerciseId: uuid('certification_required_exercise_id'),
certificationExerciseMinScorePercent: integer('certification_exercise_min_score_percent'),
certificationEmailMessage: text('certification_email_message'),
status: text().default('ACTIVE').notNull(),
type: courseType().default('LIVE_CLASS')
},
@@ -904,7 +907,8 @@ export const exercise = pgTable(
.primaryKey()
.notNull(),
isUnlocked: boolean('is_unlocked').default(true),
dueBy: timestamp('due_by', { mode: 'string' })
dueBy: timestamp('due_by', { mode: 'string' }),
allowMultipleAttempts: boolean('allow_multiple_attempts').default(false).notNull()
},
(table) => [
foreignKey({
@@ -938,7 +942,9 @@ export const groupmember = pgTable(
profileId: uuid('profile_id'),
email: varchar(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(),
assignedStudentId: varchar('assigned_student_id')
assignedStudentId: varchar('assigned_student_id'),
certificateEarnedAt: timestamp('certificate_earned_at', { withTimezone: true, mode: 'string' }),
certificationEmailSentAt: timestamp('certification_email_sent_at', { withTimezone: true, mode: 'string' })
},
(table) => [
foreignKey({
-1
View File
@@ -29,7 +29,6 @@ export async function seedProfile({ usersData }: { usersData: TUserSeedData[] })
fullname: resolveFullName(user),
username: `${user.email.split('@')[0]}${Date.now()}`,
email: user.email,
avatarUrl: 'https://pgrest.classroomio.com/storage/v1/object/public/avatars/avatar.png',
canAddCourse: true,
isEmailVerified: true
}))
@@ -0,0 +1,4 @@
import UserAvatar from './user-avatar.svelte';
export { UserAvatar };
export { UserAvatar as default };
@@ -0,0 +1,22 @@
<script lang="ts">
import * as Avatar from '../../base/avatar';
import UserIcon from '@lucide/svelte/icons/user';
interface Props {
src?: string | null;
alt?: string;
class?: string;
iconClass?: string;
}
let { src, alt = 'User', class: className = 'size-6', iconClass = '' }: Props = $props();
</script>
<Avatar.Root class={className}>
{#if src}
<Avatar.Image {src} {alt} />
{/if}
<Avatar.Fallback>
<UserIcon class="ui:text-muted-foreground {iconClass || 'h-1/2 w-1/2'}" />
</Avatar.Fallback>
</Avatar.Root>