Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: delete account UI #37

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,8 @@ GITHUB_APP_PRIVATE_KEY=""

# GitHub App Name in your App's Installation URL (https://github.com/apps/${GITHUB_APP_SLUG}/installations/new)
GITHUB_APP_SLUG=

#Trigger API credential URL(https://trigger.dev/docs/documentation/introduction)
TRIGGER_API_KEY=
TRIGGER_API_URL=
NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY=
48 changes: 48 additions & 0 deletions app/(dashboard)/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import DeleteAccountCard from "@/components/delete-account-card";
import { DashboardHeader } from "@/components/header";
import { DashboardShell } from "@/components/shell";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";

export const metadata = {
title: "Settings",
description: "Your account settings",
};

const emailSettingItems = [
{
id: 1,
label: "Get Email for moments of celebration",
description: "We'll send you an email when you level up or earn a badge.",
},
{
id: 2,
label: "Get Email for new projects",
description: "We'll send you an email when a new project is added that matches your interests.",
},
];

export default async function SettingsPage() {
const handleDeleteAccount = () => {
console.log("Deleting account");
};
return (
<DashboardShell>
<DashboardHeader heading="Settings" text="Your account settings" />
<div className="h-fit w-full space-y-3 rounded-lg bg-zinc-50 p-5 ">
<p className="text-md font-bold">Email Settings </p>
{emailSettingItems.map((item) => (
<div key={item.id} className="flex items-center space-x-2">
<Checkbox id={item.label} />
<label
htmlFor={item.label}
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
{item.label}
</label>
</div>
))}
</div>
<DeleteAccountCard />
</DashboardShell>
);
}
24 changes: 24 additions & 0 deletions components/delete-account-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client";

import { useDeleteAccountModal } from "@/components/delete-modal";
import { Button } from "@/components/ui/button";
import { Delete } from "lucide-react";

export default function DeleteAccountCard() {
const { setShowDeleteAccountModal, DeleteAccountModal } = useDeleteAccountModal();
return (
<div>
<DeleteAccountModal />
<div className="h-fit w-full space-y-3 rounded-lg bg-zinc-50 p-5 ">
<p className="text-md font-bold">Account </p>
<Button
onClick={() => {
setShowDeleteAccountModal(true);
}}
variant="destructive">
Delete Account
</Button>
</div>
</div>
);
}
93 changes: 93 additions & 0 deletions components/delete-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Button } from "@/components/ui/button";
import { Modal } from "@/components/ui/modal";
import { useToast } from "@/components/ui/use-toast";
import { useRouter } from "next/navigation";
import { Dispatch, SetStateAction, useCallback, useMemo, useState } from "react";

function DeleteAccountModal({
showDeleteAccountModal,
setShowDeleteAccountModal,
}: {
showDeleteAccountModal: boolean;
setShowDeleteAccountModal: Dispatch<SetStateAction<boolean>>;
}) {
const router = useRouter();
const [deleting, setDeleting] = useState(false);
const { toast } = useToast();

async function deleteAccount() {
setDeleting(true);
console.log("deleting account");
//TODO: add function that would be responsible for deleting the account
// await fetch(`/api/user`, {
// method: "DELETE",
// headers: {
// "Content-Type": "application/json",
// },
// }).then(async (res) => {
// if (res.status === 200) {
// update();
// // delay to allow for the route change to complete
// await new Promise((resolve) =>
// setTimeout(() => {
// router.push("/register");
// resolve(null);
// }, 200)
// );
// } else {
// setDeleting(false);
// const error = await res.text();
// throw error;
// }
// });
}

return (
<Modal showModal={showDeleteAccountModal} setShowModal={setShowDeleteAccountModal}>
<div className="flex flex-col items-center justify-center space-y-3 border-gray-200 px-4 py-4 pt-8 sm:px-16">
<h3 className="text-lg font-medium">Are you absolutely sure?</h3>
<p className="text-center text-sm text-gray-500">
This action cannot be undone. This will permanently delete your account and remove your data from
our servers.
</p>
</div>

<form
onSubmit={async (e) => {
e.preventDefault();
toast({
title: `deleting`,
description: "Next steps to be built",
});
await deleteAccount();
}}
className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:px-16">
<Button variant="destructive" loading={deleting}>
{" "}
Delete
</Button>
</form>
</Modal>
);
}

export function useDeleteAccountModal() {
const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false);

const DeleteAccountModalCallback = useCallback(() => {
return (
<DeleteAccountModal
showDeleteAccountModal={showDeleteAccountModal}
setShowDeleteAccountModal={setShowDeleteAccountModal}
/>
);
}, [showDeleteAccountModal, setShowDeleteAccountModal]);

return useMemo(
() => ({
setShowDeleteAccountModal,
DeleteAccountModal: DeleteAccountModalCallback,
}),
[setShowDeleteAccountModal, DeleteAccountModalCallback]
);
}
30 changes: 30 additions & 0 deletions components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use client"

import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"

import { cn } from "@/lib/utils"

const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName

export { Checkbox }
73 changes: 73 additions & 0 deletions components/ui/modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"use client";

import { cn } from "@/lib/utils";
import * as Dialog from "@radix-ui/react-dialog";
import { useRouter } from "next/navigation";
import { Dispatch, SetStateAction } from "react";


//inspired by dub.co

export function Modal({
children,
className,
showModal,
setShowModal,
onClose,
desktopOnly,
preventDefaultClose,
}: {
children: React.ReactNode;
className?: string;
showModal?: boolean;
setShowModal?: Dispatch<SetStateAction<boolean>>;
onClose?: () => void;
desktopOnly?: boolean;
preventDefaultClose?: boolean;
}) {
const router = useRouter();

const closeModal = ({ dragged }: { dragged?: boolean } = {}) => {
if (preventDefaultClose && !dragged) {
return;
}
// fire onClose event if provided
onClose && onClose();

// if setShowModal is defined, use it to close modal
if (setShowModal) {
setShowModal(false);
// else, this is intercepting route @modal
} else {
router.back();
}
};


return (
<Dialog.Root
open={setShowModal ? showModal : true}
onOpenChange={(open) => {
if (!open) {
closeModal();
}
}}>
<Dialog.Portal>
<Dialog.Overlay
// for detecting when there's an active opened modal
id="modal-backdrop"
className="animate-fade-in fixed inset-0 z-40 bg-gray-100 bg-opacity-50 backdrop-blur-md"
/>
<Dialog.Content
onOpenAutoFocus={(e) => e.preventDefault()}
onCloseAutoFocus={(e) => e.preventDefault()}
className={cn(
"animate-scale-in fixed inset-0 z-40 m-auto max-h-fit w-full max-w-md overflow-hidden border border-gray-200 bg-white p-0 shadow-xl sm:rounded-2xl",
className
)}>
{children}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
3 changes: 3 additions & 0 deletions env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ export const env = createEnv({
GITHUB_APP_PRIVATE_KEY: z.string().min(1).optional(),
GITHUB_APP_SLUG: z.string().min(1),
GITHUB_APP_ACCESS_TOKEN: z.string().min(1),
TRIGGER_API_KEY: z.string().min(1),
TRIGGER_API_URL: z.string().min(1),
},
client: {
NEXT_PUBLIC_APP_URL: z.string().min(1),
NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY: z.string().min(1).optional(),
},
runtimeEnv: {
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
Expand Down
3 changes: 3 additions & 0 deletions jobs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// export all your job files here

export * from "./issueReminder"
83 changes: 83 additions & 0 deletions jobs/issueReminder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { getOctokitInstance } from "@/lib/github/utils";
import { client } from "@/trigger";
import { eventTrigger } from "@trigger.dev/sdk";
import { z } from "zod";

client.defineJob({
// This is the unique identifier for your Job, it must be unique across all Jobs in your project.
id: "issue-remainder-job",
name: "issue remainder job",
version: "0.0.1",
// This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction
trigger: eventTrigger({
name: "issue.remainder",
schema: z.object({
issueNumber: z.number(),
repo: z.string(),
owner: z.string(),
commenter: z.string(),
installationId: z.number(),
}),
}),
run: async (payload, io, ctx) => {
const { issueNumber, repo, owner, commenter, installationId } = payload;

const octokit = getOctokitInstance(installationId);

//wait for 36hrs
await io.wait("waiting for 36hrs",36 * 60 * 60);

//make this a task so that it can return the completed value from the prev run
await io.runTask("pull-request-checker", async () => {
const { data: issue } = await octokit.issues.get({
owner: owner,
repo: repo,
issue_number: issueNumber,
});

if (issue.pull_request) {
io.logger.info("pull request has been created for the issue after 36hrs");
return;
} else {
//send a comment remainder to the issue
io.logger.info("pull request has not been created for the issue after 36hrs, sending a reminder");
await octokit.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: ` @${commenter}, You have 12hrs left to create a pull request for this issue. `,
});
}
});

await io.wait("waiting for 12hrs",12 * 60 * 60);

await io.runTask("pull-request-checker-after-12hrs", async () => {
const { data: issue } = await octokit.issues.get({
owner: owner,
repo: repo,
issue_number: issueNumber,
});

if (issue.pull_request) {
io.logger.info("pull request has been created for the issue after 12hrs");
return
} else {
io.logger.info("pull request has not been created for the issue after 12hrs");
await octokit.issues.createComment({
owner: owner,
repo: repo,
issue_number: issueNumber,
body: `@${commenter} has been unassigned from the issue, anyone can now take it up.`,
});
await octokit.issues.removeAssignees({
owner: owner,
repo: repo,
issue_number: issueNumber,
assignees: [commenter],
});
}
});

},
});
Loading
Loading