fix: accessibility, query cache clear, post-merge navigation

- Add aria-hidden="true" to all decorative SVGs (TelegramIcon, EmailIcon
  in MergeAccounts + ConnectedAccounts, all 4 OAuthProviderIcon SVGs)
- Add role="radiogroup" with aria-label to subscription choice container
- Clear react-query cache after merge to prevent stale data
- Wrap checkAdminStatus() in try-catch to prevent throw after token set
- Navigate to /profile/accounts after merge instead of /
- Import useQueryClient for cache management
This commit is contained in:
Fringg
2026-03-04 15:02:08 +03:00
parent 579f47e563
commit e447e993cb
3 changed files with 41 additions and 29 deletions

View File

@@ -10,7 +10,7 @@ export default function OAuthProviderIcon({
switch (provider) { switch (provider) {
case 'google': case 'google':
return ( return (
<svg className={className} viewBox="0 0 24 24"> <svg className={className} viewBox="0 0 24 24" aria-hidden="true">
<path <path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
fill="#4285F4" fill="#4285F4"
@@ -32,7 +32,7 @@ export default function OAuthProviderIcon({
case 'yandex': case 'yandex':
return ( return (
<svg className={className} viewBox="0 0 24 24"> <svg className={className} viewBox="0 0 24 24" aria-hidden="true">
<path <path
d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12z" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12z"
fill="#FC3F1D" fill="#FC3F1D"
@@ -46,7 +46,7 @@ export default function OAuthProviderIcon({
case 'discord': case 'discord':
return ( return (
<svg className={className} viewBox="0 0 24 24"> <svg className={className} viewBox="0 0 24 24" aria-hidden="true">
<path <path
d="M20.317 4.37a19.791 19.791 0 00-4.885-1.515.074.074 0 00-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 00-5.487 0 12.64 12.64 0 00-.617-1.25.077.077 0 00-.079-.037A19.736 19.736 0 003.677 4.37a.07.07 0 00-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 00.031.057 19.9 19.9 0 005.993 3.03.078.078 0 00.084-.028 14.09 14.09 0 001.226-1.994.076.076 0 00-.041-.106 13.107 13.107 0 01-1.872-.892.077.077 0 01-.008-.128 10.2 10.2 0 00.372-.292.074.074 0 01.077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 01.078.01c.12.098.246.198.373.292a.077.077 0 01-.006.127 12.299 12.299 0 01-1.873.892.077.077 0 00-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 00.084.028 19.839 19.839 0 006.002-3.03.077.077 0 00.032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 00-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" d="M20.317 4.37a19.791 19.791 0 00-4.885-1.515.074.074 0 00-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 00-5.487 0 12.64 12.64 0 00-.617-1.25.077.077 0 00-.079-.037A19.736 19.736 0 003.677 4.37a.07.07 0 00-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 00.031.057 19.9 19.9 0 005.993 3.03.078.078 0 00.084-.028 14.09 14.09 0 001.226-1.994.076.076 0 00-.041-.106 13.107 13.107 0 01-1.872-.892.077.077 0 01-.008-.128 10.2 10.2 0 00.372-.292.074.074 0 01.077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 01.078.01c.12.098.246.198.373.292a.077.077 0 01-.006.127 12.299 12.299 0 01-1.873.892.077.077 0 00-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 00.084.028 19.839 19.839 0 006.002-3.03.077.077 0 00.032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 00-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"
fill="#5865F2" fill="#5865F2"
@@ -56,7 +56,7 @@ export default function OAuthProviderIcon({
case 'vk': case 'vk':
return ( return (
<svg className={className} viewBox="0 0 24 24"> <svg className={className} viewBox="0 0 24 24" aria-hidden="true">
<path <path
d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2z" d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2z"
fill="#0077FF" fill="#0077FF"

View File

@@ -17,7 +17,7 @@ const isOAuthProvider = (provider: string): boolean => OAUTH_PROVIDERS.includes(
// Icons for providers not covered by OAuthProviderIcon // Icons for providers not covered by OAuthProviderIcon
function TelegramIcon({ className = 'h-5 w-5' }: { className?: string }) { function TelegramIcon({ className = 'h-5 w-5' }: { className?: string }) {
return ( return (
<svg className={className} viewBox="0 0 24 24"> <svg className={className} viewBox="0 0 24 24" aria-hidden="true">
<path <path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69.01-.03.01-.14-.07-.2-.08-.06-.19-.04-.28-.02-.12.03-2.02 1.28-5.69 3.77-.54.37-1.03.55-1.47.54-.48-.01-1.41-.27-2.1-.5-.85-.28-1.52-.43-1.46-.91.03-.25.38-.51 1.05-.78 4.12-1.79 6.87-2.97 8.26-3.54 3.93-1.62 4.75-1.9 5.28-1.91.12 0 .37.03.54.17.14.12.18.28.2.46-.01.06.01.24 0 .37z" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69.01-.03.01-.14-.07-.2-.08-.06-.19-.04-.28-.02-.12.03-2.02 1.28-5.69 3.77-.54.37-1.03.55-1.47.54-.48-.01-1.41-.27-2.1-.5-.85-.28-1.52-.43-1.46-.91.03-.25.38-.51 1.05-.78 4.12-1.79 6.87-2.97 8.26-3.54 3.93-1.62 4.75-1.9 5.28-1.91.12 0 .37.03.54.17.14.12.18.28.2.46-.01.06.01.24 0 .37z"
fill="#29B6F6" fill="#29B6F6"
@@ -34,6 +34,7 @@ function EmailIcon({ className = 'h-5 w-5' }: { className?: string }) {
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
strokeWidth={1.5} strokeWidth={1.5}
aria-hidden="true"
> >
<path <path
strokeLinecap="round" strokeLinecap="round"

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useParams, useNavigate, Link } from 'react-router'; import { useParams, useNavigate, Link } from 'react-router';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery, useMutation } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { authApi } from '../api/auth'; import { authApi } from '../api/auth';
import { useAuthStore } from '../store/auth'; import { useAuthStore } from '../store/auth';
@@ -74,7 +74,7 @@ function CheckCircleIcon({ className = 'h-5 w-5' }: { className?: string }) {
function TelegramIcon({ className = 'h-5 w-5' }: { className?: string }) { function TelegramIcon({ className = 'h-5 w-5' }: { className?: string }) {
return ( return (
<svg className={className} viewBox="0 0 24 24"> <svg className={className} viewBox="0 0 24 24" aria-hidden="true">
<path <path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69.01-.03.01-.14-.07-.2-.08-.06-.19-.04-.28-.02-.12.03-2.02 1.28-5.69 3.77-.54.37-1.03.55-1.47.54-.48-.01-1.41-.27-2.1-.5-.85-.28-1.52-.43-1.46-.91.03-.25.38-.51 1.05-.78 4.12-1.79 6.87-2.97 8.26-3.54 3.93-1.62 4.75-1.9 5.28-1.91.12 0 .37.03.54.17.14.12.18.28.2.46-.01.06.01.24 0 .37z" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69.01-.03.01-.14-.07-.2-.08-.06-.19-.04-.28-.02-.12.03-2.02 1.28-5.69 3.77-.54.37-1.03.55-1.47.54-.48-.01-1.41-.27-2.1-.5-.85-.28-1.52-.43-1.46-.91.03-.25.38-.51 1.05-.78 4.12-1.79 6.87-2.97 8.26-3.54 3.93-1.62 4.75-1.9 5.28-1.91.12 0 .37.03.54.17.14.12.18.28.2.46-.01.06.01.24 0 .37z"
fill="#29B6F6" fill="#29B6F6"
@@ -91,6 +91,7 @@ function EmailIcon({ className = 'h-5 w-5' }: { className?: string }) {
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
strokeWidth={1.5} strokeWidth={1.5}
aria-hidden="true"
> >
<path <path
strokeLinecap="round" strokeLinecap="round"
@@ -354,6 +355,7 @@ export default function MergeAccounts() {
const { mergeToken } = useParams<{ mergeToken: string }>(); const { mergeToken } = useParams<{ mergeToken: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const { showToast } = useToast(); const { showToast } = useToast();
const queryClient = useQueryClient();
const [selectedUserId, setSelectedUserId] = useState<number | null>(null); const [selectedUserId, setSelectedUserId] = useState<number | null>(null);
const [expiresIn, setExpiresIn] = useState(0); const [expiresIn, setExpiresIn] = useState(0);
@@ -435,10 +437,15 @@ export default function MergeAccounts() {
if (response.user) { if (response.user) {
setUser(response.user); setUser(response.user);
} }
await checkAdminStatus(); try {
await checkAdminStatus();
} catch {
// Non-critical — admin status will be checked on next navigation
}
queryClient.clear();
showToast({ type: 'success', message: t('merge.success') }); showToast({ type: 'success', message: t('merge.success') });
navigate('/', { replace: true }); navigate('/profile/accounts', { replace: true });
}, },
onError: () => { onError: () => {
showToast({ showToast({
@@ -512,27 +519,31 @@ export default function MergeAccounts() {
</motion.div> </motion.div>
)} )}
{/* Primary account card */} {/* Account cards */}
<motion.div variants={staggerItem}> <div
<AccountCard role={bothHaveSubscriptions ? 'radiogroup' : undefined}
account={data.primary} aria-label={bothHaveSubscriptions ? t('merge.chooseSubscription') : undefined}
label={t('merge.currentAccount')} >
isSelected={selectedUserId === data.primary.id} <motion.div variants={staggerItem}>
onSelect={() => setSelectedUserId(data.primary.id)} <AccountCard
showRadio={!!bothHaveSubscriptions} account={data.primary}
/> label={t('merge.currentAccount')}
</motion.div> isSelected={selectedUserId === data.primary.id}
onSelect={() => setSelectedUserId(data.primary.id)}
showRadio={!!bothHaveSubscriptions}
/>
</motion.div>
{/* Secondary account card */} <motion.div variants={staggerItem} className="mt-6">
<motion.div variants={staggerItem}> <AccountCard
<AccountCard account={data.secondary}
account={data.secondary} label={t('merge.foundAccount')}
label={t('merge.foundAccount')} isSelected={selectedUserId === data.secondary.id}
isSelected={selectedUserId === data.secondary.id} onSelect={() => setSelectedUserId(data.secondary.id)}
onSelect={() => setSelectedUserId(data.secondary.id)} showRadio={!!bothHaveSubscriptions}
showRadio={!!bothHaveSubscriptions} />
/> </motion.div>
</motion.div> </div>
{/* After merge summary */} {/* After merge summary */}
<motion.div variants={staggerItem}> <motion.div variants={staggerItem}>