Skip to content

Commit

Permalink
Merge branch 'master' into pre-authorized_code
Browse files Browse the repository at this point in the history
  • Loading branch information
jessevanmuijden committed Nov 28, 2024
2 parents a4dbd2b + 1a2ed1d commit c76fc76
Show file tree
Hide file tree
Showing 35 changed files with 732 additions and 283 deletions.
25 changes: 19 additions & 6 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, Suspense } from 'react';
import React, { useEffect, Suspense, useContext } from 'react';
import { Routes, Route, Outlet, useLocation } from 'react-router-dom';
// Import i18next and set up translations
import { I18nextProvider } from 'react-i18next';
Expand All @@ -14,6 +14,7 @@ import Snowfalling from './components/ChristmasAnimation/Snowfalling';
import Spinner from './components/Shared/Spinner';

import { withContainerContext } from './context/ContainerContext';
import StatusContext from './context/StatusContext';

import UpdateNotification from './components/Notifications/UpdateNotification';
import CredentialDetails from './pages/Home/CredentialDetails';
Expand Down Expand Up @@ -61,7 +62,15 @@ const reactLazyWithNonDefaultExports = (load, ...names) => {
return defaultExport;
};

const Layout = React.lazy(() => import('./components/Layout/Layout'));
const lazyWithDelay = (importFunction, delay = 1000) => {
return React.lazy(() =>
Promise.all([
importFunction(),
new Promise((resolve) => setTimeout(resolve, delay)),
]).then(([module]) => module)
);
};

const PrivateRoute = reactLazyWithNonDefaultExports(
() => import('./components/Auth/PrivateRoute'),
'NotificationPermissionWarning',
Expand All @@ -73,18 +82,22 @@ const CredentialHistory = React.lazy(() => import('./pages/Home/CredentialHistor
const History = React.lazy(() => import('./pages/History/History'));
const HistoryDetail = React.lazy(() => import('./pages/History/HistoryDetail'));
const Home = React.lazy(() => import('./pages/Home/Home'));
const Login = React.lazy(() => import('./pages/Login/Login'));
const LoginState = React.lazy(() => import('./pages/Login/LoginState'));
const NotFound = React.lazy(() => import('./pages/NotFound/NotFound'));
const SendCredentials = React.lazy(() => import('./pages/SendCredentials/SendCredentials'));
const Settings = React.lazy(() => import('./pages/Settings/Settings'));
const VerificationResult = React.lazy(() => import('./pages/VerificationResult/VerificationResult'));

const Layout = lazyWithDelay(() => import('./components/Layout/Layout'), 400);
const Login = lazyWithDelay(() => import('./pages/Login/Login'), 400);
const LoginState = lazyWithDelay(() => import('./pages/Login/LoginState'), 400);
const NotFound = lazyWithDelay(() => import('./pages/NotFound/NotFound'), 400);

function App() {
const { updateOnlineStatus } = useContext(StatusContext);
const location = useLocation();

useEffect(() => {
checkForUpdates();
updateOnlineStatus(false);
}, [location])

useEffect(() => {
Expand Down Expand Up @@ -135,7 +148,7 @@ function App() {
<Route element={
<PrivateRoute>
<Layout>
<Suspense fallback={<Spinner />}>
<Suspense fallback={<Spinner size='small' />}>
<PrivateRoute.NotificationPermissionWarning />
<FadeInContentTransition appear reanimateKey={location.pathname}>
<Outlet />
Expand Down
52 changes: 26 additions & 26 deletions src/components/Auth/LoginLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,43 @@ import { Trans, useTranslation } from 'react-i18next';
import * as config from '../../config';
import logo from '../../assets/images/logo.png';


export default function LoginLayout({ children, heading }: { children: React.ReactNode, heading: React.ReactNode }) {
const { t } = useTranslation();
return (
<section className="bg-gray-100 dark:bg-gray-900 h-full">
<div className='h-max min-h-dvh'>
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto min-h-dvh">
<a href="/" className="flex justify-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img className="w-40" src={logo} alt="logo" />
</a>
<section className="bg-gray-100 dark:bg-gray-900 min-h-dvh flex flex-col">
<div className="flex-grow flex flex-col items-center justify-center px-6 py-8">
<a href="/" className="flex justify-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img className="w-40" src={logo} alt="logo" />
</a>

<h1 className="text-3xl mb-7 font-bold leading-tight tracking-tight text-gray-900 text-center dark:text-white">
{heading}
</h1>
<h1 className="text-3xl mb-7 font-bold leading-tight tracking-tight text-gray-900 text-center dark:text-white">
{heading}
</h1>

<div className="relative w-full md:mt-0 sm:max-w-md xl:p-0">
{children}
</div>
<div className="relative w-full sm:max-w-md xl:p-0">
{children}
</div>
<div className='h-[5vh]'>
<p className='text-gray-700 dark:text-gray-400 text-center min-mt-10'>
<Trans
i18nKey="sidebar.poweredBy"
components={{
docLinkWalletGithub: <a
</div>

<footer className="py-4">
<p className="text-gray-700 dark:text-gray-400 text-center">
<Trans
i18nKey="sidebar.poweredBy"
components={{
docLinkWalletGithub: (
<a
href="https://github.com/wwWallet"
rel="noreferrer"
target='blank_'
target="_blank"
className="underline text-primary dark:text-primary-light"
aria-label={t('sidebar.poweredbyAriaLabel')}
/>
}}
/>
</p>
<p className='bg-gray-100 dark:bg-gray-900 text-gray-100 dark:text-gray-900'>{config.APP_VERSION}</p>
</div>
</div>
)
}}
/>
</p>
<p className="hidden">v{config.APP_VERSION}</p>
</footer>
</section>
);
}
10 changes: 7 additions & 3 deletions src/components/Credentials/CredentialImage.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useState, useEffect, useContext } from "react";
import StatusRibbon from '../../components/Credentials/StatusRibbon';
import ExpiredRibbon from './ExpiredRibbon';
import UsagesRibbon from "./UsagesRibbon";
import ContainerContext from '../../context/ContainerContext';

const CredentialImage = ({ credential, className, onClick, showRibbon = true }) => {
const CredentialImage = ({ credential, className, onClick, showRibbon = true, vcEntityInstances = null }) => {
const [parsedCredential, setParsedCredential] = useState(null);
const container = useContext(ContainerContext);

Expand All @@ -24,7 +25,10 @@ const CredentialImage = ({ credential, className, onClick, showRibbon = true })
<img src={parsedCredential.credentialImage.credentialImageURL} alt={"Credential"} className={className} onClick={onClick} />
)}
{parsedCredential && showRibbon &&
<StatusRibbon parsedCredential={parsedCredential} />
<ExpiredRibbon parsedCredential={parsedCredential} />
}
{vcEntityInstances && showRibbon &&
<UsagesRibbon vcEntityInstances={vcEntityInstances} />
}
</>
);
Expand Down
55 changes: 43 additions & 12 deletions src/components/Credentials/CredentialLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { useState, useEffect, useContext } from 'react';
import { Link, useParams, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { FaArrowLeft, FaArrowRight, FaExclamationTriangle } from "react-icons/fa";
import { PiCardsBold } from "react-icons/pi";

// Hooks
import useScreenType from '../../hooks/useScreenType';
Expand Down Expand Up @@ -31,13 +32,18 @@ const CredentialLayout = ({ children, title = null }) => {
const { t } = useTranslation();
const navigate = useNavigate();
const [isExpired, setIsExpired] = useState(null);

const [zeroSigCount, setZeroSigCount] = useState(null)
const [sigTotal, setSigTotal] = useState(null);

useEffect(() => {
const getData = async () => {
const response = await api.get('/storage/vc');
const vcEntity = response.data.vc_list
.filter((vcEntity) => vcEntity.credentialIdentifier === credentialId)[0];
const vcEntityInstances = response.data.vc_list
.filter((vcEntity) => vcEntity.credentialIdentifier === credentialId);
setZeroSigCount(vcEntityInstances.filter(instance => instance.sigCount === 0).length || 0);
setSigTotal(vcEntityInstances.length);
if (!vcEntity) {
throw new Error("Credential not found");
}
Expand All @@ -60,6 +66,22 @@ const CredentialLayout = ({ children, title = null }) => {
});
}, [vcEntity, container]);

const UsageStats = ({ zeroSigCount, sigTotal }) => {
if (zeroSigCount === null || sigTotal === null) return null;

const usageClass = zeroSigCount === 0 ? 'text-orange-600 dark:text-orange-500' : 'text-green-600 dark:text-green-500';

return (
<div className={`flex items-center text-gray-800 dark:text-white ${screenType === 'mobile' ? 'text-sm' : 'text-md'}`}>
<PiCardsBold size={18} className=' mr-1' />
<p className=' font-base'>
<span className={`${usageClass} font-semibold`}>{zeroSigCount}</span>
<span>/{sigTotal}</span> {t('pageCredentials.details.availableUsages')}
</p>
</div>
);
};

return (
<div className=" sm:px-6">
{screenType !== 'mobile' ? (
Expand All @@ -79,28 +101,37 @@ const CredentialLayout = ({ children, title = null }) => {
<button onClick={() => navigate(-1)} className="mr-2 mb-2" aria-label="Go back to the previous page">
<FaArrowLeft size={20} className="text-2xl text-primary dark:text-white" />
</button>
{title &&<H1 heading={title} hr={false} />}
{title && <H1 heading={title} hr={false} />}
</div>
)}
<PageDescription description={t('pageCredentials.details.description')} />

<div className="flex flex-wrap mt-0 lg:mt-5">
{/* Block 1: credential */}
<div className='flex flex-row w-full md:w-1/2'>
<div className='flex flex-row items-center gap-5 mt-2 mb-4 px-2'>
<div className='flex flex-row w-full lg:w-1/2'>
<div className={`flex flex-row items-center gap-5 mt-2 mb-4 px-2`}>
{vcEntity && (
// Open the modal when the credential is clicked
<button className="relative rounded-xl xm:rounded-lg w-4/5 xm:w-4/12 overflow-hidden transition-shadow shadow-md hover:shadow-lg cursor-pointer w-full"
onClick={() => setShowFullscreenImgPopup(true)}
aria-label={`${credentialFiendlyName}`}
title={t('pageCredentials.credentialFullScreenTitle', { friendlyName: credentialFiendlyName })}
>
<CredentialImage credential={vcEntity.credential} className={"w-full object-cover"} showRibbon={screenType !== 'mobile'} />
</button>
<div className='flex flex-col gap-4 w-4/5 xm:w-4/12'>
<button className="relative rounded-xl xm:rounded-lg w-full overflow-hidden transition-shadow shadow-md hover:shadow-lg cursor-pointer w-full"
onClick={() => setShowFullscreenImgPopup(true)}
aria-label={`${credentialFiendlyName}`}
title={t('pageCredentials.credentialFullScreenTitle', { friendlyName: credentialFiendlyName })}
>
<CredentialImage vcEntity={vcEntity} credential={vcEntity.credential} className={"w-full object-cover"} showRibbon={screenType !== 'mobile'} />
</button>
{screenType !== 'mobile' && zeroSigCount !== null && sigTotal &&
<UsageStats zeroSigCount={zeroSigCount} sigTotal={sigTotal} />
}
</div>
)}

<div>
{screenType === 'mobile' && (
<p className='text-xl font-bold text-primary dark:text-white'>{credentialFiendlyName}</p>
<div className='flex flex-start flex-col gap-1'>
<p className='text-xl font-bold text-primary dark:text-white'>{credentialFiendlyName}</p>
<UsageStats zeroSigCount={zeroSigCount} sigTotal={sigTotal} />
</div>
)}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
// StatusRibbon.js
// ExpiredRibbon.js
import React from 'react';
import { useTranslation } from 'react-i18next';
import { CheckExpired } from '../../functions/CheckExpired';

const StatusRibbon = ({ parsedCredential }) => {
const ExpiredRibbon = ({ parsedCredential }) => {
const { t } = useTranslation();

return (
<>
{parsedCredential && CheckExpired(parsedCredential.expiry_date) &&
<div className={`absolute bottom-0 right-0 text-white text-xs py-1 px-3 rounded-tl-lg border-t-2 border-l-2 border-gray-200 dark:border-gray-800 ${CheckExpired(parsedCredential.expiry_date) && 'bg-red-600'}`}>
{t('statusRibbon.expired')}
<div className={`absolute bottom-0 right-0 text-white text-xs py-1 px-3 rounded-tl-lg rounded-br-2xl border-t border-l border-white ${CheckExpired(parsedCredential.expirationDate) ? 'bg-red-600' : 'bg-green-500'}`}>
{t('expiredRibbon.expired')}
</div>
}
</>
);
};

export default StatusRibbon;
export default ExpiredRibbon;
19 changes: 19 additions & 0 deletions src/components/Credentials/UsagesRibbon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// UsagesRibbon.js
import React from 'react';
import { PiCardsBold } from "react-icons/pi";

const UsagesRibbon = ({ vcEntityInstances }) => {
const zeroSigCount = vcEntityInstances?.filter(instance => instance.sigCount === 0).length || 0;

return (
<>
{vcEntityInstances &&
<div className={`z-50 absolute top-[-5px] font-semibold right-[-5px] text-white text-xs py-1 px-3 flex gap-1 items-center rounded-lg border-2 border-gray-100 dark:border-gray-800 ${zeroSigCount === 0 ? 'bg-orange-500' : 'bg-green-500'}`}>
<PiCardsBold size={18} /> {zeroSigCount}
</div>
}
</>
);
};

export default UsagesRibbon;
2 changes: 1 addition & 1 deletion src/components/History/HistoryDetailContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const HistoryDetailContent = ({ historyItem }) => {
aria-label={credential.friendlyName}
title={t('pageCredentials.credentialFullScreenTitle', { friendlyName: credential.friendlyName })}
>
<CredentialImage credential={credential} className="w-full h-full rounded-xl" />
<CredentialImage credential={credential} showRibbon={false} className="w-full h-full rounded-xl" />
</div>
);

Expand Down
2 changes: 1 addition & 1 deletion src/components/Layout/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const Header = () => {

return (
<header className={`sticky top-0 z-50 w-full bg-primary dark:bg-primary-hover text-white flex items-center justify-between shadow-md md:hidden rounded-b-lg transition-all duration-300 ${isScrolled ? 'p-3' : 'p-4'}`}>
<ConnectionStatusIcon size={isScrolled ? 20 : 25} className="transition-all duration-300" />
<ConnectionStatusIcon size={isScrolled ? 'small' : 'normal'} className="transition-all duration-300" />
<div className="flex items-center">
<button className='mr-2' onClick={() => handleNavigate('/')}>
<img
Expand Down
47 changes: 40 additions & 7 deletions src/components/Layout/Navigation/ConnectionStatusIcon.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,49 @@
import React, { useContext } from 'react';
import { PiWifiHighBold, PiWifiSlashBold } from "react-icons/pi";
import { useTranslation } from 'react-i18next';
import StatusContext from '../../../context/StatusContext';
import { FaXmark } from "react-icons/fa6";

const ConnectionStatusIcon = ({ size }) => {
const { isOnline } = useContext(StatusContext);
const ConnectionStatusIcon = ({ size = 'normal', backgroundColor = 'dark' }) => {
const { connectivity } = useContext(StatusContext);
const { t } = useTranslation();

return isOnline ? (
<PiWifiHighBold size={size} title={t('common.online')} />
) : (
<PiWifiSlashBold size={size} title={t('common.offline')} />
const quality = connectivity.speed;
const bars = Array.from({ length: 5 }, (_, i) => i < quality);
const barHeights = size === 'normal' ? [4, 8, 12, 16, 20] : [3, 6, 9, 12, 16];
const filledColor = backgroundColor === 'light' ? 'bg-primary dark:bg-white' : 'bg-white';
const UnFilledColor = backgroundColor === 'light' ? 'bg-gray-300 dark:bg-gray-500' : 'bg-gray-500';

const qualityText = (quality) => {
switch (quality) {
case 5: return t('ConnectionStatusIcon.qualityLabels.excellent');
case 4: return t('ConnectionStatusIcon.qualityLabels.good');
case 3: return t('ConnectionStatusIcon.qualityLabels.fair');
case 2: return t('ConnectionStatusIcon.qualityLabels.poor');
case 1: return t('ConnectionStatusIcon.qualityLabels.veryPoor');
default: return '';
}
};

const titleText = quality !== 0
? `${t("ConnectionStatusIcon.status")} ${qualityText(quality)}`
: t('common.offline');


return (
<div className="relative flex items-end" title={titleText}>
{bars.map((filled, i) => (
<div
key={i}
className={`${size === 'small' ? 'w-[3px]' : 'w-[4px]'} mx-px rounded-t-[1px] ${filled ? filledColor : UnFilledColor}`}
style={{ height: `${barHeights[i]}px` }}
/>
))}
{quality === 0 && (
<div className="absolute inset-0 flex items-center justify-center">
<FaXmark size={16} className="text-gray-400 absolute bottom-[-4px] right-[-4px] bg-white border rounded-lg border-gray-400" />
</div>
)}
</div>
);
};

Expand Down
2 changes: 1 addition & 1 deletion src/components/Layout/Navigation/Sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const Sidebar = ({ isOpen, toggle }) => {
<ul>
<div className='flex items-center space-x-2 mb-2 p-2 rounded-r-xl'>
<div className='pr-2 border-r border-white/20'>
<ConnectionStatusIcon size={22} />
<ConnectionStatusIcon size='small' />
</div>

<FaUserCircle size={20} title={displayName || username} />
Expand Down
Loading

0 comments on commit c76fc76

Please sign in to comment.