diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue
index 48341d02f49..84a82a50ffa 100644
--- a/apps/app-frontend/src/App.vue
+++ b/apps/app-frontend/src/App.vue
@@ -7,7 +7,6 @@ import {
ImageIcon,
LogInIcon,
LogOutIcon,
- NewspaperIcon,
PlayIcon,
PlusIcon,
RefreshCwIcon,
@@ -24,7 +23,6 @@ import {
AccountSwitchOverlay,
Admonition,
Avatar,
- ButtonLink,
commonMessages,
commonSettingsMessages,
ContentInstallModal,
@@ -34,7 +32,6 @@ import {
I18nDebugPanel,
IconButton,
LoadingBar,
- NewsArticleCard,
NotificationPanel,
PopupNotificationPanel,
provideLoadingState,
@@ -65,10 +62,9 @@ import { computed, nextTick, onMounted, onUnmounted, provide, ref, watch } from
import { RouterView, useRoute, useRouter } from 'vue-router'
import { useRouteLoading } from '@/app/runtime/use-route-loading'
+import AppSidebar from '@/app/shell/sidebar/index.vue'
import AppTitleBar from '@/app/shell/title-bar/index.vue'
-import AccountsCard from '@/components/ui/AccountsCard.vue'
import ErrorModal from '@/components/ui/ErrorModal.vue'
-import FriendsList from '@/components/ui/friends/FriendsList.vue'
import HostingUpdateRequired from '@/components/ui/HostingUpdateRequired.vue'
import AddServerToInstanceModal from '@/components/ui/install_flow/AddServerToInstanceModal.vue'
import UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue'
@@ -81,9 +77,6 @@ import ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyIn
import ModrinthAccountRequiredModal from '@/components/ui/modal/ModrinthAccountRequiredModal.vue'
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
import NavButton from '@/components/ui/NavButton.vue'
-import OnboardingChecklist from '@/components/ui/onboarding-checklist/index.vue'
-import PrideFundraiserBanner from '@/components/ui/PrideFundraiserBanner.vue'
-import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
import SharedInstanceInviteHandler from '@/components/ui/shared-instances/shared-instance-invite-handler/index.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
@@ -101,6 +94,7 @@ import { useInstanceMetadataRefresh } from '@/composables/use-instance-metadata-
import { useQuickInstanceLimit } from '@/composables/use-quick-instance-limit.ts'
import { isDarkTheme, useTheme } from '@/composables/use-theme.ts'
import { config } from '@/config'
+import { provideMinecraftAccounts } from '@/features/minecraft-accounts/context'
import { getAccountAppearance, rememberAccountAppearance } from '@/helpers/account-appearance.ts'
import {
hide_ads_window,
@@ -229,7 +223,6 @@ async function handleFullscreenChange() {
const APP_LEFT_NAV_WIDTH = '4rem'
const APP_SIDEBAR_WIDTH = 300
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
-const PRIDE_FUNDRAISER_END_DATE = new Date('2026-07-01T00:00:00Z').getTime()
const credentials = ref()
const storedModrinthAccounts = ref([])
let credentialsRefreshId = 0
@@ -254,9 +247,6 @@ const hostingUpdateRequired = computed(
!!appUpdateState.availableUpdate.value &&
appUpdateState.updatesEnabled.value,
)
-const prideFundraiserEnabled = computed(
- () => appSettings.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE,
-)
const hostingIntercomIdentityKey = computed(() => {
const rawServerId = route.params.id
const serverId = Array.isArray(rawServerId) ? rawServerId[0] : rawServerId
@@ -373,8 +363,6 @@ const {
(iconPath) =>
creationGeneratedIcon.value?.path === iconPath ? creationGeneratedIcon.value.config : null,
)
-const { hasLoggedIntoMinecraft, hasLoggedIntoModrinth, showChecklist } = onboardingChecklist
-const showFriendsList = computed(() => !showChecklist.value || hasLoggedIntoModrinth.value)
async function randomizeCreationIcon() {
const generated = await creationIconEditorModal.value?.randomizeAndSave()
@@ -412,7 +400,6 @@ function onCreationIconSaved(iconPath, config) {
context.instanceIconPath.value = iconPath
}
-const news = ref([])
const displayedServerInviteNotifications = new Set()
const serverInvitePopupNotificationIds = new Set()
let liveNotificationGeneration = 0
@@ -651,18 +638,6 @@ const messages = defineMessages({
id: 'app.nav.upgrade-to-modrinth-plus',
defaultMessage: 'Upgrade to Modrinth+',
},
- news: {
- id: 'app.news.title',
- defaultMessage: 'News',
- },
- viewAllNews: {
- id: 'app.news.view-all',
- defaultMessage: 'View all news',
- },
- playingAs: {
- id: 'app.sidebar.playing-as',
- defaultMessage: 'Playing as',
- },
})
function handleAdsConsentRequired(required) {
@@ -854,22 +829,6 @@ async function setupApp() {
)
})
- fetch(`https://modrinth.com/news/feed/articles.json`)
- .then((response) => response.json())
- .then((res) => {
- if (res && res.articles) {
- news.value = res.articles
- .map((article) => ({
- ...article,
- path: article.link,
- }))
- .slice(0, 4)
- }
- })
- .catch((error) => {
- console.error('Failed to fetch news articles', error)
- })
-
traceStartupStep('Read opening command', get_opening_command).then(handleCommand)
traceStartupStep('Refresh startup credentials', fetchCredentials)
@@ -907,13 +866,6 @@ const handleClose = async () => {
await getCurrentWindow().close()
}
-const sidebarOverlayScrollbarsOptions = Object.freeze({
- overflow: {
- x: 'hidden',
- y: 'scroll',
- },
-})
-
const queryClient = useQueryClient()
watch(stateInitialized, (ready) => {
@@ -1442,7 +1394,7 @@ const modrinthAccountMenuOptions = computed(() => [
id: 'add-friend',
label: formatMessage(messages.addFriend),
icon: UserPlusIcon,
- action: () => friendsList.value?.showAddFriendModal(),
+ action: () => sidebar.value?.showAddFriendModal(),
},
{
id: 'flags',
@@ -1530,9 +1482,8 @@ onMounted(() => {
setServerUpdateToPlayModal(updateToPlayModal.value)
})
-const accounts = ref(null)
-const friendsList = ref(null)
-provide('accountsCard', accounts)
+const sidebar = ref(null)
+provideMinecraftAccounts(computed(() => sidebar.value?.minecraftAccounts ?? null))
useAppEvent('command', handleCommand, appEvents)
useAppEvent('notification', handleLiveNotification, appEvents)
@@ -2339,87 +2290,16 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
-
+
@@ -2536,41 +2416,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
overflow: hidden;
}
-.app-sidebar {
- overflow: visible;
- width: 300px;
- position: relative;
- height: calc(100vh - var(--top-bar-height));
- background: var(--brand-gradient-bg);
-
- --color-button-bg: var(--brand-gradient-button);
- --surface-4: var(--brand-gradient-button);
- --color-button-bg-hover: var(--brand-gradient-border);
- --surface-5: var(--brand-gradient-border);
- --color-divider: var(--brand-gradient-border);
- --color-divider-dark: var(--brand-gradient-border);
-}
-
-.app-sidebar::after {
- content: '';
- position: absolute;
- bottom: 250px;
- left: 0;
- right: 0;
- height: 5rem;
- background: var(--brand-gradient-fade-out-color);
- pointer-events: none;
-}
-
-.app-sidebar.has-plus::after {
- display: none;
-}
-
.disable-advanced-rendering {
- .app-sidebar::before {
- box-shadow: none;
- }
-
&.app-contents::before {
box-shadow: none;
}
@@ -2582,17 +2428,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
}
}
-.app-sidebar::before {
- content: '';
- box-shadow: -15px 0 15px -15px rgba(0, 0, 0, 0.1) inset;
- top: 0;
- bottom: 0;
- left: -2rem;
- width: 2rem;
- position: absolute;
- pointer-events: none;
-}
-
.app-viewport {
flex-grow: 1;
height: 100%;
@@ -2617,18 +2452,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
pointer-events: none;
}
-.sidebar-teleport-content {
- display: contents;
-}
-
-.sidebar-default-content {
- display: none;
-}
-
-.sidebar-teleport-content:empty + .sidebar-default-content.sidebar-enabled {
- display: contents;
-}
-
@media (prefers-reduced-motion: no-preference) {
.nav-button-animated-enter-active {
transition: all 0.5s cubic-bezier(0.15, 1.4, 0.64, 0.96);
diff --git a/apps/app-frontend/src/app/shell/sidebar/index.vue b/apps/app-frontend/src/app/shell/sidebar/index.vue
new file mode 100644
index 00000000000..64223ab351e
--- /dev/null
+++ b/apps/app-frontend/src/app/shell/sidebar/index.vue
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
diff --git a/apps/app-frontend/src/app/shell/sidebar/promotion.vue b/apps/app-frontend/src/app/shell/sidebar/promotion.vue
new file mode 100644
index 00000000000..62b665858ed
--- /dev/null
+++ b/apps/app-frontend/src/app/shell/sidebar/promotion.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+ {{ formatMessage(messages.upgradeToModrinthPlus) }}
+
+
+
diff --git a/apps/app-frontend/src/components/ui/PromotionWrapper.vue b/apps/app-frontend/src/components/ui/PromotionWrapper.vue
deleted file mode 100644
index 16615831d58..00000000000
--- a/apps/app-frontend/src/components/ui/PromotionWrapper.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-
-
-
-
diff --git a/apps/app-frontend/src/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue b/apps/app-frontend/src/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue
index 73293f5bed1..4437d405011 100644
--- a/apps/app-frontend/src/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue
+++ b/apps/app-frontend/src/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue
@@ -59,16 +59,16 @@
diff --git a/apps/app-frontend/src/components/ui/friends/FriendsList.vue b/apps/app-frontend/src/features/friends/friends-list/index.vue
similarity index 99%
rename from apps/app-frontend/src/components/ui/friends/FriendsList.vue
rename to apps/app-frontend/src/features/friends/friends-list/index.vue
index 08f0d43dd56..812cf7b5d0b 100644
--- a/apps/app-frontend/src/components/ui/friends/FriendsList.vue
+++ b/apps/app-frontend/src/features/friends/friends-list/index.vue
@@ -13,14 +13,15 @@ import {
} from '@modrinth/ui'
import { computed, ref } from 'vue'
-import FriendsSection from '@/components/ui/friends/FriendsSection.vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { useAppSettings } from '@/composables/use-app-settings.ts'
-import { useFriends } from '@/composables/use-friends'
import type { FriendWithUserData } from '@/helpers/friends.ts'
import type { ModrinthCredentials } from '@/helpers/mr_auth'
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
+import { useFriends } from '../use-friends'
+import FriendsSection from './section.vue'
+
const { formatMessage } = useVIntl()
const { handleError } = injectNotificationManager()
diff --git a/apps/app-frontend/src/components/ui/friends/FriendsSection.vue b/apps/app-frontend/src/features/friends/friends-list/section.vue
similarity index 100%
rename from apps/app-frontend/src/components/ui/friends/FriendsSection.vue
rename to apps/app-frontend/src/features/friends/friends-list/section.vue
diff --git a/apps/app-frontend/src/composables/use-friends.ts b/apps/app-frontend/src/features/friends/use-friends.ts
similarity index 100%
rename from apps/app-frontend/src/composables/use-friends.ts
rename to apps/app-frontend/src/features/friends/use-friends.ts
diff --git a/apps/app-frontend/src/components/ui/AccountsCard.vue b/apps/app-frontend/src/features/minecraft-accounts/accounts-card.vue
similarity index 100%
rename from apps/app-frontend/src/components/ui/AccountsCard.vue
rename to apps/app-frontend/src/features/minecraft-accounts/accounts-card.vue
diff --git a/apps/app-frontend/src/features/minecraft-accounts/context.ts b/apps/app-frontend/src/features/minecraft-accounts/context.ts
new file mode 100644
index 00000000000..958097bfd74
--- /dev/null
+++ b/apps/app-frontend/src/features/minecraft-accounts/context.ts
@@ -0,0 +1,16 @@
+import { createContext } from '@modrinth/ui'
+import type { Ref } from 'vue'
+
+import type { Skin } from '@/helpers/skins'
+
+export interface MinecraftAccountsActions {
+ readonly loginDisabled: boolean
+ refreshValues: () => Promise
+ setEquippedSkin: (skin: Skin) => Promise
+ setLoginDisabled: (disabled: boolean) => void
+ login: () => Promise
+}
+
+export const [injectMinecraftAccounts, provideMinecraftAccounts] = createContext<
+ Readonly[>
+>('root', 'minecraftAccounts')
diff --git a/apps/app-frontend/src/features/news/news-sidebar.vue b/apps/app-frontend/src/features/news/news-sidebar.vue
new file mode 100644
index 00000000000..17b82dcbbf3
--- /dev/null
+++ b/apps/app-frontend/src/features/news/news-sidebar.vue
@@ -0,0 +1,51 @@
+
+
+
+ ]
+
+ {{ formatMessage(messages.news) }}
+
+
+
+
+
+ {{ formatMessage(messages.viewAllNews) }}
+
+
+
+
diff --git a/apps/app-frontend/src/features/news/queries.ts b/apps/app-frontend/src/features/news/queries.ts
new file mode 100644
index 00000000000..b6ea3612a92
--- /dev/null
+++ b/apps/app-frontend/src/features/news/queries.ts
@@ -0,0 +1,27 @@
+import type { NewsArticle } from '@modrinth/ui'
+import { queryOptions } from '@tanstack/vue-query'
+
+type NewsFeedArticle = Omit & { link: string }
+
+export const newsKeys = {
+ articles: ['news', 'articles'] as const,
+}
+
+export function newsArticlesQueryOptions() {
+ return queryOptions({
+ queryKey: newsKeys.articles,
+ queryFn: async ({ signal }): Promise => {
+ const response = await fetch('https://modrinth.com/news/feed/articles.json', { signal })
+ if (!response.ok) {
+ throw new Error(`Failed to fetch news articles: HTTP ${response.status}`)
+ }
+ const feed: { articles?: NewsFeedArticle[] } | null = await response.json()
+ return (feed?.articles ?? []).map((article) => ({ ...article, path: article.link }))
+ },
+ staleTime: Infinity,
+ retry: false,
+ refetchOnMount: false,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ })
+}
diff --git a/apps/app-frontend/src/components/ui/onboarding-checklist/index.vue b/apps/app-frontend/src/features/onboarding/checklist.vue
similarity index 100%
rename from apps/app-frontend/src/components/ui/onboarding-checklist/index.vue
rename to apps/app-frontend/src/features/onboarding/checklist.vue
diff --git a/apps/app-frontend/src/pages/Skins.vue b/apps/app-frontend/src/pages/Skins.vue
index ca84f5764f0..30ec659fc18 100644
--- a/apps/app-frontend/src/pages/Skins.vue
+++ b/apps/app-frontend/src/pages/Skins.vue
@@ -25,15 +25,14 @@ import { arrayBufferToBase64 } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import { type DragDropEvent, getCurrentWebview } from '@tauri-apps/api/webview'
import { computedAsync } from '@vueuse/core'
-import type { Ref } from 'vue'
-import { computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
+import { computed, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
import EarsModIcon from '@/assets/skins/ears-mod.png'
-import type AccountsCard from '@/components/ui/AccountsCard.vue'
import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue'
import VirtualSkinSectionList from '@/components/ui/skin/VirtualSkinSectionList.vue'
import { useAppSettings } from '@/composables/use-app-settings.ts'
import { handleSevereError } from '@/composables/use-error.js'
+import { injectMinecraftAccounts } from '@/features/minecraft-accounts/context'
import { trackEvent } from '@/helpers/analytics'
import { check_reachable, get_default_user, login as login_flow, users } from '@/helpers/auth'
import { cleanupUnusedPreviews } from '@/helpers/rendering/skin-previews'
@@ -229,7 +228,7 @@ const skins = ref([])
const capes = ref([])
const offline = ref(!navigator.onLine)
-const accountsCard = inject('accountsCard') as Ref
+const minecraftAccounts = injectMinecraftAccounts()
const currentUser = ref(undefined)
const currentUserId = ref(undefined)
@@ -578,7 +577,7 @@ function setLocallyEquippedSkin(skinToApply: Skin) {
originalSelectedSkin.value =
skins.value.find((skin) => skinsMatch(skin, skinToApply)) ?? skinToApply
selectedSkin.value = originalSelectedSkin.value
- void accountsCard.value?.setEquippedSkin(originalSelectedSkin.value)
+ void minecraftAccounts.value?.setEquippedSkin(originalSelectedSkin.value)
}
function insertLocalSkin(savedSkin: Skin) {
@@ -634,7 +633,7 @@ function updateLocalSkin(savedSkin: Skin, applied: boolean, previousSkin?: Skin)
originalSelectedSkin.value = locallyEquippedSkin
selectedSkin.value = locallyEquippedSkin
- void accountsCard.value?.setEquippedSkin(locallyEquippedSkin)
+ void minecraftAccounts.value?.setEquippedSkin(locallyEquippedSkin)
} else {
const locallySavedSkin =
skins.value.find((skin) => skin.texture_key === savedSkin.texture_key) ?? savedSkin
@@ -725,7 +724,7 @@ async function preserveExternalSkins(skinsToPersist: Skin[]) {
if (skinsMatchIgnoringSource(originalSelectedSkin.value, skin)) {
originalSelectedSkin.value = preservedSkin
- void accountsCard.value?.setEquippedSkin(preservedSkin)
+ void minecraftAccounts.value?.setEquippedSkin(preservedSkin)
}
preservedSkins.push(preservedSkin)
@@ -760,8 +759,8 @@ function schedulePendingSkinRefresh() {
return
}
- if (accountsCard.value) {
- await accountsCard.value.refreshValues()
+ if (minecraftAccounts.value) {
+ await minecraftAccounts.value.refreshValues()
}
await loadCapes()
@@ -830,15 +829,15 @@ async function loadCurrentUser() {
}
async function login() {
- accountsCard.value.setLoginDisabled(true)
+ minecraftAccounts.value?.setLoginDisabled(true)
const loggedIn = await login_flow().catch(handleSevereError)
- if (loggedIn && accountsCard) {
- await accountsCard.value.refreshValues()
+ if (loggedIn && minecraftAccounts.value) {
+ await minecraftAccounts.value.refreshValues()
}
trackEvent('AccountLogIn')
- accountsCard.value.setLoginDisabled(false)
+ minecraftAccounts.value?.setLoginDisabled(false)
}
function openAddSkinFileBrowser() {
@@ -1061,7 +1060,7 @@ async function checkUserChanges() {
try {
const defaultId = await get_default_user()
if (defaultId !== currentUserId.value) {
- await accountsCard.value?.refreshValues()
+ await minecraftAccounts.value?.refreshValues()
await loadCurrentUser()
await loadCapes()
await loadSkins()
@@ -1295,13 +1294,13 @@ await loadSkins()
diff --git a/apps/app-frontend/src/pages/instance/share/use-shared-instance-invite-candidates.ts b/apps/app-frontend/src/pages/instance/share/use-shared-instance-invite-candidates.ts
index b25e9e91dfd..3d8830d42e8 100644
--- a/apps/app-frontend/src/pages/instance/share/use-shared-instance-invite-candidates.ts
+++ b/apps/app-frontend/src/pages/instance/share/use-shared-instance-invite-candidates.ts
@@ -5,7 +5,7 @@ import {
} from '@modrinth/ui'
import { computed, type Ref } from 'vue'
-import { useFriends } from '@/composables/use-friends'
+import { useFriends } from '@/features/friends/use-friends'
import { getFriendUserId } from '@/helpers/friends.ts'
import { get as getCredentials } from '@/helpers/mr_auth.ts'
import { search_user } from '@/helpers/users.ts'
diff --git a/apps/app-frontend/tailwind.config.ts b/apps/app-frontend/tailwind.config.ts
index 301221bab4e..d4b216cd473 100644
--- a/apps/app-frontend/tailwind.config.ts
+++ b/apps/app-frontend/tailwind.config.ts
@@ -5,6 +5,7 @@ const config: Config = {
content: [
'./src/app/**/*.{js,vue,ts}',
'./src/components/**/*.{js,vue,ts}',
+ './src/features/**/*.{js,vue,ts}',
'./src/layouts/**/*.vue',
'./src/pages/**/*.vue',
'./src/plugins/**/*.{js,ts}',