brand identity
Build and Push Docker Image / build (push) Successful in 2m21s

This commit is contained in:
Jason Stedwell
2026-07-03 10:09:25 -05:00
parent abb4c740e6
commit 497d54de3c
6 changed files with 147 additions and 54 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

+53 -18
View File
@@ -1,46 +1,81 @@
@import "tailwindcss"; @import "tailwindcss";
/*
* Message Point Media brand palette.
* Gold system (Middle / Light / Dark Gold) on a Dark Shade charcoal ground,
* with Light Shade text. Montserrat headings, Open Sans body.
*/
:root { :root {
--bg-dark: #0f172a; /* Brand core */
--bg-card: rgba(30, 41, 59, 0.7); --mpm-gold-mid: #dcbb4f; /* Middle Gold — default accent */
--accent-primary: #38bdf8; --mpm-gold-light: #f5cd15; /* Light Gold — accents on dark backgrounds */
--accent-secondary: #818cf8; --mpm-gold-dark: #998643; /* Dark Gold — titles on light backgrounds */
--text-main: #f1f5f9; --mpm-shade-dark: #232022; /* Dark Shade — near-black ground */
--text-dim: #94a3b8; --mpm-shade-light: #f5f1ec;/* Light Shade — off-white text */
--glass-border: rgba(255, 255, 255, 0.1); --mpm-accent: #849698; /* Light Accent — steel gray, secondary text */
--glass-highlight: rgba(255, 255, 255, 0.05);
/* Semantic tokens used across the app */
--bg-dark: var(--mpm-shade-dark);
--bg-card: rgba(40, 37, 39, 0.72);
--accent-primary: var(--mpm-gold-mid);
--accent-secondary: var(--mpm-gold-light);
--text-main: var(--mpm-shade-light);
--text-dim: var(--mpm-accent);
--glass-border: rgba(220, 187, 79, 0.18);
--glass-highlight: rgba(220, 187, 79, 0.08);
}
/* Expose brand tokens as Tailwind v4 theme colors so utilities like
text-accent-primary / text-text-dim / from-accent-primary resolve. */
@theme inline {
--color-bg-dark: var(--bg-dark);
--color-accent-primary: var(--accent-primary);
--color-accent-secondary: var(--accent-secondary);
--color-text-main: var(--text-main);
--color-text-dim: var(--text-dim);
--color-gold-dark: var(--mpm-gold-dark);
--font-sans: var(--font-open-sans), ui-sans-serif, system-ui, sans-serif;
--font-heading: var(--font-montserrat), var(--font-open-sans), sans-serif;
} }
body { body {
background: radial-gradient(circle at top left, #1e293b, #0f172a); background: radial-gradient(circle at top left, #2f2b2d, var(--mpm-shade-dark));
color: var(--text-main); color: var(--text-main);
min-height: 100vh; min-height: 100vh;
font-family: var(--font-open-sans), ui-sans-serif, system-ui, sans-serif;
}
/* Montserrat carries all headings — weight, not just size, sets hierarchy. */
h1, h2, h3, h4 {
font-family: var(--font-montserrat), var(--font-open-sans), sans-serif;
} }
.glass-card { .glass-card {
background: var(--bg-card); background: var(--bg-card);
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
border: 1px solid var(--glass-border); border: 1px solid var(--glass-border);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.45);
border-radius: 1rem; border-radius: 1rem;
} }
.glass-input { .glass-input {
background: rgba(15, 23, 42, 0.5); background: rgba(35, 32, 34, 0.5);
border: 1px solid var(--glass-border); border: 1px solid var(--glass-border);
color: white; color: var(--text-main);
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.glass-input:focus { .glass-input:focus {
border-color: var(--accent-primary); border-color: var(--accent-primary);
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.2); box-shadow: 0 0 0 2px rgba(220, 187, 79, 0.25);
outline: none; outline: none;
} }
/* Gold gradient echoes the MPM logo mark (light gold → dark gold).
Dark Shade text keeps the label readable on the gold fill. */
.btn-primary { .btn-primary {
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); background: linear-gradient(135deg, var(--mpm-gold-light), var(--mpm-gold-dark));
color: white; color: var(--mpm-shade-dark);
font-weight: 600; font-weight: 600;
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
border-radius: 0.75rem; border-radius: 0.75rem;
@@ -49,20 +84,20 @@ body {
.btn-primary:hover { .btn-primary:hover {
transform: translateY(-2px); transform: translateY(-2px);
opacity: 0.9; opacity: 0.92;
} }
.btn-secondary { .btn-secondary {
background: var(--glass-highlight); background: var(--glass-highlight);
border: 1px solid var(--glass-border); border: 1px solid var(--glass-border);
color: white; color: var(--text-main);
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
border-radius: 0.75rem; border-radius: 0.75rem;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.btn-secondary:hover { .btn-secondary:hover {
background: rgba(255, 255, 255, 0.1); background: rgba(220, 187, 79, 0.14);
} }
/* Animations */ /* Animations */
+15 -5
View File
@@ -1,13 +1,23 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Inter } from "next/font/google"; import { Montserrat, Open_Sans } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { AuthProvider } from "@/components/providers/AuthProvider"; import { AuthProvider } from "@/components/providers/AuthProvider";
const inter = Inter({ subsets: ["latin"] }); // MPM brand typography: Montserrat for headings, Open Sans for body.
const montserrat = Montserrat({
subsets: ["latin"],
weight: ["500", "600", "700", "800"],
variable: "--font-montserrat",
});
const openSans = Open_Sans({
subsets: ["latin"],
variable: "--font-open-sans",
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "WFH Daily Report", title: "WFH Daily Report | Message Point Media",
description: "Sleek and modern work from home reporting tool", description: "Daily work-from-home reporting for Message Point Media. Born to Innovate. Built to Last.",
}; };
export default function RootLayout({ export default function RootLayout({
@@ -17,7 +27,7 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en">
<body className={inter.className}> <body className={`${montserrat.variable} ${openSans.variable}`}>
<AuthProvider>{children}</AuthProvider> <AuthProvider>{children}</AuthProvider>
</body> </body>
</html> </html>
+17 -10
View File
@@ -193,13 +193,16 @@ export default function ReportForm() {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen p-4"> <div className="flex flex-col items-center justify-center min-h-screen p-4">
<div className="glass-card p-8 max-w-md w-full text-center space-y-6 animate-fade-in"> <div className="glass-card p-8 max-w-md w-full text-center space-y-6 animate-fade-in">
<h1 className="text-4xl font-bold bg-gradient-to-r from-accent-primary to-accent-secondary bg-clip-text text-transparent"> {/* eslint-disable-next-line @next/next/no-img-element */}
WFH Tracker <img src="/mpm-logo.png" alt="Message Point Media" className="h-12 mx-auto" />
<h1 className="text-4xl font-bold bg-gradient-to-r from-accent-secondary to-gold-dark bg-clip-text text-transparent">
WFH Daily Report
</h1> </h1>
<p className="text-text-dim">Please sign in with your company Google account to access your daily reports.</p> <p className="text-text-dim">Please sign in with your company Google account to access your daily reports.</p>
<button onClick={() => signIn("google")} className="btn-primary w-full flex items-center justify-center gap-2"> <button onClick={() => signIn("google")} className="btn-primary w-full flex items-center justify-center gap-2">
Continue with Google Continue with Google
</button> </button>
<p className="text-xs text-text-dim tracking-wide pt-2">Born to Innovate. Built to Last.</p>
</div> </div>
</div> </div>
); );
@@ -208,11 +211,15 @@ export default function ReportForm() {
return ( return (
<div className="max-w-4xl mx-auto p-4 md:p-8 space-y-8 animate-fade-in"> <div className="max-w-4xl mx-auto p-4 md:p-8 space-y-8 animate-fade-in">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div className="flex items-center gap-4">
<h1 className="text-3xl font-bold">{view === "REPORT" ? "Daily Report" : "Administration"}</h1> {/* eslint-disable-next-line @next/next/no-img-element */}
<p className="text-text-dim flex items-center gap-2"> <img src="/mpm-logo.png" alt="Message Point Media" className="h-10 hidden sm:block" />
<Calendar size={16} /> {new Date().toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })} <div>
</p> <h1 className="text-3xl font-bold">{view === "REPORT" ? "Daily Report" : "Administration"}</h1>
<p className="text-text-dim flex items-center gap-2">
<Calendar size={16} /> {new Date().toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
</p>
</div>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{session.user.role === "ADMIN" && ( {session.user.role === "ADMIN" && (
@@ -337,9 +344,9 @@ export default function ReportForm() {
onChange={(e) => updateTask(task.id, { status: e.target.value as TaskStatus, type: 'COMPLETED' })} onChange={(e) => updateTask(task.id, { status: e.target.value as TaskStatus, type: 'COMPLETED' })}
className="bg-transparent text-sm text-text-dim border-none p-0 focus:ring-0 outline-none appearance-none cursor-pointer" className="bg-transparent text-sm text-text-dim border-none p-0 focus:ring-0 outline-none appearance-none cursor-pointer"
> >
<option value="DONE" className="bg-slate-800">Completed</option> <option value="DONE" className="bg-bg-dark">Completed</option>
<option value="IN_PROGRESS" className="bg-slate-800">In Progress</option> <option value="IN_PROGRESS" className="bg-bg-dark">In Progress</option>
<option value="CANCELLED" className="bg-slate-800">Delayed</option> <option value="CANCELLED" className="bg-bg-dark">Delayed</option>
</select> </select>
<div className="flex-1 relative"> <div className="flex-1 relative">
<LinkIcon size={14} className="absolute left-0 top-1/2 -translate-y-1/2 text-text-dim" /> <LinkIcon size={14} className="absolute left-0 top-1/2 -translate-y-1/2 text-text-dim" />
+62 -21
View File
@@ -1,8 +1,28 @@
import { google, Auth } from 'googleapis'; import { google, Auth } from 'googleapis';
import { Readable } from 'stream'; import { Readable } from 'stream';
import { readFileSync } from 'fs';
import path from 'path';
import { prisma } from './prisma'; import { prisma } from './prisma';
import type { Prisma } from '@prisma/client'; import type { Prisma } from '@prisma/client';
// The MPM logo is embedded directly into the report HTML as a base64 data URI
// so it renders regardless of network reachability (e.g. a LAN-only deployment
// where Google's importer can't fetch an external URL). Read from disk once and
// cache; `null` means the asset was missing and the report renders without it.
let cachedLogoDataUri: string | null | undefined;
function getLogoDataUri(): string | null {
if (cachedLogoDataUri !== undefined) return cachedLogoDataUri;
try {
const logoPath = path.join(process.cwd(), 'public', 'mpm-logo-doc.png');
const b64 = readFileSync(logoPath).toString('base64');
cachedLogoDataUri = `data:image/png;base64,${b64}`;
} catch (error) {
console.error('MPM report logo could not be loaded; rendering without it:', error);
cachedLogoDataUri = null;
}
return cachedLogoDataUri;
}
type ReportWithRelations = Prisma.ReportGetPayload<{ type ReportWithRelations = Prisma.ReportGetPayload<{
include: { tasks: true; user: true }; include: { tasks: true; user: true };
}>; }>;
@@ -127,21 +147,41 @@ export function generateReportHTML(report: ReportWithRelations) {
const plannedTasks = report.tasks.filter((t) => t.type === 'PLANNED'); const plannedTasks = report.tasks.filter((t) => t.type === 'PLANNED');
const completedTasks = report.tasks.filter((t) => t.type === 'COMPLETED'); const completedTasks = report.tasks.filter((t) => t.type === 'COMPLETED');
const cellStyle = "padding: 10px; border-bottom: 1px solid #e2e8f0; font-family: Arial, sans-serif; font-size: 11pt;"; // MPM brand palette
const headerStyle = "padding: 12px 10px; background-color: #f1f5f9; border-bottom: 2px solid #cbd5e1; font-family: Arial, sans-serif; font-size: 11pt; font-weight: bold; text-align: left; color: #334155;"; const GOLD_DARK = '#998643'; // titles on light backgrounds
const GOLD_MID = '#DCBB4F'; // accent lines, dividers
const SHADE_DARK = '#232022'; // body text / table header background
const SHADE_LIGHT = '#F5F1EC'; // section panels / table header text
const OFF_WHITE = '#FAF7F2'; // alternating table rows
const ACCENT = '#849698'; // secondary / muted text
const bodyFont = "'Open Sans', Arial, sans-serif";
const headingFont = "'Montserrat', 'Open Sans', Arial, sans-serif";
const cellStyle = `padding: 10px; border-bottom: 1px solid #E7E0D5; font-family: ${bodyFont}; font-size: 11pt; color: ${SHADE_DARK};`;
const headerStyle = `padding: 12px 10px; background-color: ${SHADE_DARK}; font-family: ${headingFont}; font-size: 11pt; font-weight: 600; text-align: left; color: ${SHADE_LIGHT};`;
// Embed the logo inline so it survives conversion without a reachable host.
const logoDataUri = getLogoDataUri();
const logoHtml = logoDataUri
? `<img src="${logoDataUri}" alt="Message Point Media" style="height: 44px; margin-bottom: 16px;" />`
: '';
const rowBg = (i: number) => (i % 2 === 1 ? ` background-color: ${OFF_WHITE};` : '');
return ` return `
<html> <html>
<body style="font-family: Arial, sans-serif; color: #334155; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px;"> <body style="font-family: ${bodyFont}; color: ${SHADE_DARK}; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px;">
<h1 style="color: #0f172a; border-bottom: 3px solid #3b82f6; padding-bottom: 10px; font-family: Arial, sans-serif; margin-bottom: 20px;">WFH Daily Report</h1> ${logoHtml}
<h1 style="color: ${GOLD_DARK}; border-bottom: 3px solid ${GOLD_MID}; padding-bottom: 10px; font-family: ${headingFont}; font-weight: 700; margin-bottom: 20px;">WFH Daily Report</h1>
<div style="background-color: #f8fafc; padding: 20px; border-left: 4px solid #3b82f6; border-radius: 4px; margin-bottom: 30px; font-family: Arial, sans-serif;">
<div style="background-color: ${SHADE_LIGHT}; padding: 20px; border-left: 4px solid ${GOLD_MID}; border-radius: 4px; margin-bottom: 30px; font-family: ${bodyFont};">
<p style="margin: 0 0 8px 0; font-size: 11pt;"><strong>Date:</strong> ${dateStr}</p> <p style="margin: 0 0 8px 0; font-size: 11pt;"><strong>Date:</strong> ${dateStr}</p>
<p style="margin: 0 0 8px 0; font-size: 11pt;"><strong>Employee:</strong> ${escapeHtml(report.user.name)}</p> <p style="margin: 0 0 8px 0; font-size: 11pt;"><strong>Employee:</strong> ${escapeHtml(report.user.name)}</p>
<p style="margin: 0; font-size: 11pt;"><strong>Manager:</strong> ${escapeHtml(report.managerName || 'N/A')}</p> <p style="margin: 0; font-size: 11pt;"><strong>Manager:</strong> ${escapeHtml(report.managerName || 'N/A')}</p>
</div> </div>
<h2 style="color: #1e293b; margin-top: 30px; margin-bottom: 15px; font-family: Arial, sans-serif;">Planned Tasks</h2> <h2 style="color: ${GOLD_DARK}; margin-top: 30px; margin-bottom: 15px; font-family: ${headingFont}; font-weight: 600;">Planned Tasks</h2>
${plannedTasks.length > 0 ? ` ${plannedTasks.length > 0 ? `
<table style="width: 100%; border-collapse: collapse; margin-bottom: 30px;"> <table style="width: 100%; border-collapse: collapse; margin-bottom: 30px;">
<tr> <tr>
@@ -149,17 +189,17 @@ export function generateReportHTML(report: ReportWithRelations) {
<th style="${headerStyle} width: 20%;">Estimate</th> <th style="${headerStyle} width: 20%;">Estimate</th>
<th style="${headerStyle} width: 35%;">Notes</th> <th style="${headerStyle} width: 35%;">Notes</th>
</tr> </tr>
${plannedTasks.map((t) => ` ${plannedTasks.map((t, i) => `
<tr> <tr>
<td style="${cellStyle}">${escapeHtml(t.description)}</td> <td style="${cellStyle}${rowBg(i)}">${escapeHtml(t.description)}</td>
<td style="${cellStyle} color: #64748b;">${escapeHtml(t.timeEstimate || '-')}</td> <td style="${cellStyle}${rowBg(i)} color: ${ACCENT};">${escapeHtml(t.timeEstimate || '-')}</td>
<td style="${cellStyle} color: #64748b;">${escapeHtml(t.notes || '-')}</td> <td style="${cellStyle}${rowBg(i)} color: ${ACCENT};">${escapeHtml(t.notes || '-')}</td>
</tr> </tr>
`).join('')} `).join('')}
</table> </table>
` : `<p style="font-style: italic; color: #94a3b8; font-family: Arial, sans-serif; margin-bottom: 30px;">No planned tasks for today.</p>`} ` : `<p style="font-style: italic; color: ${ACCENT}; font-family: ${bodyFont}; margin-bottom: 30px;">No planned tasks for today.</p>`}
<h2 style="color: #1e293b; margin-top: 30px; margin-bottom: 15px; font-family: Arial, sans-serif;">Completed Tasks</h2> <h2 style="color: ${GOLD_DARK}; margin-top: 30px; margin-bottom: 15px; font-family: ${headingFont}; font-weight: 600;">Completed Tasks</h2>
${completedTasks.length > 0 ? ` ${completedTasks.length > 0 ? `
<table style="width: 100%; border-collapse: collapse; margin-bottom: 30px;"> <table style="width: 100%; border-collapse: collapse; margin-bottom: 30px;">
<tr> <tr>
@@ -167,21 +207,22 @@ export function generateReportHTML(report: ReportWithRelations) {
<th style="${headerStyle} width: 20%;">Status</th> <th style="${headerStyle} width: 20%;">Status</th>
<th style="${headerStyle} width: 40%;">Work Link</th> <th style="${headerStyle} width: 40%;">Work Link</th>
</tr> </tr>
${completedTasks.map((t) => { ${completedTasks.map((t, i) => {
const link = safeLink(t.link); const link = safeLink(t.link);
return ` return `
<tr> <tr>
<td style="${cellStyle}">${escapeHtml(t.description)}</td> <td style="${cellStyle}${rowBg(i)}">${escapeHtml(t.description)}</td>
<td style="${cellStyle} font-weight: bold; color: #059669;">${escapeHtml(t.status || 'Done')}</td> <td style="${cellStyle}${rowBg(i)} font-weight: bold; color: ${GOLD_DARK};">${escapeHtml(t.status || 'Done')}</td>
<td style="${cellStyle}">${link ? `<a href="${escapeHtml(link)}" style="color: #2563eb; text-decoration: none;">${escapeHtml(link)}</a>` : '-'}</td> <td style="${cellStyle}${rowBg(i)}">${link ? `<a href="${escapeHtml(link)}" style="color: ${GOLD_DARK}; text-decoration: none;">${escapeHtml(link)}</a>` : '-'}</td>
</tr> </tr>
`; `;
}).join('')} }).join('')}
</table> </table>
` : `<p style="font-style: italic; color: #94a3b8; font-family: Arial, sans-serif; margin-bottom: 30px;">No completed tasks reported today.</p>`} ` : `<p style="font-style: italic; color: ${ACCENT}; font-family: ${bodyFont}; margin-bottom: 30px;">No completed tasks reported today.</p>`}
<div style="margin-top: 50px; font-size: 9pt; color: #cbd5e1; text-align: center; border-top: 1px solid #e2e8f0; padding-top: 20px; font-family: Arial, sans-serif;"> <div style="margin-top: 50px; font-size: 9pt; color: ${ACCENT}; text-align: center; border-top: 1px solid ${GOLD_MID}; padding-top: 20px; font-family: ${bodyFont};">
Generated automatically by WFH App <p style="margin: 0 0 4px 0; font-family: ${headingFont}; font-weight: 600; color: ${GOLD_DARK}; letter-spacing: 0.5px;">Born to Innovate. Built to Last.</p>
<p style="margin: 0;">Message Point Media &middot; Generated automatically by the WFH Daily Report app</p>
</div> </div>
</body> </body>
</html> </html>