Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,6 @@ For more detailed Docker build instructions and security considerations, see [do

### Calendar integrations

*Note:* The calendar integration feature is a work in progress.

#### Google Calendar

1. Visit: https://console.cloud.google.com/apis/library
Expand All @@ -413,6 +411,25 @@ For more detailed Docker build instructions and security considerations, see [do
2. In `Authorized redirect URIs` add:
- `http://localhost:3000/api/google/calendar/callback`

#### Microsoft Calendar

1. Go to your existing Microsoft Azure app registration (created earlier in the Microsoft OAuth setup)
2. Add the calendar redirect URI:
1. In the "Manage" menu click "Authentication (Preview)"
2. Add the Redirect URI: `http://localhost:3000/api/outlook/calendar/callback`
3. Add calendar permissions:
1. In the "Manage" menu click "API permissions"
2. Click "Add a permission"
3. Select "Microsoft Graph"
4. Select "Delegated permissions"
5. Add the following calendar permissions:
- Calendars.Read
- Calendars.ReadWrite
6. Click "Add permissions"
7. Click "Grant admin consent" if you're an admin

Note: The calendar integration uses a separate OAuth flow from the main email OAuth, so users can connect their calendar independently.

## Contributing to the project

You can view open tasks in our [GitHub Issues](https://github.com/elie222/inbox-zero/issues).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,23 @@ interface CalendarConnectionCardProps {
connection: CalendarConnection;
}

const getProviderInfo = (provider: string) => {
const providers = {
microsoft: {
name: "Microsoft Calendar",
icon: "/images/product/outlook-calendar.svg",
alt: "Microsoft Calendar",
},
google: {
name: "Google Calendar",
icon: "/images/product/google-calendar.svg",
alt: "Google Calendar",
},
};

return providers[provider as keyof typeof providers] || providers.google;
};

export function CalendarConnectionCard({
connection,
}: CalendarConnectionCardProps) {
Expand All @@ -37,6 +54,8 @@ export function CalendarConnectionCard({
Record<string, boolean>
>({});

const providerInfo = getProviderInfo(connection.provider);

const { execute: executeDisconnect, isExecuting: isDisconnecting } =
useAction(disconnectCalendarAction.bind(null, emailAccountId));
const { execute: executeToggle } = useAction(
Expand Down Expand Up @@ -103,14 +122,14 @@ export function CalendarConnectionCard({
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Image
src="/images/product/google-calendar.svg"
alt="Google Calendar"
src={providerInfo.icon}
alt={providerInfo.alt}
width={32}
height={32}
unoptimized
/>
<div>
<CardTitle className="text-lg">Google Calendar</CardTitle>
<CardTitle className="text-lg">{providerInfo.name}</CardTitle>
<CardDescription className="flex items-center gap-2">
{connection.email}
{!connection.isConnected && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function CalendarConnections() {
{connections.length === 0 ? (
<div className="text-center text-muted-foreground py-10">
<p>No calendar connections found.</p>
<p>Connect your Google Calendar to get started.</p>
<p>Connect your Google or Microsoft Calendar to get started.</p>
</div>
) : (
<div className="grid gap-4">
Expand Down
111 changes: 111 additions & 0 deletions apps/web/app/(app)/[emailAccountId]/calendars/ConnectCalendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { useAccount } from "@/providers/EmailAccountProvider";
import { toastError } from "@/components/Toast";
import type { GetCalendarAuthUrlResponse } from "@/app/api/google/calendar/auth-url/route";
import { fetchWithAccount } from "@/utils/fetch";
import { createScopedLogger } from "@/utils/logger";
import Image from "next/image";

export function ConnectCalendar() {
const { emailAccountId } = useAccount();
const [isConnectingGoogle, setIsConnectingGoogle] = useState(false);
const [isConnectingMicrosoft, setIsConnectingMicrosoft] = useState(false);
const logger = createScopedLogger("calendar-connection");

const handleConnectGoogle = async () => {
setIsConnectingGoogle(true);
try {
const response = await fetchWithAccount({
url: "/api/google/calendar/auth-url",
emailAccountId,
init: { headers: { "Content-Type": "application/json" } },
});

if (!response.ok) {
throw new Error("Failed to initiate Google calendar connection");
}

const data: GetCalendarAuthUrlResponse = await response.json();
window.location.href = data.url;
} catch (error) {
logger.error("Error initiating Google calendar connection", {
error,
emailAccountId,
provider: "google",
});
toastError({
title: "Error initiating Google calendar connection",
description: "Please try again or contact support",
});
setIsConnectingGoogle(false);
}
};

const handleConnectMicrosoft = async () => {
setIsConnectingMicrosoft(true);
try {
const response = await fetchWithAccount({
url: "/api/outlook/calendar/auth-url",
emailAccountId,
init: { headers: { "Content-Type": "application/json" } },
});

if (!response.ok) {
throw new Error("Failed to initiate Microsoft calendar connection");
}

const data: GetCalendarAuthUrlResponse = await response.json();
window.location.href = data.url;
} catch (error) {
logger.error("Error initiating Microsoft calendar connection", {
error,
emailAccountId,
provider: "microsoft",
});
toastError({
title: "Error initiating Microsoft calendar connection",
description: "Please try again or contact support",
});
setIsConnectingMicrosoft(false);
}
};

return (
<div className="flex gap-2 flex-wrap md:flex-nowrap">
<Button
onClick={handleConnectGoogle}
disabled={isConnectingGoogle || isConnectingMicrosoft}
variant="outline"
className="flex items-center gap-2 w-full md:w-auto"
>
<Image
src="/images/google.svg"
alt="Google"
width={16}
height={16}
unoptimized
/>
{isConnectingGoogle ? "Connecting..." : "Add Google Calendar"}
</Button>

<Button
onClick={handleConnectMicrosoft}
disabled={isConnectingGoogle || isConnectingMicrosoft}
variant="outline"
className="flex items-center gap-2 w-full md:w-auto"
>
<Image
src="/images/microsoft.svg"
alt="Microsoft"
width={16}
height={16}
unoptimized
/>
{isConnectingMicrosoft ? "Connecting..." : "Add Outlook Calendar"}
</Button>
</div>
);
}

This file was deleted.

6 changes: 3 additions & 3 deletions apps/web/app/(app)/[emailAccountId]/calendars/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { PageWrapper } from "@/components/PageWrapper";
import { PageHeader } from "@/components/PageHeader";
import { CalendarConnections } from "./CalendarConnections";
import { ConnectCalendarButton } from "@/app/(app)/[emailAccountId]/calendars/ConnectCalendarButton";
import { ConnectCalendar } from "@/app/(app)/[emailAccountId]/calendars/ConnectCalendar";

export default function CalendarsPage() {
return (
<PageWrapper>
<div className="flex items-center justify-between gap-4">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3 lg:gap-4">
<PageHeader
title="Calendars"
description="Connect your calendar to allow our AI to suggest meeting times based on your availability when drafting replies."
/>
<ConnectCalendarButton />
<ConnectCalendar />
</div>
<div className="mt-6">
<CalendarConnections />
Expand Down
Loading
Loading