init commit,
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import React, { ReactNode, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Container,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import axios from "axios";
|
||||
import QRCode from "qrcode.react";
|
||||
|
||||
interface GetQRButtonProps {
|
||||
name: string;
|
||||
url: string;
|
||||
customCredentialForm?: ReactNode;
|
||||
jsonFilePath?: string;
|
||||
icon?: ReactNode;
|
||||
onQRGenerated?: (generated: boolean) => void;
|
||||
}
|
||||
|
||||
const GetQRButton: React.FC<GetQRButtonProps> = ({
|
||||
name,
|
||||
url,
|
||||
customCredentialForm,
|
||||
jsonFilePath,
|
||||
icon,
|
||||
onQRGenerated,
|
||||
}) => {
|
||||
const [responseLink, setResponseLink] = useState("");
|
||||
const [showQR, setShowQR] = useState(false);
|
||||
const [customCredential, setCustomCredential] = useState("");
|
||||
|
||||
const [showCircularProgress, setShowCircularProgress] = useState(false);
|
||||
const [showCustomCredential, setShowCustomCredential] = useState(false);
|
||||
const [errorOnRequest, setErrorOnRequest] = useState(false);
|
||||
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
setShowQR(false);
|
||||
setShowCircularProgress(true);
|
||||
setErrorOnRequest(false);
|
||||
|
||||
let response;
|
||||
|
||||
try {
|
||||
if (customCredentialForm) {
|
||||
if (customCredential) {
|
||||
response = await axios.post(url, customCredential);
|
||||
setShowCustomCredential(true);
|
||||
} else {
|
||||
setShowCircularProgress(false);
|
||||
setShowQR(false);
|
||||
return;
|
||||
}
|
||||
} else if (jsonFilePath) {
|
||||
response = await axios.post(
|
||||
url,
|
||||
await fetch(jsonFilePath).then((response) => response.json()),
|
||||
);
|
||||
setResponseLink(await response.data.data);
|
||||
} else {
|
||||
response = await axios(url);
|
||||
setResponseLink(await response.data.data);
|
||||
}
|
||||
|
||||
setResponseLink(await response.data.data);
|
||||
console.log(name, response.data.data);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
setShowCircularProgress(false);
|
||||
setErrorOnRequest(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (onQRGenerated) {
|
||||
onQRGenerated(true);
|
||||
}
|
||||
|
||||
setShowCircularProgress(false);
|
||||
setShowQR(true);
|
||||
} catch (error) {
|
||||
console.error("Error while trying to connect to the server.", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container sx={{ py: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{icon}
|
||||
{name}
|
||||
</Typography>
|
||||
{customCredentialForm &&
|
||||
React.cloneElement(customCredentialForm as React.ReactElement<any>, {
|
||||
onCustomCredentialChange: setCustomCredential,
|
||||
})}
|
||||
<Box sx={{ display: "flex", justifyContent: "right" }}>
|
||||
<Button variant="contained" color="primary" onClick={handleClick}>
|
||||
Request OOBI
|
||||
</Button>
|
||||
</Box>
|
||||
{showCircularProgress && (
|
||||
<Box sx={{ p: 2, display: "flex", justifyContent: "center" }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
{showQR && (
|
||||
<Box sx={{ p: 2, display: "flex", justifyContent: "center" }}>
|
||||
<QRCode value={responseLink} size={256} />
|
||||
</Box>
|
||||
)}
|
||||
{showCustomCredential && (
|
||||
<TextField
|
||||
id="outlined-multiline-static"
|
||||
label="Credential JSON"
|
||||
multiline
|
||||
rows={JSON.stringify(customCredential, null, 2).split("\n").length}
|
||||
fullWidth
|
||||
value={JSON.stringify(customCredential, null, 2)}
|
||||
/>
|
||||
)}
|
||||
{errorOnRequest && (
|
||||
<Alert severity="error">
|
||||
It was not possible to connect to the server. Try again
|
||||
</Alert>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default GetQRButton;
|
@@ -0,0 +1,126 @@
|
||||
import React from "react";
|
||||
import {
|
||||
AppBar,
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Toolbar,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Link } from "react-router-dom";
|
||||
import logo from "../assets/favicon.svg";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import { MENU_ITEMS } from "../App";
|
||||
|
||||
const NavBar: React.FC = () => {
|
||||
const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(
|
||||
null,
|
||||
);
|
||||
|
||||
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorElNav(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleCloseNavMenu = () => {
|
||||
setAnchorElNav(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppBar position="static">
|
||||
<Container maxWidth="xl">
|
||||
<Toolbar disableGutters>
|
||||
<a href="/">
|
||||
<img
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
style={{ width: "30px", height: "30px", margin: "10px" }}
|
||||
/>
|
||||
</a>
|
||||
<Typography
|
||||
variant="h6"
|
||||
noWrap
|
||||
component="a"
|
||||
href="/"
|
||||
sx={{
|
||||
mr: 2,
|
||||
display: { xs: "none", md: "flex" },
|
||||
fontFamily: "monospace",
|
||||
fontWeight: 700,
|
||||
color: "inherit",
|
||||
textDecoration: "none",
|
||||
}}
|
||||
>
|
||||
Cardano Foundation | Identity Wallet
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ flexGrow: 1, display: { xs: "flex", md: "none" } }}>
|
||||
<IconButton
|
||||
size="large"
|
||||
aria-label="account of current user"
|
||||
aria-controls="menu-appbar"
|
||||
aria-haspopup="true"
|
||||
onClick={handleOpenNavMenu}
|
||||
color="inherit"
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorElNav}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
open={Boolean(anchorElNav)}
|
||||
onClose={handleCloseNavMenu}
|
||||
sx={{
|
||||
display: { xs: "block", md: "none" },
|
||||
}}
|
||||
>
|
||||
{MENU_ITEMS.map((item) => (
|
||||
<MenuItem key={item.key} onClick={handleCloseNavMenu}>
|
||||
<Typography
|
||||
textAlign="center"
|
||||
component={Link}
|
||||
to={item.path}
|
||||
>
|
||||
{item.label}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flexGrow: 1, display: { xs: "none", md: "flex" } }}>
|
||||
{MENU_ITEMS.map((item) => (
|
||||
<Button
|
||||
key={item.key}
|
||||
component={Link}
|
||||
to={item.path}
|
||||
sx={{
|
||||
my: 1,
|
||||
px: 2.5,
|
||||
fontSize: "1.1rem",
|
||||
color: "white",
|
||||
display: "block",
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</Container>
|
||||
</AppBar>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBar;
|
@@ -0,0 +1,132 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
FormControl,
|
||||
Grid,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { DatePicker } from "@mui/x-date-pickers";
|
||||
import { Dayjs } from "dayjs";
|
||||
|
||||
interface PrescriptionFormProps {
|
||||
onCustomCredentialChange?: (newJson: string) => void;
|
||||
}
|
||||
|
||||
const PrescriptionForm: React.FC<PrescriptionFormProps> = ({
|
||||
onCustomCredentialChange,
|
||||
}) => {
|
||||
const jsonFilePath = "credentials-json/prescription-credential.json";
|
||||
const [type, setType] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [expirationDate, setExpirationDate] = React.useState<Dayjs | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const [isSuccessfulValidationVisible, setIsSuccessfulValidationVisible] =
|
||||
useState(false);
|
||||
const [isUnsuccessfulValidationVisible, setIsUnsuccessfulValidationVisible] =
|
||||
useState(false);
|
||||
|
||||
const isInformationCorrect = () => {
|
||||
if (type === "" || name === "" || expirationDate === null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const generateJson = async () => {
|
||||
if (!isInformationCorrect()) {
|
||||
setIsSuccessfulValidationVisible(false);
|
||||
setIsUnsuccessfulValidationVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const prescriptionCredential = await fetch(jsonFilePath).then((response) =>
|
||||
response.json(),
|
||||
);
|
||||
prescriptionCredential.credentialSubject.prescription.type = type;
|
||||
prescriptionCredential.credentialSubject.prescription.name = name;
|
||||
prescriptionCredential.expirationDate = expirationDate?.toISOString();
|
||||
onCustomCredentialChange?.(prescriptionCredential);
|
||||
|
||||
setIsSuccessfulValidationVisible(true);
|
||||
setIsUnsuccessfulValidationVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container spacing={2} margin={2}>
|
||||
<Grid item xs={11} sm={6}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="input-label-type">Class of medicine</InputLabel>
|
||||
<Select
|
||||
labelId="select-label-type"
|
||||
id="id-select-type"
|
||||
value={type}
|
||||
label="Class of medicine"
|
||||
onChange={(e) => setType(e.target.value)}
|
||||
>
|
||||
<MenuItem value={"Antipyretics"}>Antipyretics</MenuItem>
|
||||
<MenuItem value={"Analgesics"}>Analgesics</MenuItem>
|
||||
<MenuItem value={"Antibiotics"}>Antibiotics</MenuItem>
|
||||
<MenuItem value={"Antiseptics"}>Antiseptics</MenuItem>
|
||||
<MenuItem value={"Mood stabilizers"}>Mood stabilizers</MenuItem>
|
||||
<MenuItem value={"Tranquilizers"}>Tranquilizers</MenuItem>
|
||||
<MenuItem value={"Stimulants"}>Stimulants</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={11} sm={6}>
|
||||
<TextField
|
||||
required
|
||||
id="input-name"
|
||||
label="Name"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={11} sm={6}>
|
||||
<DatePicker
|
||||
label={"Expiration Date"}
|
||||
value={expirationDate}
|
||||
onChange={(newExpirationDate) => setExpirationDate(newExpirationDate)}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
item
|
||||
xs={11}
|
||||
sm={6}
|
||||
container
|
||||
alignItems="flex-end"
|
||||
justifyContent="right"
|
||||
>
|
||||
<Button variant="contained" color="primary" onClick={generateJson}>
|
||||
Validate Information
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid container justifyContent="center" margin={2}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
{isSuccessfulValidationVisible && (
|
||||
<Alert severity="success">Successfully validated</Alert>
|
||||
)}
|
||||
|
||||
{isUnsuccessfulValidationVisible && (
|
||||
<Alert severity="error">
|
||||
It was not possible to validate the information
|
||||
</Alert>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrescriptionForm;
|
@@ -0,0 +1,98 @@
|
||||
import React, { useState } from "react";
|
||||
import { Alert, Button, Grid, TextField } from "@mui/material";
|
||||
|
||||
interface RelationshipFormProps {
|
||||
onCustomCredentialChange?: (newJson: string) => void;
|
||||
}
|
||||
|
||||
const RelationshipForm: React.FC<RelationshipFormProps> = ({
|
||||
onCustomCredentialChange,
|
||||
}) => {
|
||||
const jsonFilePath = "credentials-json/relationship-credential.json";
|
||||
const [namePartner1, setNamePartner1] = useState("");
|
||||
const [namePartner2, setNamePartner2] = useState("");
|
||||
|
||||
const [isSuccessfulValidationVisible, setIsSuccessfulValidationVisible] =
|
||||
useState(false);
|
||||
const [isUnsuccessfulValidationVisible, setIsUnsuccessfulValidationVisible] =
|
||||
useState(false);
|
||||
|
||||
const isInformationCorrect = () => {
|
||||
if (namePartner1 === "" || namePartner2 === "") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const generateJson = async () => {
|
||||
if (!isInformationCorrect()) {
|
||||
setIsSuccessfulValidationVisible(false);
|
||||
setIsUnsuccessfulValidationVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const prescriptionCredential = await fetch(jsonFilePath).then((response) =>
|
||||
response.json(),
|
||||
);
|
||||
prescriptionCredential.credentialSubject[0].name = namePartner1;
|
||||
prescriptionCredential.credentialSubject[1].name = namePartner2;
|
||||
onCustomCredentialChange?.(prescriptionCredential);
|
||||
|
||||
setIsSuccessfulValidationVisible(true);
|
||||
setIsUnsuccessfulValidationVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container spacing={2} margin={2}>
|
||||
<Grid item xs={11} sm={6}>
|
||||
<TextField
|
||||
required
|
||||
id="input-name-partner-1"
|
||||
label="Name"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onChange={(e) => setNamePartner1(e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={11} sm={6}>
|
||||
<TextField
|
||||
required
|
||||
id="input-name-partner-2"
|
||||
label="Name"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onChange={(e) => setNamePartner2(e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
item
|
||||
xs={11}
|
||||
sm={12}
|
||||
container
|
||||
alignItems="flex-end"
|
||||
justifyContent="right"
|
||||
>
|
||||
<Button variant="contained" color="primary" onClick={generateJson}>
|
||||
Validate Information
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid container justifyContent="center" margin={2}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
{isSuccessfulValidationVisible && (
|
||||
<Alert severity="success">Successfully validated</Alert>
|
||||
)}
|
||||
|
||||
{isUnsuccessfulValidationVisible && (
|
||||
<Alert severity="error">
|
||||
It was not possible to validate the information
|
||||
</Alert>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default RelationshipForm;
|
@@ -0,0 +1,66 @@
|
||||
import React, { useState } from "react";
|
||||
import { Alert, Box, Button, Container, Grid, TextField } from "@mui/material";
|
||||
import { resolveOobi } from "../../services/resolve-oobi";
|
||||
|
||||
interface GetInputButtonProps {}
|
||||
|
||||
const GetInputButton: React.FC<GetInputButtonProps> = () => {
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const [oobi, setOobi] = useState("");
|
||||
const [isAtendeeOobiEmptyVisible, setIsAtendeeOobiEmptyVisible] =
|
||||
useState(false);
|
||||
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setSubmitSuccess(false);
|
||||
if (oobi === "" || !oobi.includes("oobi")) {
|
||||
setIsAtendeeOobiEmptyVisible(true);
|
||||
return;
|
||||
} else {
|
||||
setIsAtendeeOobiEmptyVisible(false);
|
||||
}
|
||||
await resolveOobi(oobi);
|
||||
setSubmitSuccess(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container sx={{ py: 2 }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "right" }} mb={2}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => setShowInput(true)}
|
||||
>
|
||||
Input OOBI
|
||||
</Button>
|
||||
</Box>
|
||||
{showInput && (
|
||||
<>
|
||||
<Grid item xs={11} mb={2}>
|
||||
<TextField
|
||||
required
|
||||
id="input-oobi"
|
||||
label="Oobi link"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onChange={(e) => setOobi(e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sx={{ display: "flex", justifyContent: "right" }}>
|
||||
{isAtendeeOobiEmptyVisible && (
|
||||
<Alert severity="error">Please, input valid OOBI link</Alert>
|
||||
)}
|
||||
{submitSuccess && (
|
||||
<Alert severity="info">Resolve OOBI successfully</Alert>
|
||||
)}
|
||||
<Button variant="contained" color="primary" onClick={handleSubmit}>
|
||||
Submit
|
||||
</Button>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default GetInputButton;
|
@@ -0,0 +1,109 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Box, Button, Container } from "@mui/material";
|
||||
import { Html5QrcodeScanner } from "html5-qrcode";
|
||||
import "./qrscanner.css";
|
||||
import { resolveOobi } from "../../services/resolve-oobi";
|
||||
|
||||
interface GetScannerButtonProps {}
|
||||
enum ContentType {
|
||||
SCANNER = "scanner",
|
||||
RESOLVING = "resolving",
|
||||
RESOLVED = "resolved",
|
||||
}
|
||||
const GetScannerButton: React.FC<GetScannerButtonProps> = () => {
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const [restartCamera, setRestartCamera] = useState(false);
|
||||
const [contentType, setContentType] = useState<ContentType>(
|
||||
ContentType.SCANNER,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (showInput) {
|
||||
const scanner = new Html5QrcodeScanner(
|
||||
"reader",
|
||||
{
|
||||
qrbox: {
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
fps: 5,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
const success = (result: string) => {
|
||||
scanner.clear();
|
||||
handleResolveOObi(result);
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const error = (_: any) => {};
|
||||
scanner.render(success, error);
|
||||
}
|
||||
}, [restartCamera, showInput]);
|
||||
|
||||
const restartScanner = async () => {
|
||||
setRestartCamera(!restartCamera);
|
||||
setContentType(ContentType.SCANNER);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
switch (contentType) {
|
||||
case ContentType.SCANNER:
|
||||
return {
|
||||
component: <div id="reader" />,
|
||||
title: "Scan your wallet QR Code",
|
||||
};
|
||||
case ContentType.RESOLVING:
|
||||
return {
|
||||
component: <></>,
|
||||
title: "Resolving wallet OOBI",
|
||||
};
|
||||
case ContentType.RESOLVED:
|
||||
return {
|
||||
component: (
|
||||
<button className="resolve-button" onClick={() => restartScanner()}>
|
||||
Close
|
||||
</button>
|
||||
),
|
||||
title: "The wallet OOBI was resolved successfully",
|
||||
};
|
||||
}
|
||||
};
|
||||
const handleResolveOObi = async (oobi: string) => {
|
||||
if (!(oobi.length && oobi.includes("oobi"))) {
|
||||
return restartScanner();
|
||||
}
|
||||
|
||||
setContentType(ContentType.RESOLVING);
|
||||
await resolveOobi(oobi);
|
||||
setContentType(ContentType.RESOLVED);
|
||||
};
|
||||
|
||||
const content = renderContent();
|
||||
|
||||
return (
|
||||
<Container sx={{ py: 2 }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "right" }} mb={2}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => setShowInput(true)}
|
||||
>
|
||||
Get Scanner
|
||||
</Button>
|
||||
</Box>
|
||||
{showInput && (
|
||||
<>
|
||||
<div className="scannerPage">
|
||||
<div>
|
||||
<h3 className="">{content?.title}</h3>
|
||||
{content?.component}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default GetScannerButton;
|
@@ -0,0 +1,45 @@
|
||||
body {
|
||||
background-color: #787a83;
|
||||
color: whitesmoke;
|
||||
}
|
||||
|
||||
#reader {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.scannerPage {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section,
|
||||
.lockMessage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.spinner-button {
|
||||
border: 2px solid floralwhite;
|
||||
border-top: 2px solid whitesmoke;
|
||||
border-radius: 50%;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.resolve-button {
|
||||
width: 210px;
|
||||
margin-top: 10px;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
Reference in New Issue
Block a user