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
41 changes: 29 additions & 12 deletions apps/landing/src/app/features/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,20 @@ import {
} from "lucide-react";
import { Section, SectionHeader, FeatureCard, Screenshot, CTA } from "@/components/sections";
import { OPENCOM_GITHUB_DOCS_URL, OPENCOM_HOSTED_ONBOARDING_URL } from "@/lib/links";
import { createLandingPageMetadata } from "@/lib/metadata";

export const metadata: Metadata = createLandingPageMetadata({
import { InboxGraphic } from "@/components/landing/graphics/inbox-graphic";
import { ToursGraphic } from "@/components/landing/graphics/tours-graphic";
import { OutboundGraphic } from "@/components/landing/graphics/outbound-graphic";
import { TicketsGraphic } from "@/components/landing/graphics/tickets-graphic";
import { SurveysGraphic } from "@/components/landing/graphics/surveys-graphic";
import { CampaignsGraphic } from "@/components/landing/graphics/campaigns-graphic";
import { ReportsGraphic } from "@/components/landing/graphics/reports-graphic";

export const metadata: Metadata = {
title: "Features | Opencom",
description:
"Explore Opencom features across chat, inbox, tours, knowledge base, tickets, surveys, campaigns, reports, and mobile SDKs.",
path: "/features",
});
};

const featureCategories = [
{
Expand All @@ -35,7 +41,7 @@ const featureCategories = [
description:
"Real-time customer conversations with a shared team inbox and embeddable chat widget.",
icon: MessageCircle,
screenshot: "/screenshots/web-inbox.png",
Graphic: InboxGraphic,
features: [
"Real-time messaging with typing indicators",
"Shared team inbox with snooze and assignment",
Expand All @@ -50,7 +56,7 @@ const featureCategories = [
title: "Product Tours",
description: "Guide users through your product with interactive walkthroughs.",
icon: Map,
screenshot: "/screenshots/web-tours.png",
Graphic: ToursGraphic,
features: [
"Step-by-step guided tours",
"Pointer steps and post steps",
Expand Down Expand Up @@ -80,7 +86,7 @@ const featureCategories = [
title: "Outbound Messages",
description: "Proactively engage users with in-app chats, posts, and banners.",
icon: Send,
screenshot: "/screenshots/web-outbound.png",
Graphic: OutboundGraphic,
features: [
"In-app chat messages",
"Post announcements",
Expand All @@ -95,7 +101,7 @@ const featureCategories = [
title: "Tickets",
description: "Customer support ticketing with priorities, statuses, and custom forms.",
icon: Ticket,
screenshot: "/screenshots/web-tickets.png",
Graphic: TicketsGraphic,
features: [
"Priority levels (Urgent, High, Normal, Low)",
"Status tracking (Submitted, In Progress, Resolved)",
Expand All @@ -110,7 +116,7 @@ const featureCategories = [
title: "Surveys",
description: "Collect feedback and measure customer sentiment.",
icon: ClipboardCheck,
screenshot: "/screenshots/web-surveys.png",
Graphic: SurveysGraphic,
features: [
"NPS surveys",
"Custom satisfaction surveys",
Expand All @@ -125,7 +131,7 @@ const featureCategories = [
title: "Campaigns",
description: "Orchestrate multi-channel outreach campaigns.",
icon: Mail,
screenshot: "/screenshots/web-campaigns.png",
Graphic: CampaignsGraphic,
features: [
"Email campaigns with templates",
"Push notifications",
Expand Down Expand Up @@ -170,7 +176,7 @@ const featureCategories = [
title: "Reports & Analytics",
description: "Analytics and insights for your support operations.",
icon: BarChart3,
screenshot: "/screenshots/web-reports.png",
Graphic: ReportsGraphic,
features: [
"Conversation volume metrics",
"Response and resolution times",
Expand Down Expand Up @@ -257,7 +263,18 @@ export default function FeaturesPage() {
</ul>
</div>
<div className={index % 2 === 1 ? "lg:order-1" : ""}>
<Screenshot src={category.screenshot} alt={`${category.title} screenshot`} />
{category.Graphic ? (
<div className="rounded-[2.5rem] bg-[#f9fafb] dark:bg-card border border-slate-200/50 dark:border-white/5 shadow-[0_40px_80px_-20px_rgba(0,0,0,0.1)] dark:shadow-[0_40px_80px_-20px_rgba(0,0,0,0.5)] p-2 relative overflow-hidden">
{/* Subtle inner glow */}
<div className="absolute inset-0 rounded-[2.5rem] shadow-[inset_0_1px_0_rgba(255,255,255,0.8)] dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.05)] pointer-events-none z-10" />

<div className="rounded-[2rem] overflow-hidden border border-border/50 bg-muted/20 dark:bg-black relative aspect-[16/10]">
<category.Graphic />
</div>
</div>
) : (
<Screenshot src={category.screenshot} alt={`${category.title} screenshot`} />
)}
</div>
</div>
</Section>
Expand Down
55 changes: 18 additions & 37 deletions apps/landing/src/components/landing/features.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import { motion, Variants } from "framer-motion";
import { memo } from "react";
import {
ChatsCircle,
MapTrifold,
Expand All @@ -11,6 +10,13 @@ import {
ChartLineUp,
} from "@phosphor-icons/react";

import { InboxGraphic } from "./graphics/inbox-graphic";
import { ToursGraphic } from "./graphics/tours-graphic";
import { TicketsGraphic } from "./graphics/tickets-graphic";
import { AIAgentGraphic } from "./graphics/ai-graphic";
import { CampaignsGraphic } from "./graphics/campaigns-graphic";
import { ReportsGraphic } from "./graphics/reports-graphic";

const container: Variants = {
hidden: { opacity: 0 },
show: {
Expand All @@ -24,70 +30,48 @@ const item: Variants = {
show: { opacity: 1, y: 0, transition: { type: "spring", stiffness: 80, damping: 20 } },
};

// Perpetual Micro-Interaction for Icon Cards
const PerpetualIcon = memo(function PerpetualIcon({
icon: Icon,
delay = 0,
}: {
icon: React.ElementType;
delay?: number;
}) {
return (
<motion.div
animate={{
y: [0, -8, 0],
scale: [1, 1.05, 1],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut",
delay,
}}
className="relative flex items-center justify-center w-24 h-24 rounded-full bg-gradient-to-br from-primary/10 to-transparent"
>
<div className="absolute inset-0 rounded-full border border-primary/20 blur-sm" />
<Icon weight="duotone" className="w-10 h-10 text-primary" />
</motion.div>
);
});

const features = [
{
title: "Shared Inbox",
description:
"Multi-channel support inbox tailored for modern teams. Route, assign, and resolve effortlessly.",
icon: ChatsCircle,
Graphic: InboxGraphic,
},
{
title: "Product Tours",
description:
"Guide users through your app with native, beautiful onboarding tours that drive activation.",
icon: MapTrifold,
Graphic: ToursGraphic,
},
{
title: "Support Tickets",
description:
"Track complex issues alongside real-time chat. Seamlessly convert conversations to tickets.",
icon: Ticket,
Graphic: TicketsGraphic,
},
{
title: "AI Agent",
description:
"Deploy an intelligent agent trained on your docs to instantly resolve common queries 24/7.",
icon: Robot,
Graphic: AIAgentGraphic,
},
{
title: "Outbound Campaigns",
description:
"Trigger targeted in-app messages and emails based on user behavior and segment rules.",
icon: Megaphone,
Graphic: CampaignsGraphic,
},
{
title: "Analytics",
description:
"Deep insights into team performance, resolution times, and customer satisfaction metrics.",
icon: ChartLineUp,
Graphic: ReportsGraphic,
},
];

Expand Down Expand Up @@ -147,15 +131,12 @@ export function Features() {
{/* Bento Container */}
<div className="relative w-full aspect-[4/3] rounded-[2.5rem] bg-white dark:bg-card border border-slate-200/50 dark:border-white/5 shadow-[0_20px_40px_-15px_rgba(0,0,0,0.05)] dark:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.4)] mb-8 overflow-hidden flex items-center justify-center transition-transform duration-500 hover:scale-[1.02]">
{/* Subtle inner glow */}
<div className="absolute inset-0 rounded-[2.5rem] shadow-[inset_0_1px_0_rgba(255,255,255,0.5)] dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.05)] pointer-events-none" />

{/* Abstract background blobs */}
<div className="absolute inset-0 opacity-20 dark:opacity-10">
<div className="absolute top-0 right-0 w-32 h-32 bg-primary/30 blur-[40px] rounded-full" />
<div className="absolute bottom-0 left-0 w-32 h-32 bg-primary/20 blur-[40px] rounded-full" />
<div className="absolute inset-0 rounded-[2.5rem] shadow-[inset_0_1px_0_rgba(255,255,255,0.5)] dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.05)] pointer-events-none z-10" />

{/* Full bleed graphic */}
<div className="absolute inset-[2px] rounded-[2.4rem] overflow-hidden bg-muted/10">
<feature.Graphic />
</div>

<PerpetualIcon icon={feature.icon} delay={i * 0.2} />
</div>

{/* External Labels (Gallery Style) */}
Expand Down
93 changes: 93 additions & 0 deletions apps/landing/src/components/landing/graphics/ai-graphic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"use client";

import { motion } from "framer-motion";
import { Robot, Sparkle, ChatCircle, Books } from "@phosphor-icons/react";

export function AIAgentGraphic() {
return (
<div className="relative w-full h-full bg-muted/10 dark:bg-[#0a0a0a] overflow-hidden flex items-center justify-center font-sans p-8">
{/* Background Data Nodes */}
<div className="absolute inset-0 opacity-20 pointer-events-none p-6 flex flex-wrap gap-4 items-center justify-center">
{[...Array(12)].map((_, i) => (
<motion.div
key={i}
animate={{
y: [0, Math.random() * 10 - 5, 0],
opacity: [0.3, 0.7, 0.3]
}}
transition={{
duration: 3 + Math.random() * 2,
repeat: Infinity,
delay: Math.random() * 2
}}
className="w-12 h-8 rounded-md bg-border border border-border/50 flex items-center justify-center"
>
<div className="w-6 h-1 rounded-full bg-muted-foreground/30" />
</motion.div>
))}
</div>

<div className="relative z-10 w-full max-w-sm">
{/* Connection Lines */}
<svg className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-64 h-64 -z-10 opacity-30" viewBox="0 0 100 100">
<motion.path
d="M50,50 L20,20 M50,50 L80,20 M50,50 L20,80 M50,50 L80,80"
stroke="currentColor"
strokeWidth="1"
className="text-primary"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 2, repeat: Infinity, repeatType: "reverse" }}
/>
</svg>

{/* Central Brain/Agent */}
<motion.div
animate={{ y: [0, -5, 0] }}
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
className="mx-auto w-24 h-24 rounded-2xl bg-gradient-to-br from-primary to-primary/60 p-[2px] shadow-2xl shadow-primary/30 mb-8 relative"
>
<div className="absolute -inset-4 bg-primary/20 blur-xl rounded-full" />
<div className="w-full h-full bg-card rounded-2xl flex items-center justify-center relative overflow-hidden">
<div className="absolute inset-0 bg-primary/10" />
<Robot weight="duotone" className="w-12 h-12 text-primary relative z-10" />
<Sparkle weight="fill" className="absolute top-3 right-3 w-4 h-4 text-primary animate-pulse" />
</div>
</motion.div>

{/* Agent Output Simulation */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="bg-card border border-border/50 rounded-2xl shadow-xl overflow-hidden"
>
<div className="p-3 border-b border-border/50 bg-muted/20 flex items-center gap-2">
<ChatCircle weight="fill" className="w-4 h-4 text-muted-foreground" />
<span className="text-xs font-semibold">User: How do I setup SSO?</span>
</div>
<div className="p-4 bg-background">
<div className="flex items-start gap-3">
<div className="w-6 h-6 rounded bg-primary/10 flex items-center justify-center shrink-0">
<Robot weight="fill" className="w-3 h-3 text-primary" />
</div>
<div className="space-y-2 flex-1">
<div className="flex items-center gap-2 mb-1">
<div className="h-2 w-full bg-primary/20 rounded" />
</div>
<div className="h-2 w-5/6 bg-muted-foreground/20 rounded" />
<div className="h-2 w-4/6 bg-muted-foreground/20 rounded" />

{/* Source Citation */}
<div className="mt-3 inline-flex items-center gap-1.5 px-2 py-1 rounded bg-muted/50 border border-border/50">
<Books weight="duotone" className="w-3 h-3 text-muted-foreground" />
<span className="text-[9px] font-mono text-muted-foreground">docs/sso-setup.md</span>
</div>
</div>
</div>
</div>
</motion.div>
</div>
</div>
);
}
Loading
Loading