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
91 changes: 3 additions & 88 deletions apps/web/src/app/campaigns/email/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import { useState, useEffect } from "react";
import { useParams, useRouter } from "next/navigation";
import { useQuery, useMutation } from "convex/react";
import { makeFunctionReference } from "convex/server";
import { appConfirm } from "@/lib/appConfirm";
import { AppLayout } from "@/components/AppLayout";
import { Button, Input } from "@opencom/ui";
Expand All @@ -13,98 +11,15 @@ import type { Id } from "@opencom/convex/dataModel";
import { useAuth } from "@/contexts/AuthContext";
import { AudienceRuleBuilder, type AudienceRule } from "@/components/AudienceRuleBuilder";
import { sanitizeHtml } from "@/lib/sanitizeHtml";

type EmailCampaignRecord = {
_id: Id<"emailCampaigns">;
name: string;
subject: string;
previewText?: string;
content: string;
status: string;
audienceRules?: AudienceRule | null;
targeting?: AudienceRule | null;
};

type EmailCampaignStats = {
total: number;
pending: number;
sent: number;
delivered: number;
opened: number;
clicked: number;
bounced: number;
unsubscribed: number;
openRate: number;
clickRate: number;
bounceRate: number;
};

type UpdateCampaignArgs = {
id: Id<"emailCampaigns">;
name?: string;
subject?: string;
previewText?: string;
content?: string;
templateId?: Id<"emailTemplates">;
senderId?: Id<"users">;
targeting?: AudienceRule;
schedule?: {
type: "immediate" | "scheduled";
scheduledAt?: number;
timezone?: string;
};
};

type SendCampaignArgs = {
id: Id<"emailCampaigns">;
};

type SendCampaignResult = {
recipientCount: number;
};

const CAMPAIGN_QUERY = makeFunctionReference<
"query",
{ id: Id<"emailCampaigns"> },
EmailCampaignRecord | null
>("emailCampaigns:get");

const CAMPAIGN_STATS_QUERY = makeFunctionReference<
"query",
{ id: Id<"emailCampaigns"> },
EmailCampaignStats
>("emailCampaigns:getStats");

const EVENT_NAMES_QUERY = makeFunctionReference<
"query",
{ workspaceId: Id<"workspaces"> },
string[]
>("events:getDistinctNames");

const UPDATE_CAMPAIGN_REF = makeFunctionReference<
"mutation",
UpdateCampaignArgs,
Id<"emailCampaigns">
>("emailCampaigns:update");

const SEND_CAMPAIGN_REF = makeFunctionReference<"mutation", SendCampaignArgs, SendCampaignResult>(
"emailCampaigns:send"
);
import { useEmailCampaignEditorConvex } from "../../hooks/useEmailCampaignEditorConvex";

function EmailCampaignEditor() {
const params = useParams();
const router = useRouter();
const campaignId = params.id as Id<"emailCampaigns">;
const { activeWorkspace } = useAuth();

const campaign = useQuery(CAMPAIGN_QUERY, { id: campaignId });
const stats = useQuery(CAMPAIGN_STATS_QUERY, { id: campaignId });
const eventNames = useQuery(
EVENT_NAMES_QUERY,
activeWorkspace?._id ? { workspaceId: activeWorkspace._id } : "skip"
);
const updateCampaign = useMutation(UPDATE_CAMPAIGN_REF);
const sendCampaign = useMutation(SEND_CAMPAIGN_REF);
const { campaign, eventNames, sendCampaign, stats, updateCampaign } =
useEmailCampaignEditorConvex(campaignId, activeWorkspace?._id);

const [name, setName] = useState("");
const [subject, setSubject] = useState("");
Expand Down
85 changes: 85 additions & 0 deletions apps/web/src/app/campaigns/hooks/useEmailCampaignEditorConvex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"use client";

import type { Id } from "@opencom/convex/dataModel";
import type { AudienceRule } from "@/components/AudienceRuleBuilder";
import {
useWebMutation,
useWebQuery,
webMutationRef,
webQueryRef,
} from "@/lib/convex/hooks";

type WorkspaceArgs = {
workspaceId: Id<"workspaces">;
};

type CampaignArgs = {
id: Id<"emailCampaigns">;
};

type UpdateCampaignArgs = {
id: Id<"emailCampaigns">;
name?: string;
subject?: string;
previewText?: string;
content?: string;
templateId?: Id<"emailTemplates">;
senderId?: Id<"users">;
targeting?: AudienceRule;
schedule?: {
type: "immediate" | "scheduled";
scheduledAt?: number;
timezone?: string;
};
};

const CAMPAIGN_QUERY_REF = webQueryRef<
CampaignArgs,
{
_id: Id<"emailCampaigns">;
name: string;
subject: string;
previewText?: string;
content: string;
status: string;
audienceRules?: AudienceRule | null;
targeting?: AudienceRule | null;
} | null
>("emailCampaigns:get");
const CAMPAIGN_STATS_QUERY_REF = webQueryRef<
CampaignArgs,
{
total: number;
pending: number;
sent: number;
delivered: number;
opened: number;
clicked: number;
bounced: number;
unsubscribed: number;
openRate: number;
clickRate: number;
bounceRate: number;
}
>("emailCampaigns:getStats");
const EVENT_NAMES_QUERY_REF = webQueryRef<WorkspaceArgs, string[]>("events:getDistinctNames");
const UPDATE_CAMPAIGN_REF = webMutationRef<UpdateCampaignArgs, Id<"emailCampaigns">>(
"emailCampaigns:update"
);
const SEND_CAMPAIGN_REF = webMutationRef<
{ id: Id<"emailCampaigns"> },
{ recipientCount: number }
>("emailCampaigns:send");

export function useEmailCampaignEditorConvex(
campaignId: Id<"emailCampaigns">,
workspaceId?: Id<"workspaces"> | null
) {
return {
campaign: useWebQuery(CAMPAIGN_QUERY_REF, { id: campaignId }),
eventNames: useWebQuery(EVENT_NAMES_QUERY_REF, workspaceId ? { workspaceId } : "skip"),
sendCampaign: useWebMutation(SEND_CAMPAIGN_REF),
stats: useWebQuery(CAMPAIGN_STATS_QUERY_REF, { id: campaignId }),
updateCampaign: useWebMutation(UPDATE_CAMPAIGN_REF),
};
}
69 changes: 9 additions & 60 deletions apps/web/src/app/help/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,21 @@
"use client";

import { useParams } from "next/navigation";
import { useQuery, useMutation } from "convex/react";
import { makeFunctionReference } from "convex/server";
import type { Id } from "@opencom/convex/dataModel";
import { useAuthOptional } from "@/contexts/AuthContext";
import { Button } from "@opencom/ui";
import { ArrowLeft, ThumbsUp, ThumbsDown, MessageCircle } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { parseMarkdown } from "@/lib/parseMarkdown";

const publicWorkspaceContextQuery = makeFunctionReference<
"query",
Record<string, never>,
{ _id?: Id<"workspaces">; helpCenterAccessPolicy?: string } | null
>("workspaces:getPublicWorkspaceContext");

const articleBySlugQuery = makeFunctionReference<
"query",
{ slug: string; workspaceId: Id<"workspaces"> },
{
_id: Id<"articles">;
slug: string;
title: string;
content: string;
renderedContent?: string;
status?: string;
visibility?: string;
collectionId?: Id<"collections">;
} | null
>("articles:get");

const collectionGetQuery = makeFunctionReference<
"query",
{ id: Id<"collections"> },
{ _id: Id<"collections">; slug?: string; name: string } | null
>("collections:get");

const articleFeedbackStatsQuery = makeFunctionReference<
"query",
{ articleId: Id<"articles"> },
{ helpful: number; total: number } | null
>("articles:getFeedbackStats");

const submitArticleFeedbackRef = makeFunctionReference<
"mutation",
{ articleId: Id<"articles">; helpful: boolean },
null
>("articles:submitFeedback");
import {
useHelpArticlePageConvex,
useHelpWorkspaceContextConvex,
} from "../hooks/useHelpCenterConvex";

export default function ArticlePage() {
const params = useParams();
const auth = useAuthOptional();
const workspaceContext = useQuery(publicWorkspaceContextQuery, {});
const { workspaceContext } = useHelpWorkspaceContextConvex();
const slug = params.slug as string;
const [feedbackSubmitted, setFeedbackSubmitted] = useState(false);
const [renderedContent, setRenderedContent] = useState("");
Expand All @@ -63,25 +25,12 @@ export default function ArticlePage() {
const isRestricted =
!isAuthenticated && workspaceContext?.helpCenterAccessPolicy === "restricted";
const shouldFetchArticle = Boolean(workspaceId && !isRestricted);

const article = useQuery(
articleBySlugQuery,
shouldFetchArticle && workspaceId ? { slug, workspaceId } : "skip"
const { article, collection, feedbackStats, submitFeedback } = useHelpArticlePageConvex(
slug,
shouldFetchArticle && workspaceId ? workspaceId : undefined
);
const publicArticleId =
article && article.visibility !== "internal" ? (article._id as Id<"articles">) : null;

const collection = useQuery(
collectionGetQuery,
article?.collectionId ? { id: article.collectionId } : "skip"
);

const feedbackStats = useQuery(
articleFeedbackStatsQuery,
publicArticleId ? { articleId: publicArticleId } : "skip"
);

const submitFeedback = useMutation(submitArticleFeedbackRef);
article && article.visibility !== "internal" ? article._id : null;

const handleFeedback = async (helpful: boolean) => {
if (!publicArticleId || feedbackSubmitted) return;
Expand Down
Loading
Loading