standardize loading indicators with shared spinner asset

Replace ad-hoc Loader2 loading indicators with SpinnerIcon so loading states across content and notifications pages use the same Circular-indeterminate progress indicator.

Made-with: Cursor
This commit is contained in:
Yared Yemane 2026-04-15 04:30:07 -07:00
parent d33bacf628
commit 1f0046a8ee
5 changed files with 25 additions and 20 deletions

View File

@ -1,5 +1,5 @@
import { useMemo, useState, type ChangeEvent } from "react"
import { ArrowLeft, ArrowRight, Check, GripVertical, Loader2, Plus, Rocket, Trash2, Upload } from "lucide-react"
import { ArrowLeft, ArrowRight, Check, GripVertical, Plus, Rocket, Trash2, Upload } from "lucide-react"
import { Link, useLocation, useNavigate, useParams } from "react-router-dom"
import { toast } from "sonner"
import { addQuestionToSet, createLesson, createQuestion } from "../../api/courses.api"
@ -8,6 +8,7 @@ import { PracticeQuestionEditorFields } from "../../components/content-managemen
import { Button } from "../../components/ui/button"
import { Card } from "../../components/ui/card"
import { Input } from "../../components/ui/input"
import { SpinnerIcon } from "../../components/ui/spinner-icon"
import type { QuestionOption } from "../../types/course.types"
type Step = 1 | 2 | 3 | 4
@ -356,7 +357,7 @@ export function AddNewLessonPage() {
/>
<div className="flex flex-wrap items-center gap-2">
<label className="inline-flex cursor-pointer items-center gap-2 rounded-md border border-grayScale-200 px-3 py-2 text-xs text-grayScale-700 hover:bg-grayScale-50">
{uploadingIntroVideo ? <Loader2 className="h-4 w-4 animate-spin" /> : <Upload className="h-4 w-4" />}
{uploadingIntroVideo ? <SpinnerIcon className="h-4 w-4" alt="" /> : <Upload className="h-4 w-4" />}
{uploadingIntroVideo ? "Uploading..." : "Upload video from computer"}
<input
type="file"

View File

@ -1,6 +1,6 @@
import { useMemo, useRef, useState, type ChangeEvent } from "react"
import { Link, useLocation, useParams, useNavigate } from "react-router-dom"
import { ArrowLeft, ArrowRight, ChevronDown, Grid3X3, Check, Plus, Trash2, GripVertical, Edit, Rocket, Loader2, Upload } from "lucide-react"
import { ArrowLeft, ArrowRight, ChevronDown, Grid3X3, Check, Plus, Trash2, GripVertical, Edit, Rocket, Upload } from "lucide-react"
import { toast } from "sonner"
import { Card } from "../../components/ui/card"
import { Button } from "../../components/ui/button"
@ -9,6 +9,7 @@ import { PracticeQuestionEditorFields } from "../../components/content-managemen
import { createQuestionSet, createQuestion, addQuestionToSet } from "../../api/courses.api"
import { uploadVideoFile } from "../../api/files.api"
import { Select } from "../../components/ui/select"
import { SpinnerIcon } from "../../components/ui/spinner-icon"
import type { QuestionOption } from "../../types/course.types"
type Step = 1 | 2 | 3 | 4 | 5
@ -526,7 +527,7 @@ export function AddNewPracticePage() {
className="gap-1.5"
>
{uploadingIntroVideo ? (
<Loader2 className="h-4 w-4 animate-spin" />
<SpinnerIcon className="h-4 w-4" alt="" />
) : (
<Upload className="h-4 w-4" />
)}
@ -541,7 +542,7 @@ export function AddNewPracticePage() {
>
{importingIntroVideoUrl ? (
<>
<Loader2 className="mr-1.5 h-4 w-4 animate-spin" />
<SpinnerIcon className="mr-1.5 h-4 w-4" alt="" />
Importing URL
</>
) : (

View File

@ -10,7 +10,6 @@ import {
Languages,
Lightbulb,
Link2,
Loader2,
Mic,
Plus,
Search,
@ -276,7 +275,7 @@ function MediaPreviewCard({
) : null}
{resolving ? (
<div className="flex items-center gap-2 rounded-md border border-grayScale-100 bg-grayScale-50 px-3 py-2 text-xs text-grayScale-500">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
<SpinnerIcon className="h-3.5 w-3.5" alt="" />
Resolving media URL...
</div>
) : mediaType === "image" ? (
@ -460,8 +459,11 @@ export function HumanLanguagePage() {
const saved = sessionStorage.getItem(HUMAN_LANGUAGE_SCROLL_KEY)
const targetY = saved ? Number(saved) : 0
if (Number.isFinite(targetY) && targetY > 0) {
window.requestAnimationFrame(() => window.scrollTo({ top: targetY, behavior: "auto" }))
setTimeout(() => window.scrollTo({ top: targetY, behavior: "auto" }), 250)
const restoreBehavior = window.matchMedia("(prefers-reduced-motion: reduce)").matches
? "auto"
: "smooth"
window.requestAnimationFrame(() => window.scrollTo({ top: targetY, behavior: restoreBehavior }))
setTimeout(() => window.scrollTo({ top: targetY, behavior: restoreBehavior }), 250)
}
} catch (error) {
console.error("Failed to load human-language hierarchy:", error)
@ -1625,7 +1627,7 @@ export function HumanLanguagePage() {
disabled={creatingKey === `module-${course.course_id}-${level}`}
>
{creatingKey === `module-${course.course_id}-${level}` ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
<SpinnerIcon className="h-3.5 w-3.5" alt="" />
) : (
<Plus className="h-3.5 w-3.5" />
)}
@ -1674,7 +1676,7 @@ export function HumanLanguagePage() {
disabled={creatingKey === `submodule-${course.course_id}-${level}-${parseModuleNumber(module.title) ?? 0}`}
>
{creatingKey === `submodule-${course.course_id}-${level}-${parseModuleNumber(module.title) ?? 0}` ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
<SpinnerIcon className="h-3.5 w-3.5" alt="" />
) : (
<Plus className="h-3.5 w-3.5" />
)}
@ -2152,7 +2154,7 @@ export function HumanLanguagePage() {
<div className="p-4">
{!practiceFetch || practiceFetch.status === "loading" ? (
<div className="flex flex-col items-center justify-center gap-2 py-12 text-sm text-grayScale-500">
<Loader2 className="h-5 w-5 animate-spin text-brand-500" aria-hidden />
<SpinnerIcon className="h-5 w-5 text-brand-500" alt="" />
Loading questions
</div>
) : null}
@ -2245,7 +2247,7 @@ export function HumanLanguagePage() {
>
{loadingQuestionEditId ===
(q.question_id ?? q.id) ? (
<Loader2 className="h-3 w-3 animate-spin" aria-hidden />
<SpinnerIcon className="h-3 w-3" alt="" />
) : null}
Edit
</Button>
@ -2422,7 +2424,7 @@ export function HumanLanguagePage() {
</DialogHeader>
{loadingPracticeForm ? (
<div className="flex items-center gap-2 rounded-lg border border-grayScale-200 bg-grayScale-50 px-3 py-3 text-sm text-grayScale-600">
<Loader2 className="h-4 w-4 animate-spin" />
<SpinnerIcon className="h-4 w-4" alt="" />
Loading practice details...
</div>
) : (
@ -2487,7 +2489,7 @@ export function HumanLanguagePage() {
/>
<div className="flex flex-wrap items-center gap-2">
<label className="inline-flex cursor-pointer items-center gap-2 rounded-md border border-grayScale-200 px-3 py-2 text-xs text-grayScale-700 hover:bg-grayScale-50">
{uploadingPracticeIntroVideo ? <Loader2 className="h-4 w-4 animate-spin" /> : <Video className="h-4 w-4" />}
{uploadingPracticeIntroVideo ? <SpinnerIcon className="h-4 w-4" alt="" /> : <Video className="h-4 w-4" />}
{uploadingPracticeIntroVideo ? "Uploading..." : "Upload intro video"}
<input
type="file"

View File

@ -1,5 +1,5 @@
import { type ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { ArrowLeft, ChevronDown, ChevronRight, Image as ImageIcon, Loader2, Mic, Plus, Trash2, Upload } from "lucide-react"
import { ArrowLeft, ChevronDown, ChevronRight, Image as ImageIcon, Mic, Plus, Trash2, Upload } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card"
import { Button } from "../../components/ui/button"
import { Input } from "../../components/ui/input"
@ -1926,7 +1926,7 @@ export function SpeakingPage() {
className="gap-1.5"
>
{uploadingIntroVideo ? (
<Loader2 className="h-4 w-4 animate-spin" />
<SpinnerIcon className="h-4 w-4" alt="" />
) : (
<Upload className="h-4 w-4" />
)}

View File

@ -1,11 +1,12 @@
import { useEffect, useMemo, useState } from "react"
import { useNavigate } from "react-router-dom"
import { Bell, Loader2, Mail, MailOpen, Megaphone } from "lucide-react"
import { Bell, Mail, MailOpen, Megaphone } from "lucide-react"
import { Card, CardContent } from "../../components/ui/card"
import { Button } from "../../components/ui/button"
import { Input } from "../../components/ui/input"
import { Textarea } from "../../components/ui/textarea"
import { FileUpload } from "../../components/ui/file-upload"
import { SpinnerIcon } from "../../components/ui/spinner-icon"
import { cn } from "../../lib/utils"
import { getTeamMembers } from "../../api/team.api"
import type { TeamMember } from "../../types/team.types"
@ -282,7 +283,7 @@ export function CreateNotificationPage() {
>
{sending ? (
<>
<Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" />
<SpinnerIcon className="mr-2 h-3.5 w-3.5" alt="" />
Sending
</>
) : (
@ -347,7 +348,7 @@ export function CreateNotificationPage() {
<div className="max-h-64 space-y-1.5 overflow-y-auto rounded-lg border border-grayScale-100 bg-grayScale-50/60 p-2">
{recipientsLoading && (
<div className="flex items-center justify-center py-6 text-xs text-grayScale-400">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<SpinnerIcon className="mr-2 h-4 w-4" alt="" />
Loading users
</div>
)}