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:
parent
d33bacf628
commit
1f0046a8ee
|
|
@ -1,5 +1,5 @@
|
||||||
import { useMemo, useState, type ChangeEvent } from "react"
|
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 { Link, useLocation, useNavigate, useParams } from "react-router-dom"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { addQuestionToSet, createLesson, createQuestion } from "../../api/courses.api"
|
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 { Button } from "../../components/ui/button"
|
||||||
import { Card } from "../../components/ui/card"
|
import { Card } from "../../components/ui/card"
|
||||||
import { Input } from "../../components/ui/input"
|
import { Input } from "../../components/ui/input"
|
||||||
|
import { SpinnerIcon } from "../../components/ui/spinner-icon"
|
||||||
import type { QuestionOption } from "../../types/course.types"
|
import type { QuestionOption } from "../../types/course.types"
|
||||||
|
|
||||||
type Step = 1 | 2 | 3 | 4
|
type Step = 1 | 2 | 3 | 4
|
||||||
|
|
@ -356,7 +357,7 @@ export function AddNewLessonPage() {
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<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">
|
<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"}
|
{uploadingIntroVideo ? "Uploading..." : "Upload video from computer"}
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMemo, useRef, useState, type ChangeEvent } from "react"
|
import { useMemo, useRef, useState, type ChangeEvent } from "react"
|
||||||
import { Link, useLocation, useParams, useNavigate } from "react-router-dom"
|
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 { toast } from "sonner"
|
||||||
import { Card } from "../../components/ui/card"
|
import { Card } from "../../components/ui/card"
|
||||||
import { Button } from "../../components/ui/button"
|
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 { createQuestionSet, createQuestion, addQuestionToSet } from "../../api/courses.api"
|
||||||
import { uploadVideoFile } from "../../api/files.api"
|
import { uploadVideoFile } from "../../api/files.api"
|
||||||
import { Select } from "../../components/ui/select"
|
import { Select } from "../../components/ui/select"
|
||||||
|
import { SpinnerIcon } from "../../components/ui/spinner-icon"
|
||||||
import type { QuestionOption } from "../../types/course.types"
|
import type { QuestionOption } from "../../types/course.types"
|
||||||
|
|
||||||
type Step = 1 | 2 | 3 | 4 | 5
|
type Step = 1 | 2 | 3 | 4 | 5
|
||||||
|
|
@ -526,7 +527,7 @@ export function AddNewPracticePage() {
|
||||||
className="gap-1.5"
|
className="gap-1.5"
|
||||||
>
|
>
|
||||||
{uploadingIntroVideo ? (
|
{uploadingIntroVideo ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<SpinnerIcon className="h-4 w-4" alt="" />
|
||||||
) : (
|
) : (
|
||||||
<Upload className="h-4 w-4" />
|
<Upload className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
|
|
@ -541,7 +542,7 @@ export function AddNewPracticePage() {
|
||||||
>
|
>
|
||||||
{importingIntroVideoUrl ? (
|
{importingIntroVideoUrl ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-1.5 h-4 w-4 animate-spin" />
|
<SpinnerIcon className="mr-1.5 h-4 w-4" alt="" />
|
||||||
Importing URL…
|
Importing URL…
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import {
|
||||||
Languages,
|
Languages,
|
||||||
Lightbulb,
|
Lightbulb,
|
||||||
Link2,
|
Link2,
|
||||||
Loader2,
|
|
||||||
Mic,
|
Mic,
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
|
|
@ -276,7 +275,7 @@ function MediaPreviewCard({
|
||||||
) : null}
|
) : null}
|
||||||
{resolving ? (
|
{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">
|
<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...
|
Resolving media URL...
|
||||||
</div>
|
</div>
|
||||||
) : mediaType === "image" ? (
|
) : mediaType === "image" ? (
|
||||||
|
|
@ -460,8 +459,11 @@ export function HumanLanguagePage() {
|
||||||
const saved = sessionStorage.getItem(HUMAN_LANGUAGE_SCROLL_KEY)
|
const saved = sessionStorage.getItem(HUMAN_LANGUAGE_SCROLL_KEY)
|
||||||
const targetY = saved ? Number(saved) : 0
|
const targetY = saved ? Number(saved) : 0
|
||||||
if (Number.isFinite(targetY) && targetY > 0) {
|
if (Number.isFinite(targetY) && targetY > 0) {
|
||||||
window.requestAnimationFrame(() => window.scrollTo({ top: targetY, behavior: "auto" }))
|
const restoreBehavior = window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||||
setTimeout(() => window.scrollTo({ top: targetY, behavior: "auto" }), 250)
|
? "auto"
|
||||||
|
: "smooth"
|
||||||
|
window.requestAnimationFrame(() => window.scrollTo({ top: targetY, behavior: restoreBehavior }))
|
||||||
|
setTimeout(() => window.scrollTo({ top: targetY, behavior: restoreBehavior }), 250)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load human-language hierarchy:", error)
|
console.error("Failed to load human-language hierarchy:", error)
|
||||||
|
|
@ -1625,7 +1627,7 @@ export function HumanLanguagePage() {
|
||||||
disabled={creatingKey === `module-${course.course_id}-${level}`}
|
disabled={creatingKey === `module-${course.course_id}-${level}`}
|
||||||
>
|
>
|
||||||
{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" />
|
<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}`}
|
disabled={creatingKey === `submodule-${course.course_id}-${level}-${parseModuleNumber(module.title) ?? 0}`}
|
||||||
>
|
>
|
||||||
{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" />
|
<Plus className="h-3.5 w-3.5" />
|
||||||
)}
|
)}
|
||||||
|
|
@ -2152,7 +2154,7 @@ export function HumanLanguagePage() {
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{!practiceFetch || practiceFetch.status === "loading" ? (
|
{!practiceFetch || practiceFetch.status === "loading" ? (
|
||||||
<div className="flex flex-col items-center justify-center gap-2 py-12 text-sm text-grayScale-500">
|
<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…
|
Loading questions…
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
@ -2245,7 +2247,7 @@ export function HumanLanguagePage() {
|
||||||
>
|
>
|
||||||
{loadingQuestionEditId ===
|
{loadingQuestionEditId ===
|
||||||
(q.question_id ?? q.id) ? (
|
(q.question_id ?? q.id) ? (
|
||||||
<Loader2 className="h-3 w-3 animate-spin" aria-hidden />
|
<SpinnerIcon className="h-3 w-3" alt="" />
|
||||||
) : null}
|
) : null}
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -2422,7 +2424,7 @@ export function HumanLanguagePage() {
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{loadingPracticeForm ? (
|
{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">
|
<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...
|
Loading practice details...
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -2487,7 +2489,7 @@ export function HumanLanguagePage() {
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<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">
|
<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"}
|
{uploadingPracticeIntroVideo ? "Uploading..." : "Upload intro video"}
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { type ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
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 { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card"
|
||||||
import { Button } from "../../components/ui/button"
|
import { Button } from "../../components/ui/button"
|
||||||
import { Input } from "../../components/ui/input"
|
import { Input } from "../../components/ui/input"
|
||||||
|
|
@ -1926,7 +1926,7 @@ export function SpeakingPage() {
|
||||||
className="gap-1.5"
|
className="gap-1.5"
|
||||||
>
|
>
|
||||||
{uploadingIntroVideo ? (
|
{uploadingIntroVideo ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<SpinnerIcon className="h-4 w-4" alt="" />
|
||||||
) : (
|
) : (
|
||||||
<Upload className="h-4 w-4" />
|
<Upload className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import { useNavigate } from "react-router-dom"
|
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 { Card, CardContent } from "../../components/ui/card"
|
||||||
import { Button } from "../../components/ui/button"
|
import { Button } from "../../components/ui/button"
|
||||||
import { Input } from "../../components/ui/input"
|
import { Input } from "../../components/ui/input"
|
||||||
import { Textarea } from "../../components/ui/textarea"
|
import { Textarea } from "../../components/ui/textarea"
|
||||||
import { FileUpload } from "../../components/ui/file-upload"
|
import { FileUpload } from "../../components/ui/file-upload"
|
||||||
|
import { SpinnerIcon } from "../../components/ui/spinner-icon"
|
||||||
import { cn } from "../../lib/utils"
|
import { cn } from "../../lib/utils"
|
||||||
import { getTeamMembers } from "../../api/team.api"
|
import { getTeamMembers } from "../../api/team.api"
|
||||||
import type { TeamMember } from "../../types/team.types"
|
import type { TeamMember } from "../../types/team.types"
|
||||||
|
|
@ -282,7 +283,7 @@ export function CreateNotificationPage() {
|
||||||
>
|
>
|
||||||
{sending ? (
|
{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…
|
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">
|
<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 && (
|
{recipientsLoading && (
|
||||||
<div className="flex items-center justify-center py-6 text-xs text-grayScale-400">
|
<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…
|
Loading users…
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user