ba-cafe

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

BondDashboard.tsx (22957B)


      1 "use client";
      2 
      3 import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
      4 import { 
      5   XAxis, YAxis, CartesianGrid, 
      6   ResponsiveContainer, AreaChart, Area, Tooltip, Line
      7 } from 'recharts';
      8 import { format } from 'date-fns';
      9 import { 
     10   CHARACTER_LIST, GIFT_LIST, 
     11   SPRITE_CONFIG, CharacterId,
     12   BOND_EXP_TABLE
     13 } from '@/lib/bondData';
     14 
     15 interface BondRecord {
     16   id: string; 
     17   char_key: string;
     18   bond_level: number;
     19   recorded_at: string;
     20 }
     21 
     22 const MIN_CHART_DIMENSION = 32;
     23 
     24 const getLevelFromExp = (exp: number): number => {
     25   let currentLevel = 1;
     26   for (let lv = 1; lv <= 100; lv++) {
     27     if (BOND_EXP_TABLE[lv] !== undefined && exp >= BOND_EXP_TABLE[lv]) {
     28       currentLevel = lv;
     29     } else {
     30       break;
     31     }
     32   }
     33   return currentLevel;
     34 };
     35 
     36 const TrashIcon = () => (
     37   <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
     38     <path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
     39   </svg>
     40 );
     41 
     42 const CustomTooltip = ({ active, payload, label, allData }: any) => {
     43   if (!active || !payload || !payload.length) return null;
     44 
     45   const sameDayPoints = allData.filter((d: any) => d.timestamp === label);
     46   if (sameDayPoints.length === 0) return null;
     47 
     48   const dateStr = sameDayPoints[0].recorded_at;
     49   
     50   const hasActual = sameDayPoints.some((p: any) => !p.isPrediction);
     51   const filteredPoints = sameDayPoints.filter((p: any) => {
     52     if (hasActual && p.isPrediction) return false;
     53     return true;
     54   });
     55 
     56   const sortedPoints = filteredPoints.sort((a: any, b: any) => b.bond_level - a.bond_level);
     57   const isAllPrediction = sortedPoints.every((p: any) => p.isPrediction);
     58 
     59   return (
     60     <div 
     61       className="recharts-default-tooltip"
     62       style={{
     63         margin: 0,
     64         padding: '10px',
     65         backgroundColor: 'var(--card)',
     66         border: '1px solid var(--muted)',
     67         borderRadius: '0.5rem',
     68         fontSize: '0.85rem',
     69         whiteSpace: 'nowrap'
     70       }}
     71     >
     72       <p 
     73         className="recharts-tooltip-label"
     74         style={{
     75           margin: '0px 0px 4px',
     76           color: 'var(--foreground)',
     77           fontWeight: 700
     78         }}
     79       >
     80         {isAllPrediction ? `Estimated Date: ${dateStr}` : `Date: ${dateStr}`}
     81       </p>
     82 
     83       <ul className="recharts-tooltip-item-list" style={{ padding: 0, margin: 0, listStyle: 'none' }}>
     84         {sortedPoints.map((p: any, idx: number) => (
     85           <li 
     86             key={idx} 
     87             className="recharts-tooltip-item" 
     88             style={{ display: 'block', paddingTop: '4px', paddingBottom: '4px' }}
     89           >
     90             <span style={{ fontWeight: 700 }}>
     91               Rank {p.bond_level} {p.isPrediction ? '' : `(${p.actual_exp} exp)`}
     92             </span>
     93           </li>
     94         ))}
     95       </ul>
     96     </div>
     97   );
     98 };
     99 
    100 export default function BondDashboard({ 
    101   bondHistory, 
    102   onSave,
    103   isSyncing 
    104 }: { 
    105   bondHistory: BondRecord[], 
    106   onSave: (level: number, date: string, charKey: string) => Promise<void>,
    107   isSyncing?: boolean 
    108 }) {
    109   const [selectedCharId, setSelectedCharId] = useState<CharacterId>(CHARACTER_LIST[0].id);
    110   const [isMounted, setIsMounted] = useState(false);
    111   
    112   const containerRef = useRef<HTMLDivElement | null>(null);
    113   const [isChartReady, setIsChartReady] = useState(false);
    114 
    115   useEffect(() => {
    116     if (typeof window !== 'undefined') {
    117       const savedCharId = localStorage.getItem('last_selected_char_id') as CharacterId | null;
    118       if (savedCharId && CHARACTER_LIST.some(c => c.id === savedCharId)) {
    119         setSelectedCharId(savedCharId);
    120       }
    121       setIsMounted(true);
    122     }
    123   }, []);
    124 
    125   useLayoutEffect(() => {
    126     const el = containerRef.current;
    127     if (!el) return;
    128 
    129     const update = (rect = el.getBoundingClientRect()) => {
    130       if (rect.width >= MIN_CHART_DIMENSION && rect.height >= MIN_CHART_DIMENSION) {
    131         setIsChartReady(true);
    132       }
    133     };
    134 
    135     update();
    136     if (typeof ResizeObserver === "undefined") return;
    137 
    138     const observer = new ResizeObserver((entries) => {
    139       for (const entry of entries) {
    140         if (entry.target === el) update(entry.contentRect);
    141       }
    142     });
    143     observer.observe(el);
    144     
    145     return () => observer.disconnect();
    146   }, []);
    147 
    148   const [inputLevel, setInputLevel] = useState(1);
    149   const [inputDate, setInputDate] = useState(format(new Date(), 'yyyy-MM-dd'));
    150 
    151   const [isSelectOpen, setIsSelectOpen] = useState(false);
    152   const [isDesktop, setIsDesktop] = useState(false);
    153   
    154   const dropdownRef = useRef<HTMLDivElement>(null);
    155 
    156   useEffect(() => {
    157     if (isSelectOpen && dropdownRef.current) {
    158       setTimeout(() => {
    159         const activeItem = dropdownRef.current?.querySelector('.char-dropdown-item.active');
    160         if (activeItem) {
    161           activeItem.scrollIntoView({
    162             behavior: 'smooth',
    163             block: 'center',
    164           });
    165         }
    166       }, 20);
    167     }
    168   }, [isSelectOpen]);
    169 
    170   const selectedChar = useMemo(() => 
    171     CHARACTER_LIST.find(c => c.id === selectedCharId) || CHARACTER_LIST[0], 
    172   [selectedCharId]);
    173 
    174   const sortedGifts = useMemo(() => {
    175     const ratingOrder: Record<string, number> = {
    176       'ss': 1, 'sa': 2, 'ns': 3, 'na': 4, 'nb': 5
    177     };
    178 
    179     return GIFT_LIST
    180       .filter(gift => selectedChar.giftRatings[gift.name])
    181       .sort((a, b) => {
    182         const ratingA = selectedChar.giftRatings[a.name]?.toLowerCase() || '';
    183         const ratingB = selectedChar.giftRatings[b.name]?.toLowerCase() || '';
    184         const orderA = ratingOrder[ratingA] || 99;
    185         const orderB = ratingOrder[ratingB] || 99;
    186         return orderA - orderB;
    187       });
    188   }, [selectedChar]);
    189 
    190   const filteredHistory = useMemo(() => {
    191     return bondHistory
    192       .filter(h => h.char_key === selectedCharId)
    193       .sort((a, b) => {
    194         const dateCompare = b.recorded_at.localeCompare(a.recorded_at);
    195         if (dateCompare !== 0) return dateCompare;
    196         return b.bond_level - a.bond_level;
    197       });
    198   }, [bondHistory, selectedCharId]);
    199 
    200   const displayHistoryRows = useMemo(() => {
    201     const rows = [];
    202     for (let i = 0; i < 6; i++) {
    203       rows.push(filteredHistory[i] || null);
    204     }
    205     return rows;
    206   }, [filteredHistory]);
    207 
    208   const chartDataCombined = useMemo(() => {
    209     if (filteredHistory.length === 0) return { data: [], hasPrediction: false };
    210 
    211     const actualPoints = [...bondHistory]
    212       .filter(h => h.char_key === selectedCharId)
    213       .sort((a, b) => {
    214         const dateCompare = a.recorded_at.localeCompare(b.recorded_at);
    215         if (dateCompare !== 0) return dateCompare;
    216         return a.bond_level - b.bond_level;
    217       })
    218       .map(h => ({
    219         recorded_at: h.recorded_at,
    220         timestamp: new Date(h.recorded_at).getTime(),
    221         cumulative_exp: BOND_EXP_TABLE[h.bond_level] || 0,
    222         bond_level: h.bond_level,
    223         isPrediction: false,
    224         actual_exp: BOND_EXP_TABLE[h.bond_level] || 0,
    225         predicted_exp: null as number | null,
    226       }));
    227 
    228     const lastActual = actualPoints[actualPoints.length - 1];
    229     const targetExp = 240225;
    230 
    231     if (lastActual.cumulative_exp >= targetExp || actualPoints.length < 2) {
    232       return { data: actualPoints, hasPrediction: false, xDomain: ['dataMin', 'dataMax'] as const };
    233     }
    234 
    235     const n = actualPoints.length;
    236     let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
    237     for (const p of actualPoints) {
    238       sumX += p.timestamp;
    239       sumY += p.cumulative_exp;
    240       sumXY += p.timestamp * p.cumulative_exp;
    241       sumXX += p.timestamp * p.timestamp;
    242     }
    243 
    244     const denominator = n * sumXX - sumX * sumX;
    245     if (denominator === 0) {
    246       return { data: actualPoints, hasPrediction: false, xDomain: ['dataMin', 'dataMax'] as const };
    247     }
    248 
    249     const slope = (n * sumXY - sumX * sumY) / denominator;
    250     const intercept = (sumY - slope * sumX) / n;
    251 
    252     if (slope <= 0) {
    253       return { data: actualPoints, hasPrediction: false, xDomain: ['dataMin', 'dataMax'] as const };
    254     }
    255 
    256     const predictionPoints: any[] = [];
    257     
    258     predictionPoints.push({
    259       recorded_at: lastActual.recorded_at,
    260       timestamp: lastActual.timestamp,
    261       cumulative_exp: lastActual.cumulative_exp,
    262       bond_level: lastActual.bond_level,
    263       isPrediction: true,
    264       actual_exp: null,
    265       predicted_exp: lastActual.cumulative_exp
    266     });
    267 
    268     let nextTargetLevel = Math.floor((lastActual.bond_level / 5) + 1) * 5;
    269     if (nextTargetLevel === lastActual.bond_level) {
    270       nextTargetLevel += 5;
    271     }
    272 
    273     for (let lv = nextTargetLevel; lv <= 100; lv += 5) {
    274       const expAtLv = BOND_EXP_TABLE[lv];
    275       if (expAtLv === undefined) continue;
    276 
    277       const ts = Math.round((expAtLv - intercept) / slope);
    278       
    279       predictionPoints.push({
    280         recorded_at: format(new Date(ts), 'yyyy-MM-dd'),
    281         timestamp: ts,
    282         cumulative_exp: expAtLv,
    283         bond_level: lv,
    284         isPrediction: true,
    285         actual_exp: null,
    286         predicted_exp: expAtLv
    287       });
    288     }
    289 
    290     const finalPoint = predictionPoints[predictionPoints.length - 1];
    291     if (!finalPoint || finalPoint.bond_level !== 100) {
    292       const ts100 = Math.round((targetExp - intercept) / slope);
    293       predictionPoints.push({
    294         recorded_at: format(new Date(ts100), 'yyyy-MM-dd'),
    295         timestamp: ts100,
    296         cumulative_exp: targetExp,
    297         bond_level: 100,
    298         isPrediction: true,
    299         actual_exp: null,
    300         predicted_exp: targetExp
    301       });
    302     }
    303 
    304     return {
    305       data: [...actualPoints, ...predictionPoints.slice(0)],
    306       hasPrediction: true,
    307       xDomain: [actualPoints[0].timestamp, predictionPoints[predictionPoints.length - 1].timestamp] as const
    308     };
    309   }, [filteredHistory]);
    310 
    311   const graphData = chartDataCombined.data;
    312 
    313   useEffect(() => {
    314     if (!isMounted) return;
    315     localStorage.setItem('last_selected_char_id', selectedCharId);
    316     const charHistory = bondHistory
    317       .filter(h => h.char_key === selectedCharId)
    318       .sort((a, b) => {
    319         const dateCompare = b.recorded_at.localeCompare(a.recorded_at);
    320         if (dateCompare !== 0) return dateCompare;
    321         return b.bond_level - a.bond_level;
    322       });
    323 
    324     if (charHistory.length > 0) {
    325       setInputLevel(Math.min(100, charHistory[0].bond_level + 1));
    326     } else {
    327       setInputLevel(1);
    328     }
    329   }, [selectedCharId, bondHistory, isMounted]);
    330 
    331   useEffect(() => {
    332     const handleResize = () => {
    333       setIsDesktop(window.innerWidth >= 1000);
    334     };
    335     handleResize();
    336     window.addEventListener('resize', handleResize);
    337     return () => window.removeEventListener('resize', handleResize);
    338   }, []);
    339 
    340   useEffect(() => {
    341     const handleClickOutside = (event: MouseEvent) => {
    342       if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
    343         setIsSelectOpen(false);
    344       }
    345     };
    346     document.addEventListener('mousedown', handleClickOutside);
    347     return () => document.removeEventListener('mousedown', handleClickOutside);
    348   }, []);
    349 
    350   const handleSave = async () => {
    351     await onSave(inputLevel, inputDate, selectedCharId);
    352   };
    353 
    354   const handleDelete = async (recordId: string) => {
    355     if (!window.confirm("この記録を削除してもよろしいですか?")) return;
    356 
    357     try {
    358       let token = '';
    359       if (typeof window !== 'undefined') {
    360         const authKey = Object.keys(localStorage).find(key => key.startsWith('sb-') && key.endsWith('-auth-token'));
    361         if (authKey) {
    362           const authDataString = localStorage.getItem(authKey);
    363           if (authDataString) {
    364             const authData = JSON.parse(authDataString);
    365             token = authData?.access_token || '';
    366           }
    367         }
    368       }
    369 
    370       const res = await fetch('/api/relationship', {
    371         method: 'DELETE',
    372         headers: {
    373           'Content-Type': 'application/json',
    374           ...(token ? { 'Authorization': `Bearer ${token}` } : {}),
    375         },
    376         body: JSON.stringify({ id: recordId }),
    377       });
    378 
    379       if (!res.ok) throw new Error('Failed to delete');
    380 
    381       window.location.reload();
    382     } catch (err) {
    383       console.error(err);
    384       alert("削除に失敗しました。");
    385     }
    386   };
    387 
    388   const renderGraph = (
    389     <div 
    390       ref={containerRef}
    391       className={`graph-container ${!isChartReady ? 'opacity-0 pointer-events-none' : ''}`}
    392       data-chart-ready={isChartReady ? "true" : "false"}
    393     >
    394       {isChartReady && (
    395         graphData.length > 0 ? (
    396           <ResponsiveContainer width="100%" height="100%">
    397             <AreaChart data={graphData} margin={{ top: 5, right: 5, left: -35, bottom: -10 }}>
    398               <defs>
    399                 <linearGradient id="colorExp" x1="0" y1="0" x2="0" y2="1">
    400                   <stop offset="5%" stopColor="var(--primary)" stopOpacity={0.4}/>
    401                   <stop offset="60%" stopColor="var(--primary)" stopOpacity={0.2}/>
    402                   <stop offset="95%" stopColor="var(--primary)" stopOpacity={0}/>
    403                 </linearGradient>
    404               </defs>
    405               <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--secondary)" opacity={0.2} />
    406               <XAxis 
    407                 dataKey="timestamp" 
    408                 type="number"
    409                 domain={chartDataCombined.xDomain || ['dataMin', 'dataMax']}
    410                 tickLine={true}
    411                 axisLine={true}
    412                 stroke="var(--secondary-foreground)"
    413                 fontSize={10}
    414                 tickCount={14}
    415                 tickFormatter={(ts) => {
    416                   try {
    417                     const d = new Date(ts);
    418                     const y = d.getFullYear();
    419                     const m = String(d.getMonth() + 1).padStart(2, '0');
    420                     return `${y}/${m}`;
    421                   } catch (e) {}
    422                   return '';
    423                 }}
    424               />
    425               <YAxis 
    426                 dataKey="cumulative_exp" 
    427                 domain={[0, 240225]} 
    428                 ticks={[0, 14790, 29175, 50835, 81270, 121980, 174465, 240225]}
    429                 tickFormatter={(exp) => `${getLevelFromExp(exp)}`}
    430                 stroke="var(--secondary-foreground)"
    431                 fontSize={10}
    432                 tickLine={true}
    433                 axisLine={true}
    434               />
    435               <Tooltip 
    436                 content={<CustomTooltip allData={graphData} />}
    437                 trigger="hover"
    438                 shared={true}
    439               />
    440               <Area 
    441                 type="monotone"
    442                 dataKey="actual_exp" 
    443                 stroke="var(--primary)" 
    444                 strokeWidth={3} 
    445                 fillOpacity={1} 
    446                 fill="url(#colorExp)" 
    447                 connectNulls={false}
    448               />
    449               <Line
    450                 type="monotone"
    451                 dataKey="predicted_exp"
    452                 stroke="var(--primary)"
    453                 strokeWidth={2}
    454                 strokeDasharray="5 5"
    455                 dot={false}
    456                 activeDot={false}
    457                 connectNulls={true}
    458               />
    459             </AreaChart>
    460           </ResponsiveContainer>
    461         ) : (
    462           <div className="flex h-full w-full items-center justify-center border border-dashed border-(--muted) rounded-2xl bg-(--card)/30">
    463             <span className="text-lg font-bold text-(--muted-foreground) tracking-wider opacity-60">
    464               No Data
    465             </span>
    466           </div>
    467         )
    468       )}
    469     </div>
    470   );
    471 
    472   return (
    473     <div className="bond-container">
    474       <div className="bond-split-layout">
    475         <div className="bond-left-panel">
    476           <div className="char-selector" ref={dropdownRef}>
    477             <button 
    478               className="char-selector-trigger"
    479               onClick={() => setIsSelectOpen(!isSelectOpen)}
    480             >
    481               <span className="char-selector-label">Student</span>
    482               <h1 className="char-selector-name">
    483                 {selectedChar.name}
    484                 <span className={`char-selector-arrow ${isSelectOpen ? 'open' : ''}`}>
    485    486                 </span>
    487               </h1>
    488             </button>
    489 
    490             {isSelectOpen && (
    491               <div className="char-dropdown scrollbar-hide animate-in fade-in zoom-in-95 duration-200">
    492                 <div className="char-dropdown-container">
    493                   {CHARACTER_LIST.map((char) => (
    494                     <button
    495                       key={char.id}
    496                       onClick={() => {
    497                         setSelectedCharId(char.id as CharacterId);
    498                         setIsSelectOpen(false);
    499                       }}
    500                       className={`char-dropdown-item ${selectedCharId === char.id ? 'active' : ''}`}
    501                     >
    502                       {char.name}
    503                     </button>
    504                   ))}
    505                 </div>
    506               </div>
    507             )}
    508           </div>
    509 
    510           <div className="record-input-area">
    511             <div className="input-group-container">
    512               {/* Rank フィールド */}
    513               <div className="input-field-wrapper rank-field">
    514                 <label className="input-label">Rank</label>
    515                 <div className="input-content">
    516                   <span className="rank-value">{inputLevel}</span>
    517                   <div className="spin-buttons">
    518                     <button 
    519                       onClick={() => setInputLevel(prev => Math.min(100, prev + 1))} 
    520                       className="spin-btn"
    521                     >
    522    523                     </button>
    524                     <button 
    525                       onClick={() => setInputLevel(prev => Math.max(1, prev - 1))} 
    526                       className="spin-btn"
    527                     >
    528    529                     </button>
    530                   </div>
    531                 </div>
    532               </div>
    533 
    534               {/* Date フィールド */}
    535               <div className="input-field-wrapper date-field">
    536                 <label className="input-label">Date</label>
    537                 <div className="input-content">
    538                   <input 
    539                     type="date" 
    540                     value={inputDate} 
    541                     onChange={(e) => setInputDate(e.target.value)} 
    542                     className="date-input-display"
    543                   />
    544                 </div>
    545               </div>
    546 
    547               {/* Record ボタンを同じコンテナ内に並列で配置 */}
    548               <button onClick={handleSave} disabled={isSyncing} className="btn-bond-record">
    549                 <span>Record</span>
    550                 <div>+</div>
    551               </button>
    552             </div>
    553           </div>
    554 
    555           <div className="section-wrapper">
    556             <h3 className="gift-title">Favorite Gifts</h3>
    557             <div className="gift-scroll-container scrollbar-hide">
    558               {sortedGifts.map((gift) => {
    559                 const rating = selectedChar.giftRatings[gift.name];
    560                 const rankChar = rating?.slice(-1).toUpperCase();
    561                 const row = Math.floor(gift.spriteIdx / SPRITE_CONFIG.cols);
    562                 const col = gift.spriteIdx % SPRITE_CONFIG.cols;
    563 
    564                 return (
    565                   <div key={gift.name} className="gift-item">
    566                     <div className="gift-image-box">
    567                       <div 
    568                         className="gift-sprite" 
    569                         style={{ '--col': col, '--row': row } as React.CSSProperties} 
    570                       />
    571                       <div className={`gift-rank-dot ${
    572                         rankChar === 'S' ? 's' : rankChar === 'A' ? 'a' : 'b'
    573                       }`} />
    574                     </div>
    575                     <span className="gift-category-text">
    576                       {gift.category || "GIFT"}
    577                     </span>
    578                   </div>
    579                 );
    580               })}
    581             </div>
    582           </div>
    583 
    584           <div className="section-wrapper">
    585             <h3 className="section-toggle-btn" style={{ cursor: 'default' }}>
    586               <span>History Log</span>
    587             </h3>
    588             <div className="history-grid-container">
    589               {displayHistoryRows.map((record, index) => (
    590                 <div key={record ? record.id : `empty-${index}`} className="history-item">
    591                   <div className="history-log-row">
    592                     {record ? (
    593                       <>
    594                         <span className="history-rank-num">
    595                           {record.bond_level}
    596                         </span>
    597                         <span className="history-divider">|</span>
    598                         <span className="history-date-text">
    599                           {record.recorded_at}
    600                         </span>
    601                       </>
    602                     ) : (
    603                       <span className="history-empty-text">
    604                         <span className="history-rank-num">
    605                           ---
    606                         </span>
    607                         <span className="history-divider">|</span>
    608                         <span className="history-date-text">
    609                           ---- -- --
    610                         </span>
    611                       </span>
    612                     )}
    613                   </div>
    614                   
    615                   {record ? (
    616                     <button 
    617                       onClick={() => handleDelete(record.id)} 
    618                       className="edit-icon-btn text-rose-500 hover:text-rose-700 hover:bg-rose-500/10"
    619                       aria-label="Delete record"
    620                     >
    621                       <TrashIcon />
    622                     </button>
    623                   ) : (
    624                     <span className="edit-btn-placeholder" />
    625                   )}
    626                 </div>
    627               ))}
    628             </div>
    629           </div>
    630 
    631           {!isDesktop && (
    632             <div className="section-wrapper">
    633               <h3 className="section-toggle-btn" style={{ cursor: 'default' }}>
    634                 <span>Progress Graph</span>
    635               </h3>
    636               <div className="animate-in fade-in duration-200">
    637                 {renderGraph}
    638               </div>
    639             </div>
    640           )}
    641         </div>
    642         {isDesktop && (
    643           <div className="bond-right-panel animate-in fade-in duration-300">
    644             <h3 className="section-toggle-btn" style={{ cursor: 'default' }}>Progress Graph</h3>
    645             {renderGraph}
    646           </div>
    647         )}
    648       </div>
    649     </div>
    650   );
    651 }
    652 
    653 if (typeof window !== "undefined") {
    654   const filterWarning = (args: any[], originalFn: (...args: any[]) => void) => {
    655     if (
    656       typeof args[0] === "string" &&
    657       args[0].includes("should be greater than 0")
    658     ) {
    659       return;
    660     }
    661     originalFn(...args);
    662   };
    663 
    664   const originalConsoleError = console.error;
    665   console.error = (...args: any[]) => filterWarning(args, originalConsoleError);
    666   const originalConsoleWarn = console.warn;
    667   console.warn = (...args: any[]) => filterWarning(args, originalConsoleWarn);
    668 }