feat(worker): auth gate, Dutch login screen, router and 3-tab shell
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
"react-router-dom": "^6.26.0"
|
"react-router-dom": "^6.26.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/jest-dom": "^6.4.0",
|
"@testing-library/jest-dom": "^6.4.0",
|
||||||
"@testing-library/react": "^16.0.0",
|
"@testing-library/react": "^16.0.0",
|
||||||
"@testing-library/user-event": "^14.5.0",
|
"@testing-library/user-event": "^14.5.0",
|
||||||
|
|||||||
45
apps/worker/src/App.test.tsx
Normal file
45
apps/worker/src/App.test.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { render, screen, within } from '@testing-library/react';
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import App from './App';
|
||||||
|
import { clearToken, setToken } from './lib/auth-storage';
|
||||||
|
|
||||||
|
// Stub the network layer so stub screens render without real requests.
|
||||||
|
vi.mock('./lib/api', async () => {
|
||||||
|
const actual = await vi.importActual<typeof import('./lib/api')>('./lib/api');
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
apiFetch: vi.fn().mockResolvedValue([]),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderApp() {
|
||||||
|
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<App />
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
clearToken();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the login screen when there is no token', () => {
|
||||||
|
clearToken();
|
||||||
|
renderApp();
|
||||||
|
expect(screen.getByText('E-mailadres')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: 'Inloggen' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the tab bar when a token is present', () => {
|
||||||
|
setToken('tok');
|
||||||
|
renderApp();
|
||||||
|
const tabbar = within(screen.getByRole('navigation'));
|
||||||
|
expect(tabbar.getByText('Stopwatch')).toBeInTheDocument();
|
||||||
|
expect(tabbar.getByText('Geschiedenis')).toBeInTheDocument();
|
||||||
|
expect(tabbar.getByText('Instellingen')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,35 @@
|
|||||||
export default function App() {
|
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||||
return <div>SoleLog</div>;
|
import { AuthProvider, useAuth } from './auth/AuthContext';
|
||||||
|
import Login from './screens/Login';
|
||||||
|
import TabBar from './components/TabBar';
|
||||||
|
import Stopwatch from './screens/Stopwatch';
|
||||||
|
import History from './screens/History';
|
||||||
|
import Settings from './screens/Settings';
|
||||||
|
|
||||||
|
function AuthedShell() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<main className="app-main">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Stopwatch />} />
|
||||||
|
<Route path="/history" element={<History />} />
|
||||||
|
<Route path="/settings" element={<Settings />} />
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
<TabBar />
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Gate() {
|
||||||
|
const { isAuthed } = useAuth();
|
||||||
|
return isAuthed ? <AuthedShell /> : <Login />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<AuthProvider>
|
||||||
|
<Gate />
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
48
apps/worker/src/auth/AuthContext.tsx
Normal file
48
apps/worker/src/auth/AuthContext.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { createContext, useCallback, useContext, useState, type ReactNode } from 'react';
|
||||||
|
import { clearToken, getToken } from '../lib/auth-storage';
|
||||||
|
import { signIn as apiSignIn, signUp as apiSignUp } from '../lib/api';
|
||||||
|
|
||||||
|
interface AuthContextValue {
|
||||||
|
isAuthed: boolean;
|
||||||
|
signIn: (email: string, password: string) => Promise<void>;
|
||||||
|
signUp: (email: string, password: string) => Promise<void>;
|
||||||
|
signOut: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [isAuthed, setIsAuthed] = useState<boolean>(() => getToken() !== null);
|
||||||
|
|
||||||
|
const signIn = useCallback(async (email: string, password: string) => {
|
||||||
|
await apiSignIn(email, password);
|
||||||
|
setIsAuthed(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const signUp = useCallback(
|
||||||
|
async (email: string, password: string) => {
|
||||||
|
// Register, then sign in to obtain the bearer token.
|
||||||
|
await apiSignUp(email, password);
|
||||||
|
await apiSignIn(email, password);
|
||||||
|
setIsAuthed(true);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const signOut = useCallback(() => {
|
||||||
|
clearToken();
|
||||||
|
setIsAuthed(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={{ isAuthed, signIn, signUp, signOut }}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuth(): AuthContextValue {
|
||||||
|
const ctx = useContext(AuthContext);
|
||||||
|
if (!ctx) throw new Error('useAuth must be used within an AuthProvider');
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
24
apps/worker/src/components/TabBar.tsx
Normal file
24
apps/worker/src/components/TabBar.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ to: '/', label: 'Stopwatch' },
|
||||||
|
{ to: '/history', label: 'Geschiedenis' },
|
||||||
|
{ to: '/settings', label: 'Instellingen' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export default function TabBar() {
|
||||||
|
return (
|
||||||
|
<nav className="tabbar">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<NavLink
|
||||||
|
key={tab.to}
|
||||||
|
to={tab.to}
|
||||||
|
end={tab.to === '/'}
|
||||||
|
className={({ isActive }) => (isActive ? 'tab tab-active' : 'tab')}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
apps/worker/src/screens/History.tsx
Normal file
7
apps/worker/src/screens/History.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default function History() {
|
||||||
|
return (
|
||||||
|
<div className="screen">
|
||||||
|
<h1 className="screen-title">Geschiedenis</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
apps/worker/src/screens/Login.tsx
Normal file
74
apps/worker/src/screens/Login.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { useState, type FormEvent } from 'react';
|
||||||
|
import { useAuth } from '../auth/AuthContext';
|
||||||
|
|
||||||
|
type Mode = 'signin' | 'signup';
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const { signIn, signUp } = useAuth();
|
||||||
|
const [mode, setMode] = useState<Mode>('signin');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
|
const submitLabel = mode === 'signin' ? 'Inloggen' : 'Registreren';
|
||||||
|
|
||||||
|
async function handleSubmit(e: FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
setError(null);
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
if (mode === 'signin') {
|
||||||
|
await signIn(email, password);
|
||||||
|
} else {
|
||||||
|
await signUp(email, password);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setError(mode === 'signin' ? 'Inloggen mislukt' : 'Registreren mislukt');
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="login">
|
||||||
|
<h1 className="login-title">SoleLog</h1>
|
||||||
|
<form className="login-form" onSubmit={handleSubmit}>
|
||||||
|
<label className="field">
|
||||||
|
<span className="field-label">E-mailadres</span>
|
||||||
|
<input
|
||||||
|
className="field-input"
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="field">
|
||||||
|
<span className="field-label">Wachtwoord</span>
|
||||||
|
<input
|
||||||
|
className="field-input"
|
||||||
|
type="password"
|
||||||
|
autoComplete={mode === 'signin' ? 'current-password' : 'new-password'}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{error && <p className="login-error">{error}</p>}
|
||||||
|
<button className="btn-primary" type="submit" disabled={busy}>
|
||||||
|
{submitLabel}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<button
|
||||||
|
className="login-toggle"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setMode((m) => (m === 'signin' ? 'signup' : 'signin'));
|
||||||
|
setError(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{mode === 'signin' ? 'Nog geen account? Registreren' : 'Al een account? Inloggen'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
apps/worker/src/screens/Settings.tsx
Normal file
7
apps/worker/src/screens/Settings.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default function Settings() {
|
||||||
|
return (
|
||||||
|
<div className="screen">
|
||||||
|
<h1 className="screen-title">Instellingen</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
apps/worker/src/screens/Stopwatch.tsx
Normal file
7
apps/worker/src/screens/Stopwatch.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default function Stopwatch() {
|
||||||
|
return (
|
||||||
|
<div className="screen">
|
||||||
|
<h1 className="screen-title">Stopwatch</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,3 +22,131 @@ body {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- App shell layout (mobile-first) ---- */
|
||||||
|
.app-main {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 16px 16px 80px; /* leave room for the fixed bottom tab bar */
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen {
|
||||||
|
max-width: 640px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 8px 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Bottom tab bar ---- */
|
||||||
|
.tabbar {
|
||||||
|
position: fixed;
|
||||||
|
inset: auto 0 0 0;
|
||||||
|
display: flex;
|
||||||
|
background: #ffffff;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-active {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Login screen (mobile-first) ---- */
|
||||||
|
.login {
|
||||||
|
max-width: 420px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 48px 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
min-height: 100vh;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
background: var(--primary);
|
||||||
|
border: none;
|
||||||
|
border-radius: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
background: var(--border);
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-error {
|
||||||
|
color: var(--danger);
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-toggle {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--primary);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|||||||
92
yarn.lock
92
yarn.lock
@@ -25,7 +25,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@babel/code-frame@npm:^7.29.7":
|
"@babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.29.7":
|
||||||
version: 7.29.7
|
version: 7.29.7
|
||||||
resolution: "@babel/code-frame@npm:7.29.7"
|
resolution: "@babel/code-frame@npm:7.29.7"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1793,6 +1793,7 @@ __metadata:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@solelog/shared": "workspace:*"
|
"@solelog/shared": "workspace:*"
|
||||||
"@tanstack/react-query": "npm:^5.0.0"
|
"@tanstack/react-query": "npm:^5.0.0"
|
||||||
|
"@testing-library/dom": "npm:^10.4.1"
|
||||||
"@testing-library/jest-dom": "npm:^6.4.0"
|
"@testing-library/jest-dom": "npm:^6.4.0"
|
||||||
"@testing-library/react": "npm:^16.0.0"
|
"@testing-library/react": "npm:^16.0.0"
|
||||||
"@testing-library/user-event": "npm:^14.5.0"
|
"@testing-library/user-event": "npm:^14.5.0"
|
||||||
@@ -1834,6 +1835,22 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@testing-library/dom@npm:^10.4.1":
|
||||||
|
version: 10.4.1
|
||||||
|
resolution: "@testing-library/dom@npm:10.4.1"
|
||||||
|
dependencies:
|
||||||
|
"@babel/code-frame": "npm:^7.10.4"
|
||||||
|
"@babel/runtime": "npm:^7.12.5"
|
||||||
|
"@types/aria-query": "npm:^5.0.1"
|
||||||
|
aria-query: "npm:5.3.0"
|
||||||
|
dom-accessibility-api: "npm:^0.5.9"
|
||||||
|
lz-string: "npm:^1.5.0"
|
||||||
|
picocolors: "npm:1.1.1"
|
||||||
|
pretty-format: "npm:^27.0.2"
|
||||||
|
checksum: 10c0/19ce048012d395ad0468b0dbcc4d0911f6f9e39464d7a8464a587b29707eed5482000dad728f5acc4ed314d2f4d54f34982999a114d2404f36d048278db815b1
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@testing-library/jest-dom@npm:^6.4.0":
|
"@testing-library/jest-dom@npm:^6.4.0":
|
||||||
version: 6.9.1
|
version: 6.9.1
|
||||||
resolution: "@testing-library/jest-dom@npm:6.9.1"
|
resolution: "@testing-library/jest-dom@npm:6.9.1"
|
||||||
@@ -1877,6 +1894,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/aria-query@npm:^5.0.1":
|
||||||
|
version: 5.0.4
|
||||||
|
resolution: "@types/aria-query@npm:5.0.4"
|
||||||
|
checksum: 10c0/dc667bc6a3acc7bba2bccf8c23d56cb1f2f4defaa704cfef595437107efaa972d3b3db9ec1d66bc2711bfc35086821edd32c302bffab36f2e79b97f312069f08
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/babel__core@npm:^7.20.5":
|
"@types/babel__core@npm:^7.20.5":
|
||||||
version: 7.20.5
|
version: 7.20.5
|
||||||
resolution: "@types/babel__core@npm:7.20.5"
|
resolution: "@types/babel__core@npm:7.20.5"
|
||||||
@@ -2099,6 +2123,29 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"ansi-regex@npm:^5.0.1":
|
||||||
|
version: 5.0.1
|
||||||
|
resolution: "ansi-regex@npm:5.0.1"
|
||||||
|
checksum: 10c0/9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"ansi-styles@npm:^5.0.0":
|
||||||
|
version: 5.2.0
|
||||||
|
resolution: "ansi-styles@npm:5.2.0"
|
||||||
|
checksum: 10c0/9c4ca80eb3c2fb7b33841c210d2f20807f40865d27008d7c3f707b7f95cab7d67462a565e2388ac3285b71cb3d9bb2173de8da37c57692a362885ec34d6e27df
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"aria-query@npm:5.3.0":
|
||||||
|
version: 5.3.0
|
||||||
|
resolution: "aria-query@npm:5.3.0"
|
||||||
|
dependencies:
|
||||||
|
dequal: "npm:^2.0.3"
|
||||||
|
checksum: 10c0/2bff0d4eba5852a9dd578ecf47eaef0e82cc52569b48469b0aac2db5145db0b17b7a58d9e01237706d1e14b7a1b0ac9b78e9c97027ad97679dd8f91b85da1469
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"aria-query@npm:^5.0.0":
|
"aria-query@npm:^5.0.0":
|
||||||
version: 5.3.2
|
version: 5.3.2
|
||||||
resolution: "aria-query@npm:5.3.2"
|
resolution: "aria-query@npm:5.3.2"
|
||||||
@@ -2414,6 +2461,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"dequal@npm:^2.0.3":
|
||||||
|
version: 2.0.3
|
||||||
|
resolution: "dequal@npm:2.0.3"
|
||||||
|
checksum: 10c0/f98860cdf58b64991ae10205137c0e97d384c3a4edc7f807603887b7c4b850af1224a33d88012009f150861cbee4fa2d322c4cc04b9313bee312e47f6ecaa888
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"detect-libc@npm:2.0.2":
|
"detect-libc@npm:2.0.2":
|
||||||
version: 2.0.2
|
version: 2.0.2
|
||||||
resolution: "detect-libc@npm:2.0.2"
|
resolution: "detect-libc@npm:2.0.2"
|
||||||
@@ -2421,6 +2475,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"dom-accessibility-api@npm:^0.5.9":
|
||||||
|
version: 0.5.16
|
||||||
|
resolution: "dom-accessibility-api@npm:0.5.16"
|
||||||
|
checksum: 10c0/b2c2eda4fae568977cdac27a9f0c001edf4f95a6a6191dfa611e3721db2478d1badc01db5bb4fa8a848aeee13e442a6c2a4386d65ec65a1436f24715a2f8d053
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"dom-accessibility-api@npm:^0.6.3":
|
"dom-accessibility-api@npm:^0.6.3":
|
||||||
version: 0.6.3
|
version: 0.6.3
|
||||||
resolution: "dom-accessibility-api@npm:0.6.3"
|
resolution: "dom-accessibility-api@npm:0.6.3"
|
||||||
@@ -3401,6 +3462,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lz-string@npm:^1.5.0":
|
||||||
|
version: 1.5.0
|
||||||
|
resolution: "lz-string@npm:1.5.0"
|
||||||
|
bin:
|
||||||
|
lz-string: bin/bin.js
|
||||||
|
checksum: 10c0/36128e4de34791838abe979b19927c26e67201ca5acf00880377af7d765b38d1c60847e01c5ec61b1a260c48029084ab3893a3925fd6e48a04011364b089991b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"magic-string@npm:^0.30.17":
|
"magic-string@npm:^0.30.17":
|
||||||
version: 0.30.21
|
version: 0.30.21
|
||||||
resolution: "magic-string@npm:0.30.21"
|
resolution: "magic-string@npm:0.30.21"
|
||||||
@@ -3710,7 +3780,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"picocolors@npm:^1.1.1":
|
"picocolors@npm:1.1.1, picocolors@npm:^1.1.1":
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
resolution: "picocolors@npm:1.1.1"
|
resolution: "picocolors@npm:1.1.1"
|
||||||
checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58
|
checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58
|
||||||
@@ -3735,6 +3805,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"pretty-format@npm:^27.0.2":
|
||||||
|
version: 27.5.1
|
||||||
|
resolution: "pretty-format@npm:27.5.1"
|
||||||
|
dependencies:
|
||||||
|
ansi-regex: "npm:^5.0.1"
|
||||||
|
ansi-styles: "npm:^5.0.0"
|
||||||
|
react-is: "npm:^17.0.1"
|
||||||
|
checksum: 10c0/0cbda1031aa30c659e10921fa94e0dd3f903ecbbbe7184a729ad66f2b6e7f17891e8c7d7654c458fa4ccb1a411ffb695b4f17bbcd3fe075fabe181027c4040ed
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"proc-log@npm:^7.0.0":
|
"proc-log@npm:^7.0.0":
|
||||||
version: 7.0.0
|
version: 7.0.0
|
||||||
resolution: "proc-log@npm:7.0.0"
|
resolution: "proc-log@npm:7.0.0"
|
||||||
@@ -3768,6 +3849,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"react-is@npm:^17.0.1":
|
||||||
|
version: 17.0.2
|
||||||
|
resolution: "react-is@npm:17.0.2"
|
||||||
|
checksum: 10c0/2bdb6b93fbb1820b024b496042cce405c57e2f85e777c9aabd55f9b26d145408f9f74f5934676ffdc46f3dcff656d78413a6e43968e7b3f92eea35b3052e9053
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"react-refresh@npm:^0.17.0":
|
"react-refresh@npm:^0.17.0":
|
||||||
version: 0.17.0
|
version: 0.17.0
|
||||||
resolution: "react-refresh@npm:0.17.0"
|
resolution: "react-refresh@npm:0.17.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user