Bläddra i källkod

Strapi SEO

strapiSEO
ntasicc 3 år sedan
förälder
incheckning
bc65d69758

+ 1
- 1
frontend/.env Visa fil

@@ -5,7 +5,7 @@ REACT_APP_SERVICE_ID = service_petbzsz
REACT_APP_JOB_TEMPLATE_ID = template_bfuv1sb
REACT_APP_CLIENT_TEMPLATE_ID = template_bd6fjli
REACT_APP_USER_ID = 27spvSZ2Lsf2j8RKw
REACT_APP_API_URL = "https://websitediligentapi.azurewebsites.net"
REACT_APP_API_URL = "https://diligentwebsiteapistart.azurewebsites.net"
//http://localhost:1337
MAILCHIMP_FORM_URL = http://eepurl.com/iaRrv1
GOOGLE_TRACKING_ID = "G-PTZC3WLTZ1"

+ 43
- 2
frontend/package-lock.json Visa fil

@@ -1,12 +1,12 @@
{
"name": "frontend",
"version": "1.1.9",
"version": "1.1.12",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "frontend",
"version": "1.1.9",
"version": "1.1.12",
"dependencies": {
"@faceless-ui/slider": "^1.1.14",
"@faceless-ui/window-info": "^2.1.1",
@@ -33,6 +33,7 @@
"react-ga": "^3.3.1",
"react-ga4": "^1.4.1",
"react-google-recaptcha": "^2.1.0",
"react-helmet-async": "^1.3.0",
"react-mailchimp-subscribe": "^2.1.3",
"react-markdown": "^8.0.0",
"react-router-dom": "^6.2.1",
@@ -14645,6 +14646,27 @@
"react": ">=16.4.1"
}
},
"node_modules/react-helmet-async": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-1.3.0.tgz",
"integrity": "sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"invariant": "^2.2.4",
"prop-types": "^15.7.2",
"react-fast-compare": "^3.2.0",
"shallowequal": "^1.1.0"
},
"peerDependencies": {
"react": "^16.6.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-helmet-async/node_modules/react-fast-compare": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -28415,6 +28437,25 @@
"react-async-script": "^1.1.1"
}
},
"react-helmet-async": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-1.3.0.tgz",
"integrity": "sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==",
"requires": {
"@babel/runtime": "^7.12.5",
"invariant": "^2.2.4",
"prop-types": "^15.7.2",
"react-fast-compare": "^3.2.0",
"shallowequal": "^1.1.0"
},
"dependencies": {
"react-fast-compare": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
}
}
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

+ 1
- 0
frontend/package.json Visa fil

@@ -28,6 +28,7 @@
"react-ga": "^3.3.1",
"react-ga4": "^1.4.1",
"react-google-recaptcha": "^2.1.0",
"react-helmet-async": "^1.3.0",
"react-mailchimp-subscribe": "^2.1.3",
"react-markdown": "^8.0.0",
"react-router-dom": "^6.2.1",

+ 1
- 1
frontend/public/index.html Visa fil

@@ -5,7 +5,7 @@
<link rel="icon" href="%PUBLIC_URL%/favicon_diligent.png" />
<meta name="viewport" content="width=device-width, height=device-height" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Diligent Software's Website" />
<meta name="description" content="Diligent Software's Website" data-rh="true" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a

+ 54
- 46
frontend/src/components/shared/JobForm.jsx Visa fil

@@ -25,6 +25,7 @@ export default function JobForm(props) {
const [otherInputState, setOtherInputState] = useState(true);
const [selectedPosition, setSelectedPosition] = useState('');
const [errorMsg, setErrorMsg] = useState('');
const [errorMsgPosition, setErrorMsgPosition] = useState('');
const fileInput = useRef();

function changeFormHandler(event) {
@@ -70,6 +71,7 @@ export default function JobForm(props) {
.trim()
.min(2, 'Cover Letter too short')
.required('Cover Letter is Required'),
other: Yup.string(),
});

return (
@@ -111,49 +113,55 @@ export default function JobForm(props) {
};
});
};
// if (fileInput.current.value === '') {
// }
if (jobForm.file === '') {
setErrorMsg('CV is Required');
} else {
setErrorMsg('');
if (jobForm.file.size >= 2000000) {
setErrorMsg('File too large!');
if (
selectedPosition === '' ||
(otherInputState === false && values.other === '')
)
setErrorMsgPosition('Position is Required');
else {
setErrorMsgPosition('');
if (jobForm.file === '') {
setErrorMsg('CV is Required');
} else {
const file = {
filename: 'CV.pdf',
data: jobForm.file,
};

const token = captchaRef.current.getValue();
captchaRef.current.reset();
if (token.length === 0) {
setSucMsg(true);
setMsgText('Please fill reCAPTCHA and try again. Thank you!');
setErrorMsg('');
if (jobForm.file.size >= 2000000) {
setErrorMsg('File too large!');
} else {
await axios
.post(`${process.env.REACT_APP_CAPTCHA_API}/verify-token`, {
token,
})
.then(res => {
setSucMsg(true);
if (res.data.data.success) {
setMsgText('Submission Succesful! Thank you!');
const file = {
filename: 'CV.pdf',
data: jobForm.file,
};

props.mg.messages.create('dilig.net', {
from: `${values.firstName} ${values.lastName} <${values.email}>`,
to: ['hr@dilig.net'],
subject: 'Applying for a position',
text: `Position:${values.position} ${values.other}, Cover letter: ${values.coverLetter}, Link: ${values.link} `,
html: `<p>Position: ${selectedPosition} ${values.other}</p><p>Cover letter: ${values.coverLetter}</p><p>Link (optional): ${values.link}</p>`,
attachment: file,
});
} else
setMsgText('Please fill reCAPTCHA and try again. Thank you!');
})
.catch(error => {
console.log(error);
});
const token = captchaRef.current.getValue();
captchaRef.current.reset();
if (token.length === 0) {
setSucMsg(true);
setMsgText('Please fill reCAPTCHA and try again. Thank you!');
} else {
await axios
.post(`${process.env.REACT_APP_CAPTCHA_API}/verify-token`, {
token,
})
.then(res => {
setSucMsg(true);
if (res.data.data.success) {
setMsgText('Submission Succesful! Thank you!');

props.mg.messages.create('dilig.net', {
from: `${values.firstName} ${values.lastName} <${values.email}>`,
to: ['nikola.tasic@dilig.net'],
subject: 'Applying for a position',
text: `Email: ${values.email}, Position:${values.position} ${values.other}, Cover letter: ${values.coverLetter}, Link: ${values.link} `,
html: `<p>Email: ${values.email}</p><p>Position: ${selectedPosition} ${values.other}</p><p>Cover letter: ${values.coverLetter}</p><p>Link (optional): ${values.link}</p>`,
attachment: file,
});
} else
setMsgText('Please fill reCAPTCHA and try again. Thank you!');
})
.catch(error => {
console.log(error);
});
}
}
}
}
@@ -219,14 +227,14 @@ export default function JobForm(props) {
onChange={props.handleChange}
className="mt-1 disabled:bg-gray-100 disabled:border-gray-300 dark:disabled:bg-gray-400 dark:disabled:border-gray-600 focus:ring-dg-primary-600 focus:border-dg-primary-900 dark:bg-dg-primary-1500 dark:text-white block w-full shadow-sm sm:text-sm border-dg-primary-600 rounded-md transition duration-200"
/>
</div>
{errorMsgPosition != '' ? (
<div className="h-4">
<ErrorMessage
name="other"
component="div"
className="text-sm text-right text-red-600"
/>
<div className="text-sm text-right text-red-600">
{errorMsgPosition}
</div>
</div>
</div>
) : null}
<div className="py-1">
<label
htmlFor="first-name"

+ 33
- 0
frontend/src/components/shared/ReactHelmet.jsx Visa fil

@@ -0,0 +1,33 @@
import { Helmet } from 'react-helmet-async';
import PropTypes from 'prop-types';
import '../../styles/cards.css';

const api_url = process.env.REACT_APP_API_URL;

const ReactHelmet = ({ seo }) => {
console.log(seo);
return (
<Helmet>
<title>{seo.metaTitle}</title>
<meta name="description" content={seo.metaDescription} />
<link rel="canonical" href={seo.canonicalURL} />
<meta name="keywords" content={seo.keywords} />
<meta name="viewport" content={seo.metaViewport} />
<meta name="robots" content={seo.metaRobots} />
<meta property="og:title" content={seo.metaSocial[0].title} />
<meta
property="og:image"
content={`${api_url}${seo.metaSocial[0].image.data.attributes.url}`}
/>
<meta property="og:description" content={seo.metaSocial[0].description} />
<meta property="twitter:title" content={seo.metaSocial[1].title} />
<meta
property="twitter:image"
content={`${api_url}${seo.metaSocial[1].image.data.attributes.url}`}
/>
<meta property="twitter:description" content={seo.metaSocial[1].description} />
</Helmet>
);
};

export default ReactHelmet;

+ 4
- 1
frontend/src/index.js Visa fil

@@ -5,6 +5,7 @@ import App from './App';
import reportWebVitals from './reportWebVitals';
import { CookiesProvider } from 'react-cookie';
import { BrowserRouter } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async';

if (module.hot) module.hot.accept();

@@ -12,7 +13,9 @@ ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<CookiesProvider>
<App />
<HelmetProvider>
<App />
</HelmetProvider>
</CookiesProvider>
</BrowserRouter>
</React.StrictMode>,

+ 33
- 1
frontend/src/pages/About.jsx Visa fil

@@ -24,6 +24,7 @@ import TimelineCardsWrapper2 from '../components/TimelineWrapper2';
import TimelineLogo from '../components/TimelineLogo';
import PageLayout from '../layout/PageLayout';
import useAnalytics from '../hooks/useAnalytics';
import ReactHelmet from '../components/shared/ReactHelmet';

const _data = {
heading: {
@@ -186,15 +187,46 @@ const _data = {
},
};

const api_url = process.env.REACT_APP_API_URL;

export default function About() {
useEffect(() => {
const [cnt, setCnt] = useState('');
const [isLoaded, setIsLoaded] = useState('');

useEffect(async () => {
document.title = 'About Us';
var vid = document.getElementById('animation');
vid.playbackRate = 2;
await axios
.get(
`${api_url}/api/aboutpage?populate[0]=SEO&populate[1]=SEO.metaSocial&populate[2]=SEO.metaImage&populate[3]=SEO.metaSocial.image`,
)
.then(res => {
setCnt(res.data.data.attributes);
setIsLoaded(true);
})
.catch(err => {
console.log(err);
setIsLoaded(false);
});
}, []);

useAnalytics('About Us');

if (!isLoaded) {
return (
<div className="z-50 w-full h-screen bg-white dark:bg-dg-primary-1700 dark:text-white flex items-center justify-center text-3xl font-semibold">
<video id="animation" width="540" height="540" autoPlay muted loop>
<source src={Animation_Diligent} type="video/webm" />
Loading...
</video>
</div>
);
}

return (
<PageLayout>
{cnt.SEO && <ReactHelmet seo={cnt.SEO} />}
<div className="bg-white dark:bg-dg-primary-1700 w-full pt-32">
{/* Heading Section */}
<section

+ 16
- 2
frontend/src/pages/Home.jsx Visa fil

@@ -30,7 +30,22 @@ import PortfolioSection from '../components/PortfolioSection';
import PageLayout from '../layout/PageLayout';
import MapDilig from '../components/Map';
import useAnalytics from '../hooks/useAnalytics';
import ReactHelmet from '../components/shared/ReactHelmet';

const api_url = process.env.REACT_APP_API_URL;
// const fieldArray = [
// 'landing',
// 'why',
// 'why.heading',
// 'why.card_left.icon',
// 'why.card_mid.icon',
// 'why.card_right.icon',
// 'landing.heading',
// 'SEO',
// 'SEO.metaSocial',
// 'SEO.metaImage',
// 'SEO.metaSocial.image',
// ];

export default function Home({ forwardedRef }) {
const [cnt, setCnt] = useState('');
@@ -53,8 +68,6 @@ export default function Home({ forwardedRef }) {
document.title = 'Diligent Software';
}, []);

// useAnalytics();

useEffect(() => {
var vid = document.getElementById('animation');
vid.playbackRate = 2;
@@ -86,6 +99,7 @@ export default function Home({ forwardedRef }) {
} else {
return (
<PageLayout>
{cnt.SEO && <ReactHelmet seo={cnt.SEO} />}
<div className="bg-white dark:bg-dg-primary-1700 w-full pt-32 overflow-hidden">
{/* <FormSwitch /> */}


+ 33
- 1
frontend/src/pages/Portfolio.jsx Visa fil

@@ -14,6 +14,7 @@ import PageLayout from '../layout/PageLayout';

import StrataThumb from './../assets/images/CaseStudy/StrataThumb.jpg';
import useAnalytics from '../hooks/useAnalytics';
import ReactHelmet from '../components/shared/ReactHelmet';

const _data = {
heading: {
@@ -73,15 +74,46 @@ const _data = {
],
};

const api_url = process.env.REACT_APP_API_URL;

export default function Portfolio() {
useEffect(() => {
const [cnt, setCnt] = useState('');
const [isLoaded, setIsLoaded] = useState('');

useEffect(async () => {
document.title = 'Case Studies';
var vid = document.getElementById('animation');
vid.playbackRate = 2;
await axios
.get(
`${api_url}/api/portfoliopage?populate[0]=SEO&populate[1]=SEO.metaSocial&populate[2]=SEO.metaImage&populate[3]=SEO.metaSocial.image`,
)
.then(res => {
setCnt(res.data.data.attributes);
setIsLoaded(true);
})
.catch(err => {
console.log(err);
setIsLoaded(false);
});
}, []);

useAnalytics('Case Studies');

if (!isLoaded) {
return (
<div className="z-50 w-full h-screen bg-white dark:bg-dg-primary-1700 dark:text-white flex items-center justify-center text-3xl font-semibold">
<video id="animation" width="540" height="540" autoPlay muted loop>
<source src={Animation_Diligent} type="video/webm" />
Loading...
</video>
</div>
);
}

return (
<PageLayout>
{cnt.SEO && <ReactHelmet seo={cnt.SEO} />}
<div className="flex flex-col gap-90p pt-32">
<Wrapper>
<h1 className="hidden">Our Work - Case Studies</h1>

+ 35
- 2
frontend/src/pages/WorkWithUs.jsx Visa fil

@@ -1,4 +1,4 @@
import React, { Children, useEffect } from 'react';
import React, { Children, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import CustomLink from '../components/root/CustomLink';
import TertiaryButton from '../components/root/TertiaryButton';
@@ -15,6 +15,9 @@ import { ReactComponent as FintechIcon } from './../assets/icons/workwithus/empt
import { ReactComponent as HospitalIcon } from './../assets/icons/workwithus/hospital.svg';
import { ReactComponent as SchoolIcon } from './../assets/icons/workwithus/teacher.svg';
import useAnalytics from '../hooks/useAnalytics';
import axios from 'axios';
import Animation_Diligent from '../assets/animation_diligent.webm';
import ReactHelmet from '../components/shared/ReactHelmet';

const _data = {
downloadIcon: '',
@@ -88,14 +91,44 @@ const HelpParagraph = ({ title, paragraph }) => {
);
};

const api_url = process.env.REACT_APP_API_URL;

const WorkWithUs = () => {
useAnalytics('Work With Us');
useEffect(() => {
const [cnt, setCnt] = useState('');
const [isLoaded, setIsLoaded] = useState('');

useEffect(async () => {
document.title = 'Work With Us';
var vid = document.getElementById('animation');
vid.playbackRate = 2;
await axios
.get(
`${api_url}/api/aboutpage?populate[0]=SEO&populate[1]=SEO.metaSocial&populate[2]=SEO.metaImage&populate[3]=SEO.metaSocial.image`,
)
.then(res => {
setCnt(res.data.data.attributes);
setIsLoaded(true);
})
.catch(err => {
console.log(err);
setIsLoaded(false);
});
}, []);

if (!isLoaded) {
return (
<div className="z-50 w-full h-screen bg-white dark:bg-dg-primary-1700 dark:text-white flex items-center justify-center text-3xl font-semibold">
<video id="animation" width="540" height="540" autoPlay muted loop>
<source src={Animation_Diligent} type="video/webm" />
Loading...
</video>
</div>
);
}
return (
<div className="mt-90p">
{cnt.SEO && <ReactHelmet seo={cnt.SEO} />}
<Wrapper padding={' py-[48px]'}>
<PageTitle heading={'Diligent at a Glance'} subheading={'work with us'} />
</Wrapper>

+ 13
- 0
frontend/src/utils/strapiApiBuilder.js Visa fil

@@ -0,0 +1,13 @@
const api_url = process.env.REACT_APP_API_URL;

export const strapiApiBuilder = (page, stringArray) => {
const api = `${api_url}/api/${page}`;
let query = '';
stringArray.map((field, index) => {
if (index === 0) query += `?populate[${index}]=${field}`;
else {
query += `&populate[${index}]=${field}`;
}
});
return api + query;
};

+ 19
- 3
frontend/yarn.lock Visa fil

@@ -5276,7 +5276,7 @@
"resolved" "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz"
"version" "2.2.0"

"invariant@2.2.4":
"invariant@^2.2.4", "invariant@2.2.4":
"integrity" "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="
"resolved" "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz"
"version" "2.2.4"
@@ -7915,7 +7915,7 @@
"strip-ansi" "^6.0.1"
"text-table" "^0.2.0"

"react-dom@*", "react-dom@^16 || ^17 || ^18", "react-dom@^16.8 || ^17 || ^18", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom@^17.0.2", "react-dom@>= 16.8.0", "react-dom@>=16.8", "react-dom@>=16.8 || ^17.0.0 || ^18.0.0":
"react-dom@*", "react-dom@^16 || ^17 || ^18", "react-dom@^16.6.0 || ^17.0.0 || ^18.0.0", "react-dom@^16.8 || ^17 || ^18", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom@^17.0.2", "react-dom@>= 16.8.0", "react-dom@>=16.8", "react-dom@>=16.8 || ^17.0.0 || ^18.0.0":
"integrity" "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA=="
"resolved" "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz"
"version" "17.0.2"
@@ -7943,6 +7943,11 @@
"resolved" "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz"
"version" "2.0.4"

"react-fast-compare@^3.2.0":
"integrity" "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
"resolved" "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz"
"version" "3.2.0"

"react-ga@^3.3.1":
"integrity" "sha512-4Vc0W5EvXAXUN/wWyxvsAKDLLgtJ3oLmhYYssx+YzphJpejtOst6cbIHCIyF50Fdxuf5DDKqRYny24yJ2y7GFQ=="
"resolved" "https://registry.npmjs.org/react-ga/-/react-ga-3.3.1.tgz"
@@ -7961,6 +7966,17 @@
"prop-types" "^15.5.0"
"react-async-script" "^1.1.1"

"react-helmet-async@^1.3.0":
"integrity" "sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg=="
"resolved" "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-1.3.0.tgz"
"version" "1.3.0"
dependencies:
"@babel/runtime" "^7.12.5"
"invariant" "^2.2.4"
"prop-types" "^15.7.2"
"react-fast-compare" "^3.2.0"
"shallowequal" "^1.1.0"

"react-is@^16.13.1", "react-is@^16.7.0", "react-is@>= 16.8.0":
"integrity" "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
"resolved" "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
@@ -8080,7 +8096,7 @@
optionalDependencies:
"fsevents" "^2.3.2"

"react@*", "react@^15.6.2 || ^16.0 || ^17 || ^18", "react@^16 || ^17 || ^18", "react@^16.8 || ^17 || ^18", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^17.0.2", "react@>= 16", "react@>= 16.3.0", "react@>= 16.8", "react@>= 16.8.0", "react@>=15", "react@>=16", "react@>=16.4.1", "react@>=16.8", "react@>=16.8 || ^17.0.0 || ^18.0.0", "react@>=16.8.0", "react@17.0.2":
"react@*", "react@^15.6.2 || ^16.0 || ^17 || ^18", "react@^16 || ^17 || ^18", "react@^16.6.0 || ^17.0.0 || ^18.0.0", "react@^16.8 || ^17 || ^18", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^17.0.2", "react@>= 16", "react@>= 16.3.0", "react@>= 16.8", "react@>= 16.8.0", "react@>=15", "react@>=16", "react@>=16.4.1", "react@>=16.8", "react@>=16.8 || ^17.0.0 || ^18.0.0", "react@>=16.8.0", "react@17.0.2":
"integrity" "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA=="
"resolved" "https://registry.npmjs.org/react/-/react-17.0.2.tgz"
"version" "17.0.2"

Laddar…
Avbryt
Spara