Skip to content
9 changes: 5 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IonReactRouter } from "@ionic/react-router";
import { Route, Redirect } from "react-router-dom";
import Home from "./pages/Home";
import MeshKitPage from "./pages/MeshKit";
import { ROUTES } from "./constants";

/* Core CSS required for Ionic components to work properly */
import "@ionic/react/css/core.css";
Expand All @@ -29,14 +30,14 @@ const App: React.FC = () => (
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route exact path="/home">
<Route exact path={ROUTES.HOME}>
<Home />
</Route>
<Route exact path="/meshkit">
<Route exact path={ROUTES.MESHKIT}>
<MeshKitPage />
</Route>
<Route exact path="/">
<Redirect to="/home" />
<Route exact path={ROUTES.ROOT}>
<Redirect to={ROUTES.HOME} />
</Route>
</IonRouterOutlet>
</IonReactRouter>
Expand Down
76 changes: 46 additions & 30 deletions src/components/Dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ import { RestoreSuccessModal } from "../Modals/RestoreSuccessModal";
import * as AppGeneral from "../socialcalc/index.js";
import { useHistory } from "react-router";
import { DATA } from "../../app-data.js";
import {
IPFS_GATEWAY_URL,
DEFAULT_FILE_NAME,
RESTORED_INVOICE_DEFAULT,
ConnectionStatus,
ActivityType,
MetricKey,
DEFAULT_METRICS,
CID_DISPLAY_LENGTH,
MAX_ACTIVITY_DISPLAY,
REPORT_FILENAME,
REPORT_HEADER,
TOAST_DURATION_LONG,
TOAST_POSITION,
ROUTES,
} from "../../constants.js";

interface DashboardProps {
store: Local;
Expand All @@ -26,13 +42,13 @@ const Dashboard: React.FC<DashboardProps> = ({ store, onOpenFile, currentBillTyp
const [fileList, setFileList] = useState<{ [key: string]: any }>({});
const [backupHistory, setBackupHistory] = useState<BackupRecord[]>([]);
const [activityLogs, setActivityLogs] = useState<ActivityLog[]>([]);
const [metrics, setMetrics] = useState<AppMetrics>({ invoicesCreated: 0, invoicesBackedUp: 0, successfulRestores: 0, filesUploaded: 0, messagesSent: 0 });
const [metrics, setMetrics] = useState<AppMetrics>(DEFAULT_METRICS);
const [isLoading, setIsLoading] = useState(false);
const [toastMessage, setToastMessage] = useState("");
const [showToast, setShowToast] = useState(false);

// MeshKit Status
const [connStatus, setConnStatus] = useState<string>("Pending");
const [connStatus, setConnStatus] = useState<string>(ConnectionStatus.PENDING);

// Modals / Alerts
const [showAlertDelete, setShowAlertDelete] = useState(false);
Expand Down Expand Up @@ -66,9 +82,9 @@ const Dashboard: React.FC<DashboardProps> = ({ store, onOpenFile, currentBillTyp
const checkConnection = async () => {
try {
const isOnline = await testConnection();
setConnStatus(isOnline ? "Online" : "Offline");
setConnStatus(isOnline ? ConnectionStatus.ONLINE : ConnectionStatus.OFFLINE);
} catch {
setConnStatus("Offline");
setConnStatus(ConnectionStatus.OFFLINE);
}
};

Expand Down Expand Up @@ -118,8 +134,8 @@ const Dashboard: React.FC<DashboardProps> = ({ store, onOpenFile, currentBillTyp
);
await store._saveFile(file);
await store._addBackupHistory({ cid: record.cid, timestamp: new Date().toISOString(), name: key });
await store._incrementMetric('invoicesBackedUp');
await store._logActivity({ type: 'BACKUP', description: `Backed up ${key} to IPFS`, cid: record.cid });
await store._incrementMetric(MetricKey.INVOICES_BACKED_UP);
await store._logActivity({ type: ActivityType.BACKUP, description: `Backed up ${key} to IPFS`, cid: record.cid });
loadData();
setBackupSuccessCid(record.cid);
} catch (error) {
Expand All @@ -134,12 +150,12 @@ const Dashboard: React.FC<DashboardProps> = ({ store, onOpenFile, currentBillTyp
setIsLoading(true);
try {
const backup = await restoreInvoiceFromIPFS(cid);
let filename = overwriteKey || backup.name || "restored_invoice";
let filename = overwriteKey || backup.name || RESTORED_INVOICE_DEFAULT;

if (!overwriteKey) {
let counter = 1;
while (await store._checkKey(filename)) {
filename = `${backup.name || "restored_invoice"}_${counter}`;
filename = `${backup.name || RESTORED_INVOICE_DEFAULT}_${counter}`;
counter++;
}
}
Expand All @@ -156,8 +172,8 @@ const Dashboard: React.FC<DashboardProps> = ({ store, onOpenFile, currentBillTyp
);

await store._saveFile(file);
await store._incrementMetric('successfulRestores');
await store._logActivity({ type: 'RESTORE', description: `Restored ${filename} from IPFS`, cid });
await store._incrementMetric(MetricKey.SUCCESSFUL_RESTORES);
await store._logActivity({ type: ActivityType.RESTORE, description: `Restored ${filename} from IPFS`, cid });
loadData();
handleOpen(filename);
setRestoreSuccessFile(filename);
Expand All @@ -170,8 +186,8 @@ const Dashboard: React.FC<DashboardProps> = ({ store, onOpenFile, currentBillTyp

const handleShare = async (cid: string) => {
try {
await navigator.clipboard.writeText(`CID: ${cid}\nGateway: https://gateway.pinata.cloud/ipfs/${cid}`);
await store._logActivity({ type: 'SHARE', description: `Shared IPFS CID`, cid });
await navigator.clipboard.writeText(`CID: ${cid}\nGateway: ${IPFS_GATEWAY_URL}${cid}`);
await store._logActivity({ type: ActivityType.SHARE, description: `Shared IPFS CID`, cid });
loadData();
displayToast("CID & Gateway Link copied to clipboard!");
} catch (err) {
Expand All @@ -187,15 +203,15 @@ const Dashboard: React.FC<DashboardProps> = ({ store, onOpenFile, currentBillTyp
const confirmDelete = async () => {
if (fileToDelete) {
await store._deleteFile(fileToDelete);
await store._logActivity({ type: 'DELETE', description: `Deleted invoice ${fileToDelete}` });
await store._logActivity({ type: ActivityType.DELETE, description: `Deleted invoice ${fileToDelete}` });
loadData();
setFileToDelete(null);
displayToast("Invoice deleted");
}
};

const handleNewFile = async (filename: string) => {
if (!filename || filename.trim() === "" || filename === "default") {
if (!filename || filename.trim() === "" || filename === DEFAULT_FILE_NAME) {
displayToast("Invalid filename");
return;
}
Expand All @@ -206,38 +222,38 @@ const Dashboard: React.FC<DashboardProps> = ({ store, onOpenFile, currentBillTyp
const content = encodeURIComponent(JSON.stringify(DATA["home"][AppGeneral.getDeviceType()]["msc"]));
const file = new LocalFile(new Date().toString(), new Date().toString(), content, filename, currentBillType);
await store._saveFile(file);
await store._incrementMetric('invoicesCreated');
await store._logActivity({ type: 'CREATE', description: `Created new invoice ${filename}` });
await store._incrementMetric(MetricKey.INVOICES_CREATED);
await store._logActivity({ type: ActivityType.CREATE, description: `Created new invoice ${filename}` });
loadData();
handleOpen(filename);
};

const handleExportReport = async () => {
const hist = await store._getBackupHistory();
let report = "========================================\n";
report += " MESHKIT + IPFS DECENTRALIZED REPORT \n";
report += ` ${REPORT_HEADER} \n`;
report += "========================================\n\n";
hist.forEach(h => {
report += `Invoice Name : ${h.name}\n`;
report += `CID : ${h.cid}\n`;
report += `Timestamp : ${new Date(h.timestamp).toLocaleString()}\n`;
report += `Provider : Pinata IPFS\n`;
report += `Recovery Status : Verified & Available\n`;
report += `Gateway Link : https://gateway.pinata.cloud/ipfs/${h.cid}\n`;
report += `Gateway Link : ${IPFS_GATEWAY_URL}${h.cid}\n`;
report += `----------------------------------------\n`;
});
const blob = new Blob([report], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "Decentralized_Storage_Report.txt";
a.download = REPORT_FILENAME;
a.click();
displayToast("Report exported successfully");
};

const handleExploreCID = () => {
if (!exploreCid) return displayToast("Enter a valid CID");
window.open(`https://gateway.pinata.cloud/ipfs/${exploreCid}`, "_blank");
window.open(`${IPFS_GATEWAY_URL}${exploreCid}`, "_blank");
};

const uniqueCIDs = new Set(backupHistory.map(h => h.cid)).size;
Expand Down Expand Up @@ -305,7 +321,7 @@ const Dashboard: React.FC<DashboardProps> = ({ store, onOpenFile, currentBillTyp
</IonBadge>
{fileList[key].cid && (
<span style={{ marginLeft: '10px', fontSize: '0.85em', color: 'var(--ion-color-medium)' }}>
CID: {fileList[key].cid.substring(0, 8)}...
CID: {fileList[key].cid.substring(0, CID_DISPLAY_LENGTH)}...
</span>
)}
</div>
Expand Down Expand Up @@ -345,13 +361,13 @@ const Dashboard: React.FC<DashboardProps> = ({ store, onOpenFile, currentBillTyp
<p className="ion-padding ion-text-center" style={{ color: 'var(--ion-color-medium)' }}>No activities recorded.</p>
) : (
<IonList lines="full">
{activityLogs.slice(0, 15).map((log, idx) => {
{activityLogs.slice(0, MAX_ACTIVITY_DISPLAY).map((log, idx) => {
let icon = documentOutline;
let color = "primary";
if (log.type === 'BACKUP') { icon = cloudUploadOutline; color = "success"; }
if (log.type === 'RESTORE') { icon = cloudDownloadOutline; color = "secondary"; }
if (log.type === 'DELETE') { icon = trashOutline; color = "danger"; }
if (log.type === 'SHARE') { icon = shareSocialOutline; color = "tertiary"; }
if (log.type === ActivityType.BACKUP) { icon = cloudUploadOutline; color = "success"; }
if (log.type === ActivityType.RESTORE) { icon = cloudDownloadOutline; color = "secondary"; }
if (log.type === ActivityType.DELETE) { icon = trashOutline; color = "danger"; }
if (log.type === ActivityType.SHARE) { icon = shareSocialOutline; color = "tertiary"; }

return (
<IonItem key={log.id}>
Expand Down Expand Up @@ -389,7 +405,7 @@ const Dashboard: React.FC<DashboardProps> = ({ store, onOpenFile, currentBillTyp
<IonButton expand="block" fill="outline" onClick={() => setShowRestorePrompt(true)}>
<IonIcon icon={cloudDownloadOutline} slot="start" /> Restore from CID
</IonButton>
<IonButton expand="block" fill="outline" color="secondary" onClick={() => history.push("/meshkit")}>
<IonButton expand="block" fill="outline" color="secondary" onClick={() => history.push(ROUTES.MESHKIT)}>
<IonIcon icon={hardwareChipOutline} slot="start" /> MeshKit SDK Demo
</IonButton>
</IonCardContent>
Expand Down Expand Up @@ -424,7 +440,7 @@ const Dashboard: React.FC<DashboardProps> = ({ store, onOpenFile, currentBillTyp
<IonItem>
<IonIcon icon={hardwareChipOutline} slot="start" color="primary" />
<IonLabel>Connection</IonLabel>
<IonBadge color={connStatus === 'Online' ? 'success' : connStatus === 'Offline' ? 'danger' : 'warning'} slot="end">
<IonBadge color={connStatus === ConnectionStatus.ONLINE ? 'success' : connStatus === ConnectionStatus.OFFLINE ? 'danger' : 'warning'} slot="end">
{connStatus}
</IonBadge>
</IonItem>
Expand Down Expand Up @@ -481,8 +497,8 @@ const Dashboard: React.FC<DashboardProps> = ({ store, onOpenFile, currentBillTyp
isOpen={showToast}
onDidDismiss={() => setShowToast(false)}
message={toastMessage}
duration={3000}
position="bottom"
duration={TOAST_DURATION_LONG}
position={TOAST_POSITION}
/>

<BackupSuccessModal
Expand Down
27 changes: 18 additions & 9 deletions src/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ import { saveOutline, save, mail, print, cloudUploadOutline, cloudDownloadOutlin
import { APP_NAME } from "../../app-data.js";
import { backupInvoiceToIPFS, restoreInvoiceFromIPFS } from "../../services/MeshkitService";
import { BackupSuccessModal } from "../Modals/BackupSuccessModal";
import {
DEFAULT_FILE_NAME,
MAX_FILENAME_LENGTH,
EMAIL_RECIPIENT,
EMAIL_BODY_DEFAULT,
EMAIL_ATTACHMENT_NAME,
TOAST_DURATION_SHORT,
TOAST_POSITION,
} from "../../constants";

const Menu: React.FC<{
showM: boolean;
Expand Down Expand Up @@ -37,13 +46,13 @@ const Menu: React.FC<{
/* Utility functions */
const _validateName = async (filename) => {
filename = filename.trim();
if (filename === "default" || filename === "Untitled") {
if (filename === DEFAULT_FILE_NAME || filename === "Untitled") {
setToastMessage("Cannot update default file!");
return false;
} else if (filename === "" || !filename) {
setToastMessage("Filename cannot be empty");
return false;
} else if (filename.length > 30) {
} else if (filename.length > MAX_FILENAME_LENGTH) {
setToastMessage("Filename too long");
return false;
} else if (/^[a-zA-Z0-9- ]*$/.test(filename) === false) {
Expand Down Expand Up @@ -77,7 +86,7 @@ const Menu: React.FC<{
}
};
const doSave = () => {
if (props.file === "default") {
if (props.file === DEFAULT_FILE_NAME) {
setShowAlert1(true);
return;
}
Expand Down Expand Up @@ -114,7 +123,7 @@ const Menu: React.FC<{
}
};
const doBackupToIPFS = async () => {
if (props.file === "default") {
if (props.file === DEFAULT_FILE_NAME) {
setShowAlertBackupMissingFile(true);
return;
}
Expand Down Expand Up @@ -191,11 +200,11 @@ const Menu: React.FC<{
const content = AppGeneral.getCurrentHTMLContent();
const base64 = btoa(content);
EmailComposer.open({
to: ["jackdwell08@gmail.com"],
to: [EMAIL_RECIPIENT],
cc: [],
bcc: [],
body: "PFA",
attachments: [{ type: "base64", path: base64, name: "Invoice.html" }],
body: EMAIL_BODY_DEFAULT,
attachments: [{ type: "base64", path: base64, name: EMAIL_ATTACHMENT_NAME }],
subject: `${APP_NAME} attached`,
isHtml: true,
});
Expand Down Expand Up @@ -357,9 +366,9 @@ const Menu: React.FC<{
setShowToast1(false);
setShowAlert3(true);
}}
position="bottom"
position={TOAST_POSITION}
message={toastMessage}
duration={500}
duration={TOAST_DURATION_SHORT}
/>
</React.Fragment>
);
Expand Down
3 changes: 2 additions & 1 deletion src/components/Modals/BackupSuccessModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import { IonModal, IonContent, IonButton, IonIcon, IonText } from '@ionic/react';
import { checkmarkCircle, copyOutline, openOutline } from 'ionicons/icons';
import { IPFS_GATEWAY_URL } from '../../constants';

interface Props {
isOpen: boolean;
Expand All @@ -16,7 +17,7 @@ export const BackupSuccessModal: React.FC<Props> = ({ isOpen, onClose, cid }) =>
};

const openGateway = () => {
window.open(`https://gateway.pinata.cloud/ipfs/${cid}`, "_blank");
window.open(`${IPFS_GATEWAY_URL}${cid}`, "_blank");
};

return (
Expand Down
7 changes: 4 additions & 3 deletions src/components/NewFile/NewFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { File, Local } from "../Storage/LocalStorage";
import { DATA } from "../../app-data.js";
import { IonAlert, IonIcon } from "@ionic/react";
import { add } from "ionicons/icons";
import { DEFAULT_FILE_NAME } from "../../constants";

const NewFile: React.FC<{
file: string;
Expand All @@ -13,7 +14,7 @@ const NewFile: React.FC<{
}> = (props) => {
const [showAlertNewFileCreated, setShowAlertNewFileCreated] = useState(false);
const newFile = () => {
if (props.file !== "default") {
if (props.file !== DEFAULT_FILE_NAME) {
const content = encodeURIComponent(AppGeneral.getSpreadsheetContent());
const data = props.store._getFile(props.file);
const file = new File(
Expand All @@ -27,8 +28,8 @@ const NewFile: React.FC<{
props.updateSelectedFile(props.file);
}
const msc = DATA["home"][AppGeneral.getDeviceType()]["msc"];
AppGeneral.viewFile("default", JSON.stringify(msc));
props.updateSelectedFile("default");
AppGeneral.viewFile(DEFAULT_FILE_NAME, JSON.stringify(msc));
props.updateSelectedFile(DEFAULT_FILE_NAME);
setShowAlertNewFileCreated(true);
};

Expand Down
Loading