diff --git a/apps/mobile/src/icons/list_check_2_cute_re.tsx b/apps/mobile/src/icons/list_check_2_cute_re.tsx
new file mode 100644
index 000000000..381178e7c
--- /dev/null
+++ b/apps/mobile/src/icons/list_check_2_cute_re.tsx
@@ -0,0 +1,26 @@
+import * as React from "react"
+import Svg, { Path } from "react-native-svg"
+
+interface ListCheck2CuteReIconProps {
+ width?: number
+ height?: number
+ color?: string
+}
+
+export const ListCheck2CuteReIcon = ({
+ width = 24,
+ height = 24,
+ color = "#10161F",
+}: ListCheck2CuteReIconProps) => {
+ return (
+
+ )
+}
diff --git a/apps/mobile/src/icons/shuffle_2_cute_re.tsx b/apps/mobile/src/icons/shuffle_2_cute_re.tsx
new file mode 100644
index 000000000..fd4ee82c0
--- /dev/null
+++ b/apps/mobile/src/icons/shuffle_2_cute_re.tsx
@@ -0,0 +1,31 @@
+import * as React from "react"
+import Svg, { Path } from "react-native-svg"
+
+interface Shuffle2CuteReIconProps {
+ width?: number
+ height?: number
+ color?: string
+}
+
+export const Shuffle2CuteReIcon = ({
+ width = 24,
+ height = 24,
+ color = "#10161F",
+}: Shuffle2CuteReIconProps) => {
+ return (
+
+ )
+}
diff --git a/apps/mobile/src/modules/onboarding/hooks/use-reading-behavior.ts b/apps/mobile/src/modules/onboarding/hooks/use-reading-behavior.ts
new file mode 100644
index 000000000..f12ebf93a
--- /dev/null
+++ b/apps/mobile/src/modules/onboarding/hooks/use-reading-behavior.ts
@@ -0,0 +1,41 @@
+import { setGeneralSetting, useGeneralSettingKey } from "@/src/atoms/settings/general"
+
+type ReadingBehavior = "radical" | "balanced" | "conservative"
+
+export const useReadingBehavior = () => {
+ const markAsReadWhenScrolling = useGeneralSettingKey("scrollMarkUnread")
+ const markAsReadWhenInView = useGeneralSettingKey("renderMarkUnread")
+
+ const behavior: ReadingBehavior =
+ markAsReadWhenInView && markAsReadWhenScrolling
+ ? "radical"
+ : !markAsReadWhenInView && !markAsReadWhenScrolling
+ ? "conservative"
+ : "balanced"
+
+ const updateSettings = (behavior: ReadingBehavior) => {
+ switch (behavior) {
+ case "radical": {
+ setGeneralSetting("scrollMarkUnread", true)
+ setGeneralSetting("renderMarkUnread", true)
+ break
+ }
+ case "balanced": {
+ setGeneralSetting("scrollMarkUnread", true)
+ setGeneralSetting("renderMarkUnread", false)
+ break
+ }
+ case "conservative": {
+ setGeneralSetting("scrollMarkUnread", false)
+ setGeneralSetting("renderMarkUnread", false)
+ break
+ }
+ }
+ }
+ return {
+ behavior,
+ markAsReadWhenScrolling,
+ markAsReadWhenInView,
+ updateSettings,
+ }
+}
diff --git a/apps/mobile/src/modules/onboarding/preset.ts b/apps/mobile/src/modules/onboarding/preset.ts
new file mode 100644
index 000000000..bfbafb49c
--- /dev/null
+++ b/apps/mobile/src/modules/onboarding/preset.ts
@@ -0,0 +1,122 @@
+import { FeedViewType } from "@follow/constants"
+
+export type PresetFeedConfig = {
+ title: string
+ feedId: string
+ url: string
+ view: FeedViewType
+}
+
+export const presetFeeds: PresetFeedConfig[] = [
+ {
+ feedId: "41358761177015296",
+ title: "知乎热榜 - 全站",
+ url: "rsshub://zhihu/hot/total",
+ view: FeedViewType.Articles,
+ },
+ {
+ feedId: "100020530265058357",
+ title: "阮一峰的网络日志",
+ url: "https://feeds.feedburner.com/ruanyifeng",
+ view: FeedViewType.Articles,
+ },
+
+ {
+ feedId: "41358830592746496",
+ title: "微博热搜榜",
+ url: "rsshub://weibo/search/hot",
+ view: FeedViewType.SocialMedia,
+ },
+ {
+ feedId: "100411504863520768",
+ title: "Twitter @Elon Musk",
+ url: "rsshub://twitter/user/elonmusk",
+ view: FeedViewType.SocialMedia,
+ },
+ {
+ feedId: "41324816676184077",
+ title: "Twitter @DIŸgöd ☀️",
+ url: "rsshub://twitter/user/DIYgod",
+ view: FeedViewType.SocialMedia,
+ },
+
+ {
+ feedId: "78806242632741888",
+ title: "bilibili 排行榜-全站",
+ url: "rsshub://bilibili/ranking/0",
+ view: FeedViewType.Videos,
+ },
+
+ {
+ feedId: "60338304723722240",
+ title: "实时财经快讯 - FastBull",
+ url: "rsshub://fastbull/express-news",
+ view: FeedViewType.Articles,
+ },
+ {
+ feedId: "55611390687386624",
+ title: "格隆汇快讯-7x24小时市场快讯-财经市场热点",
+ url: "rsshub://gelonghui/live",
+ view: FeedViewType.Articles,
+ },
+ {
+ feedId: "49375919416104960",
+ title: "深潮TechFlow - 快讯",
+ url: "rsshub://techflowpost/express",
+ view: FeedViewType.Articles,
+ },
+ {
+ feedId: "55982073122828305",
+ title: "TED Talks Daily",
+ url: "https://feeds.acast.com/public/shows/67587e77c705e441797aff96",
+ view: FeedViewType.Articles,
+ },
+ {
+ feedId: "72541715399995392",
+ title: "TheBlockBeats - 快讯",
+ url: "rsshub://theblockbeats/newsflash/0",
+ view: FeedViewType.Articles,
+ },
+ {
+ feedId: "100184911354754055",
+ title: "小Lin说",
+ url: "https://www.youtube.com/feeds/videos.xml?channel_id=UCilwQlk62k1z7aUEZPOB6yw",
+ view: FeedViewType.Videos,
+ },
+ {
+ feedId: "100185810923910148",
+ title: "张小珺Jùn|商业访谈录",
+ url: "https://feed.xyzfm.space/dk4yh3pkpjp3",
+ view: FeedViewType.Articles,
+ },
+ {
+ feedId: "56584656988676096",
+ title: "迷因电波",
+ url: "rsshub://xiaoyuzhou/podcast/61d52b3bee197a3aac3dac44",
+ view: FeedViewType.Articles,
+ },
+ {
+ feedId: "76051724651752448",
+ title: "极致音乐汇 的 bilibili 空间",
+ url: "rsshub://bilibili/user/video/1691501735",
+ view: FeedViewType.Videos,
+ },
+ {
+ feedId: "66701376672681984",
+ title: "AP Top News - AP News",
+ url: "rsshub://apnews/api/apf-topnews",
+ view: FeedViewType.Articles,
+ },
+ {
+ feedId: "44366244616936448",
+ title: "金十数据",
+ url: "rsshub://jin10",
+ view: FeedViewType.Articles,
+ },
+ {
+ feedId: "52325519371718656",
+ title: "Hacker News",
+ url: "rsshub://hackernews",
+ view: FeedViewType.Articles,
+ },
+]
diff --git a/apps/mobile/src/modules/onboarding/step-finished.tsx b/apps/mobile/src/modules/onboarding/step-finished.tsx
new file mode 100644
index 000000000..3cea7b44c
--- /dev/null
+++ b/apps/mobile/src/modules/onboarding/step-finished.tsx
@@ -0,0 +1,13 @@
+import { Text, View } from "react-native"
+
+import { Logo } from "@/src/components/ui/logo"
+
+export const StepFinished = () => (
+
+
+ You're all set!
+
+ You have completed the guide. Enjoy your journey!
+
+
+)
diff --git a/apps/mobile/src/modules/onboarding/step-interests.tsx b/apps/mobile/src/modules/onboarding/step-interests.tsx
new file mode 100644
index 000000000..c7a44780d
--- /dev/null
+++ b/apps/mobile/src/modules/onboarding/step-interests.tsx
@@ -0,0 +1,110 @@
+import { cn } from "@follow/utils"
+import { useCallback, useState } from "react"
+import { Text, TouchableOpacity, View } from "react-native"
+import Animated, { FadeIn, FadeOut } from "react-native-reanimated"
+
+import { Search3CuteReIcon } from "@/src/icons/search_3_cute_re"
+import { Shuffle2CuteReIcon } from "@/src/icons/shuffle_2_cute_re"
+import { toast } from "@/src/lib/toast"
+import { useSubscription } from "@/src/store/subscription/hooks"
+import { subscriptionSyncService } from "@/src/store/subscription/store"
+import { accentColor } from "@/src/theme/colors"
+
+import type { PresetFeedConfig } from "./preset"
+import { presetFeeds } from "./preset"
+
+const subscribeFeed = async (config: PresetFeedConfig) => {
+ await subscriptionSyncService.subscribe({
+ feedId: config.feedId,
+ title: config.title,
+ url: config.url,
+ view: config.view,
+ category: "",
+ isPrivate: false,
+ })
+
+ toast.success(`Subscribed to ${config.title}`, {
+ position: "bottom",
+ })
+}
+
+const unsubscribeFeed = async (feedId: string) => {
+ await subscriptionSyncService.unsubscribe(feedId)
+ toast.success(`Unsubscribed from feed`, {
+ position: "bottom",
+ })
+}
+
+export const StepInterests = () => {
+ const [displayFeeds, setDisplayFeeds] = useState(presetFeeds.slice(0, 7))
+
+ const shuffleFeeds = useCallback(() => {
+ const shuffled = [...presetFeeds].sort(() => Math.random() - 0.5).slice(0, 7)
+ setDisplayFeeds(shuffled)
+ }, [])
+ return (
+
+
+
+ Discover Interests
+
+ Subscribe to feeds that match your interests.
+
+
+
+
+
+ Suggestions feed
+
+
+ Shuffle
+
+
+
+
+ {displayFeeds.map((feed) => (
+
+ ))}
+
+
+
+ )
+}
+
+const FeedChip = (feed: PresetFeedConfig) => {
+ const isSubscribed = useSubscription(feed.feedId)
+
+ const handleSubscribe = useCallback(
+ async (feed: PresetFeedConfig) => {
+ if (isSubscribed) {
+ await unsubscribeFeed(feed.feedId)
+ return
+ }
+ await subscribeFeed(feed)
+ },
+ [isSubscribed],
+ )
+
+ return (
+
+ handleSubscribe(feed)}
+ className={cn(
+ "flex rounded-full px-4 py-2",
+ isSubscribed ? "bg-accent" : "bg-secondary-system-fill",
+ )}
+ >
+
+ {feed.title}
+
+
+
+ )
+}
diff --git a/apps/mobile/src/modules/onboarding/step-preferences.tsx b/apps/mobile/src/modules/onboarding/step-preferences.tsx
new file mode 100644
index 000000000..c9349f3a4
--- /dev/null
+++ b/apps/mobile/src/modules/onboarding/step-preferences.tsx
@@ -0,0 +1,151 @@
+import { router } from "expo-router"
+import type { PropsWithChildren } from "react"
+import { ActivityIndicator, Pressable, Text, TouchableOpacity, View } from "react-native"
+import { useColor } from "react-native-uikit-colors"
+
+import { UserAvatar } from "@/src/components/ui/avatar/UserAvatar"
+import { GroupedInsetListNavigationLinkIcon } from "@/src/components/ui/grouped/GroupedList"
+import { DocmentCuteReIcon } from "@/src/icons/docment_cute_re"
+import { FileImportCuteReIcon } from "@/src/icons/file_import_cute_re"
+import { ListCheck2CuteReIcon } from "@/src/icons/list_check_2_cute_re"
+import { MingcuteRightLine } from "@/src/icons/mingcute_right_line"
+import { Settings1CuteReIcon } from "@/src/icons/settings_1_cute_re"
+import { useWhoami } from "@/src/store/user/hooks"
+import { accentColor } from "@/src/theme/colors"
+
+import { importOpml, setAvatar } from "../settings/utils"
+import { useReadingBehavior } from "./hooks/use-reading-behavior"
+
+export const StepPreferences = () => {
+ const { behavior } = useReadingBehavior()
+
+ return (
+
+
+
+
+ Personalize Your Experience
+
+
+ Set your preferences to make Follow work best for you. You can always change these later
+ in Settings.
+
+
+
+
+
+
+
+ }
+ onPress={() => {
+ router.push("/onboarding/edit-profile")
+ }}
+ >
+
+ Change your name, email, and profile picture
+
+
+
+ {/* Reading Preferences Card */}
+
+
+
+ }
+ onPress={() => {
+ router.push("/onboarding/select-reading-mode")
+ }}
+ >
+ {behavior === "radical" && (
+
+ Automatically mark entries as read when displayed
+
+ )}
+ {behavior === "balanced" && (
+
+ Automatically mark entries as read when scrolled out of view
+
+ )}
+ {behavior === "conservative" && (
+
+ Mark entries as read only when clicked
+
+ )}
+
+
+ {/* Import Card */}
+
+
+
+ }
+ onPress={importOpml}
+ >
+
+
+ If you have used RSS before, you can import an OPML file
+
+
+
+
+
+ )
+}
+
+export const EditProfileSection = () => {
+ const whoami = useWhoami()
+
+ if (!whoami) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+ Set Avatar
+
+
+ )
+}
+
+type PreferenceCardProps = PropsWithChildren<{
+ title: string
+ icon?: React.ReactNode
+ onPress?: () => void
+}>
+
+const PreferenceCard = ({ title, children, onPress, icon }: PreferenceCardProps) => {
+ const rightIconColor = useColor("tertiaryLabel")
+
+ return (
+
+ {icon}
+
+ {title}
+ {children}
+
+
+
+ )
+}
diff --git a/apps/mobile/src/modules/onboarding/step-welcome.tsx b/apps/mobile/src/modules/onboarding/step-welcome.tsx
new file mode 100644
index 000000000..01af0007b
--- /dev/null
+++ b/apps/mobile/src/modules/onboarding/step-welcome.tsx
@@ -0,0 +1,13 @@
+import { Text, View } from "react-native"
+
+import { Logo } from "@/src/components/ui/logo"
+
+export const StepWelcome = () => (
+
+
+ Welcome to Follow!
+
+ This guide will help you get started with the app.
+
+
+)
diff --git a/apps/mobile/src/modules/settings/routes/EditProfile.tsx b/apps/mobile/src/modules/settings/routes/EditProfile.tsx
index 611477f56..4a440f723 100644
--- a/apps/mobile/src/modules/settings/routes/EditProfile.tsx
+++ b/apps/mobile/src/modules/settings/routes/EditProfile.tsx
@@ -2,7 +2,7 @@ import { withOpacity } from "@follow/utils/src/color"
import { useMutation } from "@tanstack/react-query"
import { router } from "expo-router"
import type { FC } from "react"
-import { useState } from "react"
+import { useCallback, useState } from "react"
import {
ActivityIndicator,
Text,
@@ -13,6 +13,8 @@ import {
import { KeyboardController } from "react-native-keyboard-controller"
import { RotateableLoading } from "@/src/components/common/RotateableLoading"
+import { ModalHeader } from "@/src/components/layouts/header/ModalHeader"
+import { SafeModalScrollView } from "@/src/components/layouts/views/SafeModalScrollView"
import {
NavigationBlurEffectHeader,
SafeNavigationScrollView,
@@ -29,8 +31,6 @@ import {
import { CheckCircleCuteReIcon } from "@/src/icons/check_circle_cute_re"
import { CheckLineIcon } from "@/src/icons/check_line"
import { CloseCircleFillIcon } from "@/src/icons/close_circle_fill"
-import { apiClient, apiFetch, getBizFetchErrorMessage } from "@/src/lib/api-fetch"
-import { pickImage } from "@/src/lib/native/picker"
import { toast } from "@/src/lib/toast"
import { useWhoami } from "@/src/store/user/hooks"
import type { MeModel } from "@/src/store/user/store"
@@ -38,6 +38,8 @@ import { userSyncService } from "@/src/store/user/store"
import type { UserProfileEditable } from "@/src/store/user/types"
import { accentColor, useColor } from "@/src/theme/colors"
+import { setAvatar } from "../utils"
+
export const EditProfileScreen = () => {
const whoami = useWhoami()
@@ -57,6 +59,26 @@ export const EditProfileScreen = () => {
)
}
+export const EditProfileModal = () => {
+ const whoami = useWhoami()
+
+ if (!whoami) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
+ )
+}
+
const AvatarSection: FC<{
whoami: MeModel
}> = ({ whoami }) => {
@@ -69,37 +91,7 @@ const AvatarSection: FC<{
className={!whoami?.name || !whoami.image ? "bg-system-background" : ""}
/>
- {
- const result = await pickImage({
- fileName: "avatar.jpg",
- maxSizeKB: 290,
- })
-
- if (!result) return
- const { formData } = result
- const res = await apiFetch<{
- url: string
- }>(apiClient.upload.avatar.$url().toString(), {
- method: "POST",
- headers: {
- "Content-Type": "multipart/form-data",
- },
- body: formData,
- }).catch((err) => {
- toast.error(getBizFetchErrorMessage(err))
- throw err
- })
-
- const { url } = res
-
- userSyncService.updateProfile({
- image: url,
- })
- }}
- >
+
Set Avatar
@@ -108,7 +100,8 @@ const AvatarSection: FC<{
const ProfileForm: FC<{
whoami: MeModel
-}> = ({ whoami }) => {
+ layout?: "modal" | "screen"
+}> = ({ whoami, layout = "screen" }) => {
const [dirtyFields, setDirtyFields] = useState>({})
const { mutateAsync: updateProfile, isPending } = useMutation({
@@ -124,27 +117,36 @@ const ProfileForm: FC<{
})
const label = useColor("label")
+ const headerRight = useCallback(
+ () => (
+
+ ) : (
+
+ )
+ }
+ onPress={() => {
+ updateProfile()
+ }}
+ />
+ ),
+ [dirtyFields, isPending, label, updateProfile],
+ )
+
+ const Header =
+ layout === "modal" ? (
+
+ ) : (
+
+ )
+
return (
- (
-
- ) : (
-
- )
- }
- onPress={() => {
- updateProfile()
- }}
- />
- )}
- title="Edit Profile"
- />
+ {Header}
{
diff --git a/apps/mobile/src/modules/settings/utils.ts b/apps/mobile/src/modules/settings/utils.ts
index 7b4942a1a..e9965a593 100644
--- a/apps/mobile/src/modules/settings/utils.ts
+++ b/apps/mobile/src/modules/settings/utils.ts
@@ -3,8 +3,38 @@ import * as FileSystem from "expo-file-system"
import * as Sharing from "expo-sharing"
import { getDbPath } from "@/src/database"
-import { apiFetch, getBizFetchErrorMessage } from "@/src/lib/api-fetch"
+import { apiClient, apiFetch, getBizFetchErrorMessage } from "@/src/lib/api-fetch"
+import { pickImage } from "@/src/lib/native/picker"
import { toast } from "@/src/lib/toast"
+import { userSyncService } from "@/src/store/user/store"
+
+export const setAvatar = async () => {
+ const result = await pickImage({
+ fileName: "avatar.jpg",
+ maxSizeKB: 290,
+ })
+
+ if (!result) return
+ const { formData } = result
+ const res = await apiFetch<{
+ url: string
+ }>(apiClient.upload.avatar.$url().toString(), {
+ method: "POST",
+ headers: {
+ "Content-Type": "multipart/form-data",
+ },
+ body: formData,
+ }).catch((err) => {
+ toast.error(getBizFetchErrorMessage(err))
+ throw err
+ })
+
+ const { url } = res
+
+ userSyncService.updateProfile({
+ image: url,
+ })
+}
type FeedResponseList = {
id: string
diff --git a/apps/mobile/src/screens/(modal)/onboarding/edit-profile.tsx b/apps/mobile/src/screens/(modal)/onboarding/edit-profile.tsx
new file mode 100644
index 000000000..831dd6602
--- /dev/null
+++ b/apps/mobile/src/screens/(modal)/onboarding/edit-profile.tsx
@@ -0,0 +1 @@
+export { EditProfileModal as default } from "@/src/modules/settings/routes/EditProfile"
diff --git a/apps/mobile/src/screens/(modal)/onboarding/select-reading-mode.tsx b/apps/mobile/src/screens/(modal)/onboarding/select-reading-mode.tsx
new file mode 100644
index 000000000..6e91f5ad8
--- /dev/null
+++ b/apps/mobile/src/screens/(modal)/onboarding/select-reading-mode.tsx
@@ -0,0 +1,96 @@
+import { cn } from "@follow/utils"
+import type { PropsWithChildren } from "react"
+import { Pressable, Text, View } from "react-native"
+
+import { ModalHeader } from "@/src/components/layouts/header/ModalHeader"
+import { SafeModalScrollView } from "@/src/components/layouts/views/SafeModalScrollView"
+import { GroupedInsetListNavigationLinkIcon } from "@/src/components/ui/grouped/GroupedList"
+import { Eye2CuteReIcon } from "@/src/icons/eye_2_cute_re"
+import { Grid2CuteReIcon } from "@/src/icons/grid_2_cute_re"
+import { PowerIcon } from "@/src/icons/power"
+import { useReadingBehavior } from "@/src/modules/onboarding/hooks/use-reading-behavior"
+
+const SelectReadingModeScreen = () => {
+ const { behavior, updateSettings } = useReadingBehavior()
+
+ return (
+
+
+
+
+
+
+
+ }
+ isSelected={behavior === "radical"}
+ onPress={() => {
+ updateSettings("radical")
+ }}
+ >
+
+ Radical: Automatically mark entries as read when displayed
+
+
+
+
+
+
+ }
+ isSelected={behavior === "balanced"}
+ onPress={() => {
+ updateSettings("balanced")
+ }}
+ >
+
+ Balanced: Automatically mark entries as read when scrolled out of view
+
+
+
+
+
+
+ }
+ isSelected={behavior === "conservative"}
+ onPress={() => {
+ updateSettings("conservative")
+ }}
+ >
+ Conservative: Mark entries as read only when clicked
+
+
+
+ )
+}
+
+const Card = ({
+ children,
+ onPress,
+ icon,
+ isSelected,
+}: PropsWithChildren<{
+ icon?: React.ReactNode
+ onPress?: () => void
+ isSelected?: boolean
+}>) => {
+ return (
+
+ {icon}
+ {children}
+
+ )
+}
+
+export default SelectReadingModeScreen
diff --git a/apps/mobile/src/screens/_layout.tsx b/apps/mobile/src/screens/_layout.tsx
index cb41687c5..0cefdc0e7 100644
--- a/apps/mobile/src/screens/_layout.tsx
+++ b/apps/mobile/src/screens/_layout.tsx
@@ -81,6 +81,17 @@ function AnimatedStack() {
},
}}
/>
+
)
}
diff --git a/apps/mobile/src/screens/onboarding.tsx b/apps/mobile/src/screens/onboarding.tsx
new file mode 100644
index 000000000..3702db1a0
--- /dev/null
+++ b/apps/mobile/src/screens/onboarding.tsx
@@ -0,0 +1,95 @@
+import { router } from "expo-router"
+import { useCallback, useState } from "react"
+import { Text, TouchableOpacity, View } from "react-native"
+import { useSafeAreaInsets } from "react-native-safe-area-context"
+import { SheetScreen } from "react-native-sheet-transitions"
+
+import { StepFinished } from "../modules/onboarding/step-finished"
+import { StepInterests } from "../modules/onboarding/step-interests"
+import { StepPreferences } from "../modules/onboarding/step-preferences"
+import { StepWelcome } from "../modules/onboarding/step-welcome"
+
+export default function Onboarding() {
+ const insets = useSafeAreaInsets()
+ const [currentStep, setCurrentStep] = useState(1)
+ const totalSteps = 4
+
+ const handleNext = useCallback(() => {
+ if (currentStep < totalSteps) {
+ setCurrentStep(currentStep + 1)
+ } else {
+ // Complete onboarding
+ router.replace("/")
+ }
+ }, [currentStep])
+
+ return (
+ {
+ router.back()
+ }}
+ >
+
+
+
+ {/* Content */}
+ {currentStep === 1 && }
+ {currentStep === 2 && }
+ {currentStep === 3 && }
+ {currentStep === 4 && }
+
+ {/* Navigation buttons */}
+
+
+
+ {currentStep < totalSteps - 1
+ ? "Next"
+ : currentStep === totalSteps - 1
+ ? "Finish Setup"
+ : "Let's Go!"}
+
+
+
+
+
+ )
+}
+
+export function ProgressIndicator({
+ currentStep,
+ totalSteps,
+ setCurrentStep,
+}: {
+ currentStep: number
+ totalSteps: number
+ setCurrentStep: (step: number) => void
+}) {
+ return (
+
+ {Array.from({ length: totalSteps }).map((_, index) => (
+ {
+ setCurrentStep(index + 1)
+ }}
+ >
+ = index + 1 ? "bg-accent" : "bg-gray-300"
+ }`}
+ />
+
+ ))}
+
+ )
+}
diff --git a/icons/mgc/list_check_2_cute_re.svg b/icons/mgc/list_check_2_cute_re.svg
new file mode 100644
index 000000000..025162a48
--- /dev/null
+++ b/icons/mgc/list_check_2_cute_re.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/mgc/shuffle_2_cute_re.svg b/icons/mgc/shuffle_2_cute_re.svg
new file mode 100644
index 000000000..24c77e7d1
--- /dev/null
+++ b/icons/mgc/shuffle_2_cute_re.svg
@@ -0,0 +1 @@
+
\ No newline at end of file