Skip to content
Open
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
1 change: 1 addition & 0 deletions public/file.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/globe.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/next.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/vercel.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/window.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 49 additions & 0 deletions src/app/api/monite-token/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { NextResponse } from "next/server"; // Utility to create HTTP responses in Next.js API routes

// POST handler for generating a Monite API token
export async function POST() {
// Load configuration from environment variables (with defaults for base URL & version)
const base = process.env.MONITE_API_BASE || "https://api.sandbox.monite.com/v1";
const version = process.env.MONITE_API_VERSION || "2024-05-25";
const clientId = process.env.MONITE_CLIENT_ID;
const clientSecret = process.env.MONITE_CLIENT_SECRET;
const entityUserId = process.env.MONITE_ENTITY_USER_ID;

// Validate required environment variables
if (!clientId || !clientSecret || !entityUserId) {
return NextResponse.json(
{ error: "Missing Monite env vars" }, // Error message
{ status: 400 } // Bad Request
);
}

// Request token from Monite API
const resp = await fetch(`${base}/auth/token`, {
method: "POST",
headers: {
"content-type": "application/json", // Tell API we're sending JSON
"x-monite-version": version, // Specify API version
},
body: JSON.stringify({
grant_type: "entity_user", // Grant type for entity user authentication
client_id: clientId,
client_secret: clientSecret,
entity_user_id: entityUserId,
}),
cache: "no-store", // Ensure token request is never cached
});

// Handle token request failure
if (!resp.ok) {
const text = await resp.text(); // Get raw response text for debugging
return NextResponse.json(
{ error: "Token request failed", details: text }, // Include error details
{ status: 400 }
);
}

// Parse successful token response
const data = await resp.json();
console.log("Token request successful", data); // Debug log
return NextResponse.json(data); // Return token payload to client
}
76 changes: 76 additions & 0 deletions src/app/components/MoniteComp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use client'; // Next.js directive indicating this component should run on the client side

import React from "react";
import {
MoniteProvider, // Provider that sets up Monite SDK context
Payables, // Component for displaying/managing payables
Receivables, // Component for displaying/managing receivables
ApprovalPolicies, // Component for managing approval policies
ApprovalRequests, // Component for managing approval requests
Counterparts, // Component for managing business counterparts
Products, // Component for managing products
Tags, // Component for managing tags
UserRoles, // Component for managing user roles
Onboarding // Component for onboarding flows
} from "@monite/sdk-react";

// Define a TypeScript union type for all supported Monite view options
export type MoniteView =
"payables" |
"counterparts" |
"receivables" |
"approval_policies" |
"approval_requests" |
"products" |
"tags" |
"user_roles" |
"onboarding";

// Main Monite component
export default function MoniteComp({ view }: { view: MoniteView }) {

console.log(view); // Debug log: shows which view was requested

// Monite API credentials/config (replace with dynamic values in production)
const entityId = "15238f3f-b306-4b28-996c-6dcbfa784bbd";
const apiBaseUrl = "https://api.sandbox.monite.com/v1";
console.log(entityId); // Debug log: shows entity ID being used

// Async function to retrieve an access token from our backend API route
const fetchToken = async () => {
const res = await fetch("/api/monite-token", {
method: "POST",
headers: { "Content-Type": "application/json" },
cache: "no-store", // Always fetch a fresh token
});
if (!res.ok) throw new Error("Failed to fetch Monite token");
return res.json(); // Return token JSON payload
};

// Monite configuration object to pass to the provider
const monite = {
apiUrl: apiBaseUrl,
entityId,
fetchToken,
};

// Default widget is Payables; overridden based on the `view` prop
let Widget: React.ReactNode = <Payables />;
if (view === "counterparts") Widget = <Counterparts />;
if (view === "receivables") Widget = <Receivables />;
if (view === "products") Widget = <Products />;
if (view === "approval_policies") Widget = <ApprovalPolicies />;
if (view === "approval_requests") Widget = <ApprovalRequests />;
if (view === "tags") Widget = <Tags />;
if (view === "user_roles") Widget = <UserRoles />;
if (view === "onboarding") Widget = <Onboarding />;

// Render Monite widget inside MoniteProvider for proper context
return (
<div className="monite-scope">
<MoniteProvider monite={monite} locale={{ code: "en-US" }} theme={{}}>
{Widget}
</MoniteProvider>
</div>
);
}
60 changes: 60 additions & 0 deletions src/app/components/MoniteDash.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// app/components/MoniteDashboard.tsx
'use client'; // Next.js directive to run this component on the client side

import { useState } from "react"; // React hook for managing component state
import styles from "../page.module.css"; // Import CSS module for scoped styling
import MoniteComp, { type MoniteView } from "./MoniteComp"; // Monite component and its view type

// Navigation configuration: defines available Monite views and their labels
const NAV: { id: MoniteView; label: string }[] = [
// You can extend this list with additional views in the future:
// { id: "invoices", label: "Invoices" },
{ id: "payables", label: "Payables" },
{ id: "counterparts", label: "Counterparts" },
{ id: "receivables", label: "Receivables" },
{ id: "products", label: "Products & services" },
{ id: "approval_policies", label: "Approval Policies" },
{ id: "approval_requests", label: "Approval Requests" },
{ id: "user_roles", label: "User Roles" },
{ id: "tags", label: "Tags" },
{ id: "onboarding", label: "Onboarding" }
];

// Main dashboard component
export default function MoniteDashboard() {
// State to track which Monite view is currently selected
const [selected, setSelected] = useState<MoniteView>("payables");

return (
<>
{/* Sidebar navigation */}
<aside className={styles.sidebar}>
{/* Sidebar header */}
<div className={styles.sidebarHeader}>Monite React SDK Components</div>

{/* Navigation buttons for each available view */}
<nav className={styles.nav}>
{NAV.map((item) => (
<button
key={item.id} // Unique key for React's rendering
type="button"
onClick={() => setSelected(item.id)} // Change selected view on click
className={
selected === item.id
? `${styles.navItem} ${styles.navItemActive}` // Highlight active item
: styles.navItem
}
>
{item.label} {/* Display label from NAV config */}
</button>
))}
</nav>
</aside>

{/* Main content area showing the selected Monite component */}
<main className={styles.main}>
<MoniteComp view={selected} /> {/* Render MoniteComp for the selected view */}
</main>
</>
);
}
Binary file added src/app/favicon.ico
Binary file not shown.
56 changes: 56 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* ===== Monite table look (scoped) ===== */
:root {
--monite-table-border: #e5e7eb;
--monite-table-header-bg: #f9fafb;
--monite-table-row-hover: #f3f4f6;
--monite-muted: #6b7280;
}

@media (prefers-color-scheme: dark) {
:root {
--monite-table-border: #2a2a2a;
--monite-table-header-bg: #111214;
--monite-table-row-hover: #131417;
--monite-muted: #9ca3af;
}
}

/* Scope all styles to your Monite area */
.monite-scope {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}

/* If the SDK renders native <table> elements, these will apply */
.monite-scope table {
width: 100%;
border-collapse: collapse;
border: 1px solid var(--monite-table-border);
background: var(--background);
border-radius: 12px; /* browsers that respect rounded tables */
}

.monite-scope th,
.monite-scope td {
padding: 12px 16px;
border-bottom: 1px solid var(--monite-table-border);
vertical-align: middle;
}

.monite-scope thead th {
background: var(--monite-table-header-bg);
text-align: left;
font-weight: 600;
position: sticky;
top: 0;
z-index: 1;
color: var(--foreground);
}

.monite-scope tr:last-child td { border-bottom: 0; }
.monite-scope tbody tr:hover td { background: var(--monite-table-row-hover); }

.monite-scope .text-right { text-align: right; }
.monite-scope .text-center { text-align: center; }
.monite-scope .muted { color: var(--monite-muted); }
9 changes: 9 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import './globals.css';

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
97 changes: 97 additions & 0 deletions src/app/page.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/* app/page.module.css */

.page {
display: grid;
grid-template-columns: 240px 1fr;
gap: 24px;
min-height: 100dvh;
padding: 24px;
background: var(--background);
color: var(--foreground);
}

/* --- Sidebar --- */
.sidebar {
position: sticky;
top: 24px;
align-self: start;
height: fit-content;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 16px;
background: var(--background);
}

@media (prefers-color-scheme: dark) {
.sidebar { border-color: #2a2a2a; }
}

.sidebarHeader {
font-weight: 700;
font-size: 14px;
letter-spacing: 0.02em;
text-transform: uppercase;
opacity: 0.8;
margin-bottom: 8px;
}

.nav {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 6px;
}

.navItem {
appearance: none;
background: transparent;
color: inherit;
border: 1px solid transparent;
padding: 10px 12px;
border-radius: 10px;
text-align: left;
cursor: pointer;
font: inherit;
}

.navItem:hover {
background: rgba(127, 127, 127, 0.08);
}

.navItemActive {
background: rgba(127, 127, 127, 0.12);
border-color: rgba(127, 127, 127, 0.25);
font-weight: 600;
}

/* --- Main content --- */
.main {
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 16px;
background: var(--background);
min-height: calc(100dvh - 48px);
overflow: hidden; /* keeps Monite tables tidy within the card */
}

@media (prefers-color-scheme: dark) {
.main { border-color: #2a2a2a; }
}

/* --- Responsive --- */
@media (max-width: 900px) {
.page {
grid-template-columns: 1fr;
gap: 16px;
padding: 16px;
}
.sidebar {
position: static;
top: auto;
order: 2;
}
.main {
order: 1;
min-height: auto;
}
}
11 changes: 11 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// no "use client" here
import styles from "./page.module.css";
import MoniteDash from "./components/MoniteDash";

export default function Home() {
return (
<div className={styles.page}>
<MoniteDash />
</div>
);
}