Skip to content
Merged
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
8 changes: 4 additions & 4 deletions app/(api)/api/jobs/user/mark-for-deletion/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { clerkClient } from "@clerk/nextjs/server";
import { serve } from "@upstash/workflow/nextjs";
import { and, eq, isNull, lte } from "drizzle-orm";
import { Resend } from "resend";
import {
SevenDayWarning,
sevenDayWarningPlainText,
Expand All @@ -8,10 +12,6 @@ import {
} from "@/components/emails/thirty-day-deletion-notice";
import { opsUser } from "@/ops/drizzle/schema";
import { getOpsDatabase } from "@/ops/useOps";
import { clerkClient } from "@clerk/nextjs/server";
import { serve } from "@upstash/workflow/nextjs";
import { and, eq, isNull, lte } from "drizzle-orm";
import { Resend } from "resend";

async function getUserDetails(user: {
id: string;
Expand Down
8 changes: 4 additions & 4 deletions app/(api)/api/webhook/auth/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { verifyWebhook } from "@clerk/nextjs/webhooks";
import { eq } from "drizzle-orm";
import type { NextRequest } from "next/server";
import { Resend } from "resend";
import {
AccountDeleted,
accountDeletedPlainText,
Expand All @@ -7,10 +11,6 @@ import { deleteDatabase } from "@/lib/utils/useDatabase";
import { triggerBlobDeletionWorkflow } from "@/lib/utils/workflow";
import { opsOrganization, opsUser } from "@/ops/drizzle/schema";
import { getOpsDatabase } from "@/ops/useOps";
import { verifyWebhook } from "@clerk/nextjs/webhooks";
import { eq } from "drizzle-orm";
import type { NextRequest } from "next/server";
import { Resend } from "resend";

type ClerkOrgData = {
createdBy?: {
Expand Down
29 changes: 19 additions & 10 deletions app/(dashboard)/[tenant]/projects/[projectId]/events/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
"use client";

import { useSession } from "@clerk/nextjs";
import { Title } from "@radix-ui/react-dialog";
import { useQuery } from "@tanstack/react-query";
import { RssIcon, Upload } from "lucide-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { parseAsBoolean, useQueryState } from "nuqs";
import { useMemo, useState } from "react";
import { ImportIcsDialog } from "@/components/core/import-ics-dialog";
import { SpinnerWithSpacing } from "@/components/core/loaders";
import { Panel } from "@/components/core/panel";
import PageSection from "@/components/core/section";
Expand All @@ -11,14 +20,6 @@ import { Button, buttonVariants } from "@/components/ui/button";
import type { EventWithCreator } from "@/drizzle/types";
import { toDateString, toUTC } from "@/lib/utils/date";
import { useTRPC } from "@/trpc/client";
import { useSession } from "@clerk/nextjs";
import { Title } from "@radix-ui/react-dialog";
import { useQuery } from "@tanstack/react-query";
import { RssIcon } from "lucide-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { parseAsBoolean, useQueryState } from "nuqs";
import { useMemo, useState } from "react";

export default function Events() {
const { session } = useSession();
Expand Down Expand Up @@ -68,13 +69,21 @@ export default function Events() {
<span className="inline-flex space-x-1">
<Link
href={calendarSubscriptionUrl}
className={buttonVariants({ variant: "link" })}
className={buttonVariants({ variant: "outline" })}
>
<RssIcon className="mr-2 h-5 w-5" />
<RssIcon className="mr-1 h-5 w-5" />
Calendar Subscription
</Link>
</span>
</div>
<div className="isolate inline-flex sm:space-x-3">
<ImportIcsDialog projectId={+projectId!}>
<Button variant="outline">
<Upload className="mr-1 h-5 w-5" />
Import Event
</Button>
</ImportIcsDialog>
</div>
</div>
</PageSection>

Expand Down
261 changes: 132 additions & 129 deletions bun.lock

Large diffs are not rendered by default.

213 changes: 213 additions & 0 deletions components/core/import-ics-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
"use client";

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { AlertCircle, CheckCircle, FileIcon, Upload } from "lucide-react";
import { useState } from "react";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { displayMutationError } from "@/lib/utils/error";
import { useTRPC } from "@/trpc/client";
import { Spinner } from "./loaders";

interface ImportResult {
success: boolean;
imported: number;
total: number;
errors?: string[];
}

interface ImportIcsDialogProps {
projectId: number;
children?: React.ReactNode;
}

export function ImportIcsDialog({ projectId, children }: ImportIcsDialogProps) {
const [open, setOpen] = useState(false);
const [file, setFile] = useState<File | null>(null);
const [result, setResult] = useState<ImportResult | null>(null);

const queryClient = useQueryClient();
const trpc = useTRPC();

const importMutation = useMutation(
trpc.events.importFromIcs.mutationOptions({
onSuccess: (data) => {
setResult(data);
// Invalidate all event-related queries
queryClient.invalidateQueries({
predicate: (query) => {
return (
query.queryKey[0] === "events" ||
(Array.isArray(query.queryKey) &&
query.queryKey.includes("getTodayData"))
);
},
});
},
onError: displayMutationError,
}),
);

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0] || null;
setFile(selectedFile);
setResult(null);
importMutation.reset();
};

const handleImport = async () => {
if (!file) return;

if (!file.name.endsWith(".ics")) {
// Handle error - could set a local error state or use notification
return;
}

try {
const icsContent = await file.text();
importMutation.mutate({
projectId,
icsContent,
});
} catch (err) {
console.error("Failed to read file:", err);
}
};

const handleClose = () => {
setOpen(false);
setFile(null);
setResult(null);
importMutation.reset();
};

const renderResult = () => {
if (importMutation.error) {
return (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{importMutation.error.message}</AlertDescription>
</Alert>
);
}

if (result) {
return (
<div className="space-y-3">
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription>
Successfully imported {result.imported} out of {result.total}{" "}
events.
</AlertDescription>
</Alert>

{result.errors && result.errors.length > 0 && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<div className="space-y-1">
<p>Some events could not be imported:</p>
<ul className="list-disc list-inside text-sm">
{result.errors.slice(0, 3).map((error) => (
<li key={error.substring(0, 20)} className="truncate">
{error}
</li>
))}
{result.errors.length > 3 && (
<li>... and {result.errors.length - 3} more</li>
)}
</ul>
</div>
</AlertDescription>
</Alert>
)}
</div>
);
}

return null;
};

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{children || (
<Button variant="outline" size="sm">
<Upload className="mr-2 h-4 w-4" />
Import Event (ICS file)
</Button>
)}
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Import Calendar Events</DialogTitle>
<DialogDescription>
Upload an ICS file to import calendar events into this project.
</DialogDescription>
</DialogHeader>

<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="ics-file">Select ICS File</Label>
<div className="flex items-center space-x-2">
<Input
id="ics-file"
type="file"
accept=".ics"
onChange={handleFileChange}
className="flex-1"
/>
</div>
{file && (
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
<FileIcon className="h-4 w-4" />
<span>{file.name}</span>
<span>({Math.round(file.size / 1024)} KB)</span>
</div>
)}
</div>

{renderResult()}
</div>

<DialogFooter>
{result ? (
<Button onClick={handleClose}>Close</Button>
) : (
<>
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button
onClick={handleImport}
disabled={!file || importMutation.isPending}
className="min-w-[100px]"
>
{importMutation.isPending ? (
<Spinner className="text-secondary" message="Importing..." />
) : (
<>
<Upload className="mr-2 h-4 w-4" />
Import
</>
)}
</Button>
</>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}
5 changes: 5 additions & 0 deletions instrumentation-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,

// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,
// Enable logs to be sent to Sentry
enableLogs: true,

// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});
Expand Down
Loading