[#1] Bare Skeleton with redux store, oidc and router-dom

This commit is contained in:
Camille Moussu
2025-06-25 17:42:54 +02:00
parent 57f9ba8427
commit cceb83519f
32 changed files with 21256 additions and 1 deletions
+8
View File
@@ -0,0 +1,8 @@
REACT_APP_SSO_BASE_URL="https://example.com"
REACT_APP_SSO_CLIENT_ID="example"
REACT_APP_SSO_CLIENT_SECRET=""
REACT_APP_SSO_SCOPE="openid name email"
REACT_APP_SSO_REDIRECT_URI="https://example.com/callback"
REACT_APP_SSO_RESPONSE_TYPE="code"
REACT_APP_SSO_CODE_CHALLENGE_METHOD="S256"
REACT_APP_SSO_POST_LOGOUT_REDIRECT="http://example.com?logout=1"
+23
View File
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# secrets
.DS_Store
.env
.env.development
.env.test
.env.production
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+9
View File
@@ -0,0 +1,9 @@
{
"semi": true,
"singleQuote": false,
"trailingComma": "es5",
"printWidth": 80,
"tabWidth": 2,
"arrowParens": "always",
"endOfLine": "lf"
}
+34 -1
View File
@@ -17,7 +17,40 @@ It is meant as a drop in replacement of [esn-frontend-calendar](https://github.c
### Compile it and run it
More details to be supplied when available.
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
### Run it with docker
+13
View File
@@ -0,0 +1,13 @@
import React from "react";
import { screen } from "@testing-library/react";
import App from "../src/App";
import { JSX } from "react/jsx-runtime";
test("renders learn react link", () => {
renderWithProviders(<App />);
const linkElement = screen.getByText("Twake");
expect(linkElement).toBeInTheDocument();
});
function renderWithProviders(arg0: JSX.Element) {
throw new Error("Function not implemented.");
}
+13
View File
@@ -0,0 +1,13 @@
import React from "react";
import { screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import { Menubar } from "../../src/components/Menubar/Menubar";
import { renderWithProviders } from "../utils/Renderwithproviders";
describe("Calendar App Component Display Tests", () => {
test("renders the Navbar component", () => {
renderWithProviders(<Menubar />);
const navbarElement = screen.getByText("Twake");
expect(navbarElement).toBeInTheDocument();
});
});
+60
View File
@@ -0,0 +1,60 @@
import type { RenderOptions } from "@testing-library/react";
import {
act,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
import React, { PropsWithChildren } from "react";
import { useTranslation } from "react-i18next";
import { Provider } from "react-redux";
import "../i18n";
import { MemoryRouter } from "react-router-dom";
import type { AppStore, RootState } from "../../src/app/store";
import { setupStore } from "../../src/app/store";
import { t } from "i18next";
import { userData } from "../../src/features/User/userDataTypes";
interface ExtendedRenderOptions extends Omit<RenderOptions, "queries"> {
preloadedState?: Partial<RootState>;
store?: AppStore;
}
export function renderWithProviders(
ui: React.ReactElement,
extendedRenderOptions: ExtendedRenderOptions = {}
) {
const {
preloadedState = {
user: { userData: null as unknown as userData },
router: {
location: {
pathname: "",
search: "",
hash: "",
state: null,
key: "o01z0jry",
},
},
},
store = setupStore(preloadedState),
...renderOptions
} = extendedRenderOptions;
const Wrapper = ({ children }: PropsWithChildren) => {
useTranslation();
return (
<MemoryRouter initialEntries={["/manager.html"]}>
<Provider store={store}>{children}</Provider>
</MemoryRouter>
);
};
return {
store,
...render(ui, { wrapper: Wrapper, ...renderOptions }),
};
}
+20443
View File
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
{
"name": "front",
"version": "0.1.0",
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^2.8.2",
"@types/node": "^16.18.126",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"i18next": "^23.7.9",
"openid-client": "^6.5.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-i18next": "^13.5.0",
"react-redux": "^9.2.0",
"react-router-dom": "^6.23.1",
"redux-first-history": "^5.2.0",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "PORT=5000 react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"i18next-resources-for-ts": "1.4.0",
"jest": "^27.5.1",
"jest-preview": "^0.3.1",
"react-scripts": "^5.0.1",
"typescript": "^4.9.5"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

+45
View File
@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link href="https://fonts.googleapis.com/css2?family=Cal+Sans&display=swap" rel="stylesheet">
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

+25
View File
@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
+3
View File
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
+38
View File
@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
+30
View File
@@ -0,0 +1,30 @@
import { Suspense } from "react";
import { Route, Routes } from "react-router-dom";
import { HistoryRouter as Router } from "redux-first-history/rr6";
import { Menubar } from "./components/Menubar/Menubar";
import { CallbackResume } from "./features/User/LoginCallback";
import { history } from "./app/store";
import './App.css'
function App() {
return (
<Suspense fallback="loading">
<Router history={history}>
<Routes>
<Route
path="/"
element={
<div className="App">
<header className="App-header">
<Menubar />
</header>
</div>
}
/>
<Route path="/callback" element={<CallbackResume />} />
</Routes>
</Router>
</Suspense>
);
}
export default App;
+5
View File
@@ -0,0 +1,5 @@
import { useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
+30
View File
@@ -0,0 +1,30 @@
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import userReducer from "../features/User/userSlice";
import { createReduxHistoryContext } from "redux-first-history";
import { createBrowserHistory } from "history";
const { createReduxHistory, routerMiddleware, routerReducer } =
createReduxHistoryContext({ history: createBrowserHistory() });
const rootReducer = combineReducers({
router: routerReducer,
user: userReducer,
});
export const setupStore = (preloadedState?: Partial<RootState>) => {
return configureStore({
reducer: rootReducer,
preloadedState,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(routerMiddleware),
});
};
export const store = setupStore();
export const history = createReduxHistory(store);
export type RootState = ReturnType<typeof rootReducer>;
export type AppStore = ReturnType<typeof setupStore>;
export type AppDispatch = AppStore["dispatch"];
+89
View File
@@ -0,0 +1,89 @@
/* Basic reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.twake {
font-family: "Cal Sans";
font-weight: 400;
font-size: 28.07px;
line-height: 100%;
letter-spacing: 0%;
text-align: center;
vertical-align: middle;
padding: 0.2rem;
}
.calendar {
font-family: "Cal Sans";
padding: 0.2rem;
font-weight: 400;
font-size: 28.07px;
line-height: 100%;
letter-spacing: 0%;
text-align: center;
vertical-align: middle;
background: linear-gradient(180deg, #ffb73d, #f26c32);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
/* Optional for broader support */
background-clip: text;
color: transparent;
}
/* Menubar styles */
.menubar {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #1a1a1a62;
padding: 0.5rem;
color: white;
position: absolute;
width: 100%;
top: 0;
z-index: 1000;
}
.menubar-item {
display: flex;
align-items: center;
}
.logo {
padding: 0.5rem 1rem;
font-size: 1.5rem;
}
.nav-month {
padding-right: 2px;
padding-left: 2px;
gap: 38px;
}
.day-selector {
font-weight: 100;
font-size: small;
}
.search-bar {
width: 600;
height: 43.88999938964844;
top: 18px;
left: 887px;
}
.menu-tools {
width: 383.20001220703125;
height: 49;
top: 16px;
left: 1511px;
gap: 24px;
}
/* Responsive for mobile */
@media (max-width: 768px) {
}
+54
View File
@@ -0,0 +1,54 @@
import React from "react";
import { Auth } from "../../features/User/oidcAuth";
import logo from "../../static/images/calendar.svg";
import "./Menubar.css";
import { useAppSelector } from "../../app/hooks";
export function Menubar() {
return (
<div className="menubar">
<div className="menubar-item tc-home">
<img className="logo" src={logo} />
<p>
<span className="twake">Twake</span>
<span className="calendar">Calendar</span>
</p>
</div>
<div className="menubar-item nav-month">
<p>Current Month</p>
<p className="day-selector"> droite today gauche</p>
</div>
<div className="menubar-item search-bar">
<p>big search bar</p>
</div>
<div className="menubar-item menu-tools">
<HandleLogin />
</div>
</div>
);
}
function HandleLogin() {
const userData = useAppSelector((state) => state.user.userData);
if (!userData) {
return (
<button
onClick={async () => {
const loginurl = await Auth();
sessionStorage.setItem(
"redirectState",
JSON.stringify({
code_verifier: loginurl.code_verifier,
state: loginurl.state,
})
);
window.location.assign(loginurl.redirectTo);
}}
>
login
</button>
);
} else {
return <p>{userData.sub}</p>;
}
}
+65
View File
@@ -0,0 +1,65 @@
import React, { useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Callback } from "./oidcAuth";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { push } from "redux-first-history";
import { setUserData } from "./userSlice";
export function CallbackResume() {
const dispatch = useAppDispatch();
const hasRun = useRef(false);
const saved = sessionStorage.getItem("redirectState")
? JSON.parse(sessionStorage.getItem("redirectState")!)
: null;
const [tokens, setTokens] = useState<any>();
const [userInfo, setUserInfo] = useState<any>();
const location = useAppSelector((state) => state.router.location);
useEffect(() => {
if (hasRun.current) {
return;
}
hasRun.current = true;
const runCallback = async () => {
try {
const data = await Callback(saved?.code_verifier, saved?.state);
console.log("data:", data);
setTokens(data?.tokenSet);
setUserInfo(data?.userinfo);
dispatch(setUserData(data?.userinfo));
sessionStorage.removeItem("redirectState");
// Redirect to main page after successful callback
dispatch(push("/"));
} catch (e) {
console.error("OIDC callback error:", e);
}
};
if (saved?.code_verifier) {
runCallback();
} else {
console.warn("Missing redirectState");
sessionStorage.removeItem("redirectState");
dispatch(push("/"));
}
}, [dispatch, saved]);
return (
<div>
<p>Processing OIDC callback...</p>
{/* Optionally show loading or debug info */}
{tokens && (
<ul>
<li>
ID_Token: <pre>{JSON.stringify(tokens?.id_token)}</pre>
</li>
<li>
Access_Token: <pre>{JSON.stringify(tokens?.access_token)}</pre>
</li>
<li>
User info: <pre>{JSON.stringify(userInfo, null, 2)}</pre>
</li>
</ul>
)}
</div>
);
}
+87
View File
@@ -0,0 +1,87 @@
import * as client from "openid-client";
export const clientConfig = {
url: process.env.REACT_APP_SSO_BASE_URL || "",
client_id: process.env.REACT_APP_SSO_CLIENT_ID || "",
client_secret: process.env.REACT_APP_SSO_CLIENT_SECRET || "",
scope: process.env.REACT_APP_SSO_SCOPE || "",
redirect_uri: process.env.REACT_APP_SSO_REDIRECT_URI || "",
response_type: process.env.REACT_APP_SSO_RESPONSE_TYPE || "",
code_challenge_method: process.env.REACT_APP_SSO_CODE_CHALLENGE_METHOD || "",
post_logout_redirect_uri:
process.env.REACT_APP_SSO_POST_LOGOUT_REDIRECT || "",
};
export async function getClientConfig() {
console.log(process.env.REACT_APP_SSO_BASE_URL)
return await client.discovery(
new URL(clientConfig.url),
clientConfig.client_id,
clientConfig.client_secret
);
}
export async function Auth() {
console.log("bap");
let code_verifier = client.randomPKCECodeVerifier();
let code_challenge = await client.calculatePKCECodeChallenge(code_verifier);
const openIdClientConfig = await getClientConfig();
let parameters: Record<string, string> = {
redirect_uri: clientConfig.redirect_uri,
scope: clientConfig.scope!,
code_challenge,
code_challenge_method: clientConfig.code_challenge_method,
};
let state!: string;
if (!openIdClientConfig.serverMetadata().supportsPKCE()) {
state = client.randomState();
parameters.state = state;
}
let redirectTo = client.buildAuthorizationUrl(openIdClientConfig, parameters);
console.log(redirectTo);
return { redirectTo, code_verifier, state };
}
export async function Logout() {
const openIdClientConfig = await getClientConfig();
const endSessionUrl = client.buildEndSessionUrl(openIdClientConfig, {
post_logout_redirect_uri: clientConfig.post_logout_redirect_uri,
});
return endSessionUrl;
}
export async function Callback(code_verifier: string, state: any) {
try {
const openIdClientConfig = await getClientConfig();
const currentUrl = new URL(window.location.href);
console.log("Callback URL:", currentUrl.toString());
console.log("Code verifier:", code_verifier);
console.log("State:", state);
const tokenSet = await client.authorizationCodeGrant(
openIdClientConfig,
currentUrl,
{
pkceCodeVerifier: code_verifier,
expectedState: state,
}
);
const { access_token } = tokenSet;
const claims = tokenSet.claims()!;
const { sub } = claims;
const userinfo = await client.fetchUserInfo(
openIdClientConfig,
access_token,
sub
);
return { tokenSet, userinfo };
} catch (e) {
console.error("Token grant error:", e);
}
}
+5
View File
@@ -0,0 +1,5 @@
export interface userData {
email: string;
sid: string;
sub: string;
}
+19
View File
@@ -0,0 +1,19 @@
import { createSlice } from "@reduxjs/toolkit";
import { userData } from "./userDataTypes";
export const userSlice = createSlice({
name: "user",
initialState: {
userData: null as unknown as userData,
},
reducers: {
setUserData: (state, action) => {
state.userData = action.payload;
},
},
});
// Action creators are generated for each case reducer function
export const { setUserData } = userSlice.actions;
export default userSlice.reducer;
+13
View File
@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
+19
View File
@@ -0,0 +1,19 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import App from "./App";
import { store } from "./app/store";
import reportWebVitals from "./reportWebVitals";
const container = document.getElementById("root")!;
const root = createRoot(container);
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
reportWebVitals();
+1
View File
@@ -0,0 +1 @@
/// <reference types="react-scripts" />
+15
View File
@@ -0,0 +1,15 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
+5
View File
@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
+23
View File
@@ -0,0 +1,23 @@
<svg width="42" height="43" viewBox="0 0 42 43" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="0.576172" width="42" height="42" rx="14.2009" fill="white"/>
<rect y="0.576172" width="42" height="42" rx="14.2009" fill="url(#paint0_linear_1954_136)"/>
<g filter="url(#filter0_d_1954_136)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.716 9.18628C14.489 9.06472 14.2204 9 13.8605 9H13.7466C13.3867 9 13.1181 9.06472 12.8911 9.18628C12.6641 9.30775 12.486 9.48614 12.3646 9.7134C12.2432 9.94066 12.1786 10.2096 12.1786 10.5699L12.1797 11.0974C11.6477 11.2206 11.2497 11.3905 10.8502 11.6044C9.94231 12.0906 9.22973 12.804 8.74415 13.713C8.25859 14.622 8 15.5237 8 18.0102V26.021C8 28.5076 8.25859 29.4092 8.74415 30.3183C9.22973 31.2273 9.94231 31.9407 10.8502 32.4268C11.7582 32.913 12.6587 33.1719 15.1422 33.1719H26.8577C29.3412 33.1719 30.2418 32.913 31.1497 32.4268C32.0577 31.9407 32.7702 31.2273 33.2558 30.3183C33.7414 29.4092 34 28.5076 34 26.021V18.0102C34 15.5237 33.7414 14.622 33.2558 13.713C32.7702 12.804 32.0577 12.0906 31.1497 11.6044C30.7507 11.3908 30.3532 11.221 29.822 11.0978L29.8214 10.5699C29.8214 10.2096 29.7568 9.94066 29.6354 9.7134C29.5139 9.48614 29.3358 9.30775 29.1089 9.18628C28.8819 9.06472 28.6132 9 28.2534 9H28.1395C27.7796 9 27.511 9.06472 27.284 9.18628C27.057 9.30775 26.8788 9.48614 26.7574 9.7134C26.6361 9.94066 26.5714 10.2096 26.5714 10.5699V10.8593L15.4286 10.8575V10.5699C15.4286 10.2096 15.3639 9.94066 15.2425 9.7134C15.1212 9.48614 14.943 9.30775 14.716 9.18628ZM12.7975 17.832H29.2025C29.7406 17.832 29.9357 17.8881 30.1324 17.9934C30.3291 18.0988 30.4835 18.2533 30.5887 18.4504C30.694 18.6473 30.7499 18.8427 30.7499 19.3814V27.775L30.7375 28.008C30.6214 29.0794 29.7149 29.9133 28.6095 29.9133L13.9827 29.8812L13.6874 29.8768C12.9553 29.8559 12.6487 29.7587 12.3399 29.5927C11.9926 29.4061 11.7201 29.1328 11.5345 28.7845C11.3488 28.4363 11.25 28.0912 11.25 27.1392V19.3814L11.2534 19.1705C11.267 18.7868 11.3211 18.6191 11.4113 18.4504C11.5164 18.2533 11.6708 18.0988 11.8676 17.9934C12.0643 17.8881 12.2594 17.832 12.7975 17.832ZM26.643 20.1562H25.5712C24.9503 20.1562 24.7253 20.221 24.4983 20.3425C24.2713 20.4641 24.0931 20.6424 23.9717 20.8696C23.8504 21.0969 23.7857 21.3223 23.7857 21.944V23.0169C23.7857 23.6386 23.8504 23.864 23.9717 24.0913C24.0931 24.3186 24.2713 24.4969 24.4983 24.6184C24.7253 24.74 24.9503 24.8047 25.5712 24.8047H26.643C27.2639 24.8047 27.489 24.74 27.716 24.6184C27.943 24.4969 28.1211 24.3186 28.2425 24.0913C28.3639 23.864 28.4285 23.6386 28.4285 23.0169V21.944C28.4285 21.3223 28.3639 21.0969 28.2425 20.8696C28.1211 20.6424 27.943 20.4641 27.716 20.3425C27.489 20.221 27.2639 20.1562 26.643 20.1562Z" fill="white"/>
</g>
<defs>
<filter id="filter0_d_1954_136" x="5.80729" y="7.24583" width="30.3854" height="28.5573" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.438541"/>
<feGaussianBlur stdDeviation="1.09635"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1954_136"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1954_136" result="shape"/>
</filter>
<linearGradient id="paint0_linear_1954_136" x1="3.96217" y1="42.5538" x2="50.3303" y2="-5.31354" gradientUnits="userSpaceOnUse">
<stop offset="0.286667" stop-color="#FD813C"/>
<stop offset="1" stop-color="#FFF500"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
, "__test__/App.test.tsx" ]
}