Skip to content

Commit

Permalink
feat: extension installation modal
Browse files Browse the repository at this point in the history
  • Loading branch information
virtual-designer committed Feb 8, 2024
1 parent 8f4426c commit 4948919
Show file tree
Hide file tree
Showing 15 changed files with 407 additions and 6 deletions.
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"react": "^18",
"react-dom": "^18",
"react-icons": "^5.0.1",
"recoil": "^0.7.7"
"recoil": "^0.7.7",
"semver": "^7.6.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"@commitlint/cli": "^18.6.0",
Expand All @@ -35,6 +37,8 @@
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/semver": "^7.5.6",
"@types/uuid": "^9.0.8",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.0",
Expand All @@ -43,4 +47,4 @@
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
}
}
1 change: 0 additions & 1 deletion public/next.svg

This file was deleted.

1 change: 0 additions & 1 deletion public/vercel.svg

This file was deleted.

2 changes: 2 additions & 0 deletions src/app/extension/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ExtensionDownloadModal from "@/components/Extension/ExtensionDownloadModa
import ExtensionIcon from "@/components/Extension/ExtensionIcon";
import ExtensionInfoList from "@/components/Extension/ExtensionInfoList";
import ExtensionInfoMobileList from "@/components/Extension/ExtensionInfoListMobile";
import ExtensionInstallModal from "@/components/Extension/ExtensionInstallModal";
import ExtensionSecurity from "@/components/Extension/ExtensionSecurity";
import { Divider } from "@/components/Layout/Divider";
import { INDEX_URL } from "@/config/urls";
Expand Down Expand Up @@ -116,6 +117,7 @@ export default async function ExtensionPage({ params }: ServerSidePageProps) {
return (
<>
<ExtensionDownloadModal extension={extension} />
<ExtensionInstallModal extension={extension} />

<Container>
<main className="my-5 lg:my-10">
Expand Down
29 changes: 27 additions & 2 deletions src/app/recoil.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,33 @@
"use client";

import ExtensionPageAtom from "@/atoms/ExtensionPageAtom";
import useLocalStorage from "@/hooks/useLocalStorage";
import { LocalStorageSystemInstance } from "@/types/SystemInstance";
import { PropsWithChildren } from "react";
import { RecoilRoot } from "recoil";
import { MutableSnapshot, RecoilRoot } from "recoil";

export default function RecoilProvider({ children }: PropsWithChildren) {
return <RecoilRoot>{children}</RecoilRoot>;
const [get] = useLocalStorage("global_state");

const initializeState = (snapshot: MutableSnapshot) => {
const persisted = get();

if (persisted) {
const persistedState = JSON.parse(persisted);

if ("instances" in persistedState) {
snapshot.set(ExtensionPageAtom, {
instances: [
...persistedState.instances,
] as LocalStorageSystemInstance[],
downloadModalOpen: false,
installModalOpen: false,
});
}
}
};

return (
<RecoilRoot initializeState={initializeState}>{children}</RecoilRoot>
);
}
13 changes: 13 additions & 0 deletions src/atoms/ExtensionPageAtom.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import { LocalStorageSystemInstance } from "@/types/SystemInstance";
import { atom } from "recoil";
import { v4 as uuid } from "uuid";

const ExtensionPageState = atom({
key: "extensionPageState",
default: {
downloadModalOpen: false,
installModalOpen: false,
instances: [
{
id: uuid(),
url: "https://www.sudobot.org",
},
{
id: uuid(),
url: "https://whatever.org",
},
] as LocalStorageSystemInstance[],
},
});

Expand Down
6 changes: 6 additions & 0 deletions src/components/Extension/ExtensionControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ export default function ExtensionControls({
sx={{ px: 1.5 }}
fullWidth
startIcon={<DeployedCodeUpdate />}
onClick={() =>
setState((state) => ({
...state,
installModalOpen: true,
}))
}
>
Install
</Button>
Expand Down
26 changes: 26 additions & 0 deletions src/components/Extension/ExtensionInstallChooseInstances.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Box } from "@mui/material";
import { FC } from "react";
import Instances from "../Instance/Instances";

interface ExtensionInstallationChooseInstancesProps {}

const ExtensionInstallationChooseInstances: FC<
ExtensionInstallationChooseInstancesProps
> = () => {
return (
<Box
sx={{
pt: 2,
}}
>
<p className="text-sm md:text-base text-[#555] dark:text-[#999]">
Choose the instances you want to install the extension on.
</p>
<br />

<Instances />
</Box>
);
};

export default ExtensionInstallationChooseInstances;
31 changes: 31 additions & 0 deletions src/components/Extension/ExtensionInstallCredentials.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Box, TextField } from "@mui/material";
import { FC } from "react";

interface ExtensionInstallCredentialsProps {}

const ExtensionInstallCredentials: FC<
ExtensionInstallCredentialsProps
> = () => {
return (
<Box sx={{ pt: 2 }}>
<p className="text-sm md:text-base text-[#555] dark:text-[#999]">
Enter the credentials to authenticate and install the extension.
</p>

<div className="grid md:grid-cols-2 gap-4 mt-4">
<TextField
type="text"
placeholder="Username"
className="input"
/>
<TextField
type="password"
placeholder="Password"
className="input"
/>
</div>
</Box>
);
};

export default ExtensionInstallCredentials;
179 changes: 179 additions & 0 deletions src/components/Extension/ExtensionInstallModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
"use client";

import ExtensionPageState from "@/atoms/ExtensionPageAtom";
import { APIExtension } from "@/types/APIExtension";
import {
Box,
Button,
Modal,
Step,
StepLabel,
Stepper,
Typography,
} from "@mui/material";
import { useState } from "react";
import { MdClose } from "react-icons/md";
import { useRecoilState } from "recoil";
import DeployedCodeUpdate from "../Icons/DeployedCodeUpdate";
import ExtensionInstallationChooseInstances from "./ExtensionInstallChooseInstances";
import ExtensionInstallCredentials from "./ExtensionInstallCredentials";

type Props = {
extension: APIExtension;
};

const steps = ["Choose instances", "Install", "Finish"];
const components = [
ExtensionInstallationChooseInstances,
ExtensionInstallCredentials,
() => <p>Finish</p>,
];

export default function ExtensionInstallModal({ extension }: Props) {
const [state, setState] = useRecoilState(ExtensionPageState);
const [activeStep, setActiveStep] = useState(0);
const [skipped, setSkipped] = useState(new Set<number>());
const onClose = () => {
setState((state) => ({ ...state, installModalOpen: false }));
setActiveStep(0);
setSkipped(new Set<number>());
};

const StepComponent = components[activeStep];

const isStepOptional = (step: number) => {
return false;
};

const isStepSkipped = (step: number) => {
return skipped.has(step);
};

const handleNext = () => {
let newSkipped = skipped;
if (isStepSkipped(activeStep)) {
newSkipped = new Set(newSkipped.values());
newSkipped.delete(activeStep);
}

setActiveStep((prevActiveStep) => prevActiveStep + 1);
setSkipped(newSkipped);
};

const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};

const handleSkip = () => {
if (!isStepOptional(activeStep)) {
throw new Error("You can't skip a step that isn't optional.");
}

setActiveStep((prevActiveStep) => prevActiveStep + 1);
setSkipped((prevSkipped) => {
const newSkipped = new Set(prevSkipped.values());
newSkipped.add(activeStep);
return newSkipped;
});
};

const handleReset = () => {
setActiveStep(0);
};

return (
<Modal
open={state.installModalOpen}
onClose={onClose}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
"@media (max-width: 640px)": {
marginTop: "10%",
},
}}
>
<div className="bg-white shadow dark:shdaow-none dark:bg-[#222] w-[90%] md:w-[50%] h-[82%] md:h-[60%] rounded-lg relative">
<div className="p-4 rounded-t-lg flex items-center justify-between">
<h1 className="text-xl md:text-2xl font-semibold text-[#333] dark:text-[#ddd]">
<DeployedCodeUpdate size="1.3" />
<span className="ml-3">Install {extension.name}</span>
</h1>
<Button onClick={onClose} sx={{ minWidth: 0, px: 1 }}>
<MdClose size="1.4em" />
</Button>
</div>
<hr className="border-t dark:border-[#444]" />
<div className="p-4 h-[70%] overflow-y-scroll">
<Box sx={{ width: "100%" }}>
<Stepper activeStep={activeStep}>
{steps.map((label, index) => {
const stepProps: { completed?: boolean } = {};
const labelProps: {
optional?: React.ReactNode;
} = {};

if (isStepOptional(index)) {
labelProps.optional = (
<Typography variant="caption">
Optional
</Typography>
);
}

if (isStepSkipped(index)) {
stepProps.completed = false;
}

return (
<Step key={label} {...stepProps}>
<StepLabel {...labelProps}>
{label}
</StepLabel>
</Step>
);
})}
</Stepper>
{activeStep === steps.length ? (
<>
<Typography sx={{ mt: 2, mb: 1 }}>
All steps completed - you&apos;re finished
</Typography>
</>
) : (
<StepComponent />
)}
</Box>

<div className="rounded-b-lg flex justify-end gap-4 p-4 absolute bottom-0 left-0 w-[100%] z-[10000] bg-white dark:bg-[#222] [box-shadow:0_-1px_1px_0_rgba(0,0,0,0.1)] dark:[box-shadow:0_0_1px_0_rgba(255,255,255,0.4)]">
<Button
color="inherit"
disabled={activeStep === 0}
onClick={handleBack}
sx={{ mr: 1 }}
>
Back
</Button>
<Box sx={{ flex: "1 1 auto" }} />
{isStepOptional(activeStep) && (
<Button
color="inherit"
onClick={handleSkip}
sx={{ mr: 1 }}
>
Skip
</Button>
)}
<Button onClick={onClose}>Cancel</Button>{" "}
<Button onClick={handleNext} variant="outlined">
{activeStep === steps.length - 1
? "Finish"
: "Next"}
</Button>
</div>
</div>
</div>
</Modal>
);
}
Loading

0 comments on commit 4948919

Please sign in to comment.