diff --git a/app/applications/[uuid]/page.tsx b/app/applications/[uuid]/page.tsx index ace6953..339d86d 100644 --- a/app/applications/[uuid]/page.tsx +++ b/app/applications/[uuid]/page.tsx @@ -136,20 +136,15 @@ export default function ApplicationDetailPage({ params }: any) { }; return ( -
+

Views and Visits Data for {appName ? appName : {typedParams.uuid}}

-
+
@@ -159,16 +154,11 @@ export default function ApplicationDetailPage({ params }: any) { value={subscriptionUuid} onChange={e => setSubscriptionUuid(e.target.value)} className="w-full p-2 border rounded mb-4" - style={{ - borderColor: 'var(--stanford-gray)', - color: 'var(--stanford-black)', - fontFamily: 'Source Sans Pro, Arial, sans-serif', - }} required />
-
-
@@ -211,33 +191,27 @@ export default function ApplicationDetailPage({ params }: any) { onClick={fetchAppDetail} disabled={loading || !subscriptionUuid} className="px-6 py-2 rounded-md font-semibold transition-colors duration-150" - style={{ - backgroundColor: 'var(--stanford-cardinal)', - color: 'var(--stanford-white)', - border: '2px solid var(--stanford-cardinal)', - fontFamily: 'Source Sans Pro, Arial, sans-serif', - }} > {loading ? 'Fetching Data...' : 'Fetch Analytics Data'} {loading && (
-
{loadingStep}
+
{loadingStep}
)} {!loading && elapsedTime !== null && (
-
+
Data loaded in {elapsedTime.toFixed(1)} seconds
)}
-

+

(Note that it can take several minutes to fetch data from the Acquia API.)

diff --git a/app/applications/page.tsx b/app/applications/page.tsx index 832ac75..266a319 100644 --- a/app/applications/page.tsx +++ b/app/applications/page.tsx @@ -17,8 +17,7 @@ async function fetchData() { viewsRes.ok ? viewsRes.json() : [], visitsRes.ok ? visitsRes.json() : [], ]); -// console.log('viewsRaw:', viewsRaw); -// console.log('visitsRaw:', visitsRaw); + // Defensive: ensure arrays const views = Array.isArray(viewsRaw) ? viewsRaw diff --git a/app/globals.css b/app/globals.css index 5c35358..7ca2ab0 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,7 +1,20 @@ -/* Add this to your CSS */ -.recharts-wrapper, .recharts-surface { - width: 100% !important; - height: 100% !important; +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "tailwindcss/utilities"; + +/* Stanford Font */ +html { + font-size: 100%; +} + +.recharts-wrapper { + width: 90% !important; + margin: 0 auto; +} + +.recharts-tooltip-label, +.recharts-tooltip-item-list { + font-size: 18px; } .w-full { @@ -32,46 +45,4 @@ width: 100%; height: 400px; min-height: 400px; -} - -/* Stanford Brand Colors */ -:root { - --stanford-cardinal: #8C1515; - --stanford-black: #2B2D2F; - --stanford-gray: #4D4F53; - --stanford-white: #FFFFFF; - --stanford-gold: #B83A4B; -} - -/* Stanford Font */ -body { - font-family: 'Source Sans Pro', Arial, sans-serif; - background-color: var(--stanford-white); - color: var(--stanford-black); -} - -/* Table Styling */ -.stanford-table { - border: 2px solid var(--stanford-cardinal); - border-radius: 0.5rem; - background-color: var(--stanford-white); - box-shadow: 0 4px 6px -1px rgba(44, 21, 21, 0.1), 0 2px 4px -1px rgba(44, 21, 21, 0.06); -} - -.stanford-table th { - background-color: var(--stanford-cardinal); - color: var(--stanford-white); - font-weight: bold; - text-transform: uppercase; -} - -.stanford-table td { - border: 1px solid var(--stanford-gray); - color: var(--stanford-black); -} - -.stanford-table tfoot td { - background-color: var(--stanford-gray); - color: var(--stanford-white); - font-weight: bold; -} +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 98a6c50..ce98c89 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,22 +1,121 @@ import './globals.css'; import type { Metadata } from 'next'; -import { Inter } from 'next/font/google'; +import { Source_Sans_3, Source_Serif_4 } from 'next/font/google'; +import localFont from 'next/font/local'; +import { GlobalFooter } from '@/components/GlobalFooter/GlobalFooter'; +import { cnb } from 'cnbuilder'; -const inter = Inter({ subsets: ['latin'] }); +const source_sans = Source_Sans_3({ + subsets: ['latin'], + style: ['italic','normal'], + display: 'swap', + variable: '--font-source-sans', +}); + +const source_serif = Source_Serif_4({ + subsets: ['latin'], + style: ['italic','normal'], + display: 'swap', + variable: '--font-source-serif', +}); + +const stanford = localFont({ + src: '../public/fonts/stanford.woff2', + weight: '300', + variable: '--font-stanford', +}); export const metadata: Metadata = { - title: 'Acquia API Dashboard', - description: 'Dashboard for visualizing Acquia API data', + title: 'CHURRO', + description: 'Dashboard for Acquia Views/Visits data', + applicationName: 'Stanford University', + icons: { + icon: [ + { url: '/favicon.ico', type: 'image/vnd.microsoft.icon' }, + { url: 'https://www-media.stanford.edu/assets/favicon/favicon-196x196.png', sizes: '196x196', type: 'image/png' }, + { url: 'https://www-media.stanford.edu/assets/favicon/favicon-192x192.png', sizes: '192x192', type: 'image/png' }, + { url: 'https://www-media.stanford.edu/assets/favicon/favicon-128.png', sizes: '128x128', type: 'image/png' }, + { url: 'https://www-media.stanford.edu/assets/favicon/favicon-96x96.png', sizes: '96x96', type: 'image/png' }, + { url: 'https://www-media.stanford.edu/assets/favicon/favicon-32x32.png', sizes: '32x32', type: 'image/png' }, + { url: 'https://www-media.stanford.edu/assets/favicon/favicon-16x16.png', sizes: '16x16', type: 'image/png' }, + ], + apple: [ + { url: 'https://www-media.stanford.edu/assets/favicon/apple-touch-icon-60x60.png', sizes: '60x60' }, + { url: 'https://www-media.stanford.edu/assets/favicon/apple-touch-icon-72x72.png', sizes: '72x72' }, + { url: 'https://www-media.stanford.edu/assets/favicon/apple-touch-icon-76x76.png', sizes: '76x76' }, + { url: 'https://www-media.stanford.edu/assets/favicon/apple-touch-icon-114x114.png', sizes: '114x114' }, + { url: 'https://www-media.stanford.edu/assets/favicon/apple-touch-icon-120x120.png', sizes: '120x120' }, + { url: 'https://www-media.stanford.edu/assets/favicon/apple-touch-icon-144x144.png', sizes: '144x144' }, + { url: 'https://www-media.stanford.edu/assets/favicon/apple-touch-icon-152x152.png', sizes: '152x152' }, + { url: 'https://www-media.stanford.edu/assets/favicon/apple-touch-icon-180x180.png', sizes: '180x180' }, + ], + other: [ + { rel: 'mask-icon', url: 'https://www-media.stanford.edu/assets/favicon/safari-pinned-tab.svg', color: '#ffffff' }, + ], + }, + other: { + 'msapplication-TileColor': '#FFFFFF', + 'msapplication-TileImage': 'https://www-media.stanford.edu/assets/favicon/mstile-144x144.png', + 'msapplication-square70x70logo': 'https://www-media.stanford.edu/assets/favicon/mstile-70x70.png', + 'msapplication-square150x150logo': 'https://www-media.stanford.edu/assets/favicon/mstile-150x150.png', + 'msapplication-square310x310logo': 'https://www-media.stanford.edu/assets/favicon/mstile-310x310.png', + }, }; +function StanfordHeader() { + return ( +
+ + +
+ ); +} + export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( - - {children} + + + +
{children}
+ + ); } \ No newline at end of file diff --git a/components/Container/Container.styles.ts b/components/Container/Container.styles.ts new file mode 100644 index 0000000..eba1db6 --- /dev/null +++ b/components/Container/Container.styles.ts @@ -0,0 +1,12 @@ +export const widths = { + full: 'w-full', // width: 100%; default + site: 'cc', // Use Decanter custom screen margins and sets max content width of 1500px + wide: 'cc 3xl:px-100 4xl:px-[calc((100%-1800px)/2)]', + screen: 'w-screen', // width: 100vw +}; + +export const bgColors = { + black: 'bg-gc-black text-white', + white: 'bg-white text-gc-black', + 'fog-light': 'bg-fog-light text-gc-black', +}; \ No newline at end of file diff --git a/components/Container/Container.tsx b/components/Container/Container.tsx new file mode 100644 index 0000000..f7d758d --- /dev/null +++ b/components/Container/Container.tsx @@ -0,0 +1,61 @@ +import { HTMLAttributes } from 'react'; +import { cnb } from 'cnbuilder'; +import { + paddingTops, + paddingBottoms, + paddingVerticals, + marginTops, + marginBottoms, + marginVerticals, + type MarginType, + type PaddingType, +} from '@/utilities/datasource'; +import * as styles from './Container.styles'; +import * as types from './Container.types'; + +export type ContainerProps = HTMLAttributes & { + as?: types.ContainerElementType; + width?: types.WidthType; + pt?: PaddingType; + pb?: PaddingType; + py?: PaddingType; + mt?: MarginType; + mb?: MarginType; + my?: MarginType; + bgColor?: types.BgColorType; + style?: React.CSSProperties; +}; + +export const Container = ({ + as: AsComponent = 'div', + width = 'site', + py, + pt, + pb, + mt, + mb, + my, + bgColor, + style, + className, + children, + ...props +}: ContainerProps) => ( + + {children} + +); \ No newline at end of file diff --git a/components/Container/Container.types.ts b/components/Container/Container.types.ts new file mode 100644 index 0000000..4b7d6d0 --- /dev/null +++ b/components/Container/Container.types.ts @@ -0,0 +1,7 @@ +import * as styles from './Container.styles'; + +export type ContainerElementType = 'div' | 'section' | 'article' | 'main' | 'footer' | 'aside' | 'header' | 'nav' | 'form' | 'fieldset' | 'figcaption' | 'figure'; + +export type WidthType = keyof typeof styles.widths; + +export type BgColorType = keyof typeof styles.bgColors | ''; diff --git a/components/Container/index.ts b/components/Container/index.ts new file mode 100644 index 0000000..9cb532b --- /dev/null +++ b/components/Container/index.ts @@ -0,0 +1,3 @@ +export * from './Container'; +export * from './Container.styles'; +export * from './Container.types'; \ No newline at end of file diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx index db2b720..1857427 100644 --- a/components/Dashboard.tsx +++ b/components/Dashboard.tsx @@ -9,6 +9,16 @@ import SimpleViewsBarChart from './SimpleViewsBarChart'; import CountUpTimer from './CountUpTimer'; import DataTable from './DataTable'; +const TABS = [ + { label: 'Views Pie Chart', key: 'views-pie' }, + { label: 'Views Bar Chart', key: 'views-bar' }, + { label: 'Visits Pie Chart', key: 'visits-pie' }, + { label: 'Visits Bar Chart', key: 'visits-bar' }, + { label: 'Views Table', key: 'views-table' }, + { label: 'Visits Table', key: 'visits-table' }, +]; + + const DEFAULT_SUBSCRIPTION_UUID = process.env.NEXT_PUBLIC_ACQUIA_SUBSCRIPTION_UUID || ""; const Dashboard: React.FC = () => { @@ -27,6 +37,7 @@ const Dashboard: React.FC = () => { const [elapsedTime, setElapsedTime] = useState(null); const [applications, setApplications] = useState([]); const [applicationMap, setApplicationMap] = useState>({}); + const [activeTab, setActiveTab] = useState(TABS[0].key); const fetchApplications = async () => { try { @@ -204,41 +215,20 @@ const Dashboard: React.FC = () => { return (
+ className="min-h-screen p-8">
-

- Cloud Hosting Usage Reporting with Recurring Output (CHURRO) -

-

- Stanford University IT | Stanford Web Services -

-
+
This dashboard shows your monthly usage for Acquia Cloud hosting.
- + Monthly limits: {monthlyVisitsEntitlement.toLocaleString()} visits and {monthlyViewsEntitlement.toLocaleString()} views.
- -
+
@@ -248,16 +238,11 @@ const Dashboard: React.FC = () => { value={subscriptionUuid} onChange={e => setSubscriptionUuid(e.target.value)} className="w-full p-2 border rounded mb-4" - style={{ - borderColor: 'var(--stanford-gray)', - color: 'var(--stanford-black)', - fontFamily: 'Source Sans Pro, Arial, sans-serif', - }} required /> -
+
-
-
-
+
{loading && ( -
+
-
{loadingStep}
+
{loadingStep}
)} {!loading && elapsedTime !== null && ( -
+
-
+
Data loaded in {elapsedTime.toFixed(1)} seconds
)}
-

+

(Note that it can take several minutes to fetch data from the Acquia API.)

-
+
{/* Per-Application Links */} -
-

+
+

Per-Application Reporting

- -
- {/* Data Display Section */} -
- {/* Data Tables Section */} -
+ {/* Tabs */} +
+ {TABS.map(tab => ( + + ))} +
+ + {/* Tab Content */} +
+ {activeTab === 'views-pie' && ( + ({ name: app.name, value: app.views, uuid: app.uuid }))} /> + )} + {activeTab === 'views-bar' && ( + ({ name: app.name, value: app.views, uuid: app.uuid }))} /> + )} + {activeTab === 'visits-pie' && ( + ({ name: app.name, value: app.visits, uuid: app.uuid }))} /> + )} + {activeTab === 'visits-bar' && ( + ({ name: app.name, value: app.visits, uuid: app.uuid }))} /> + )} + {activeTab === 'views-table' && ( ({ @@ -374,6 +369,8 @@ const Dashboard: React.FC = () => { valueLabel="Views" total={viewsSummary.reduce((sum, app) => sum + app.views, 0)} /> + )} + {activeTab === 'visits-table' && ( ({ @@ -385,56 +382,11 @@ const Dashboard: React.FC = () => { valueLabel="Visits" total={visitsSummary.reduce((sum, app) => sum + app.visits, 0)} /> -
- - {/* Views Section */} -
-

- Views by Application -

-
- ({ name: app.name, value: app.views, uuid: app.uuid }))} /> -
-
-
-

- Views by Application -

-
- ({ name: app.name, value: app.views, uuid: app.uuid }))} /> -
-
- - {/* Visits Section */} -
-

- Visits by Application -

-
- ({ name: app.name, value: app.visits, uuid: app.uuid }))} /> -
-
-
-

- Visits by Application -

-
- ({ name: app.name, value: app.visits, uuid: app.uuid }))} /> -
-
-
- - {loading && ( -
- Loading... -
- )} - {error && ( -
- {error} -
- )} + )} +

+ {/* ...loading and error messages... */}
+ ); }; diff --git a/components/DataTable.tsx b/components/DataTable.tsx index 38a2978..10e96ec 100644 --- a/components/DataTable.tsx +++ b/components/DataTable.tsx @@ -16,7 +16,7 @@ interface DataTableProps { const DataTable: React.FC = ({ title, data, valueLabel, total }) => { return ( -
+

{title}

@@ -24,10 +24,10 @@ const DataTable: React.FC = ({ title, data, valueLabel, total }) - -
+ Rank + Name diff --git a/components/FlexBox/FlexBox.styles.ts b/components/FlexBox/FlexBox.styles.ts new file mode 100644 index 0000000..9345c0b --- /dev/null +++ b/components/FlexBox/FlexBox.styles.ts @@ -0,0 +1,38 @@ +export const flexDirection = { + row: 'flex-row', + 'row-reverse': 'flex-row-reverse', + col: 'flex-col', + 'col-reverse': 'flex-col-reverse', +}; + +export const flexWrap = { + wrap: 'flex-wrap', + 'wrap-reverse': 'flex-wrap-reverse', + nowrap: 'flex-nowrap', +}; + +export const flexJustifyContent = { + start: 'justify-start', + end: 'justify-end', + center: 'justify-center', + between: 'justify-between', + around: 'justify-around', + evenly: 'justify-evenly', +}; + +export const flexAlignContent = { + start: 'content-start', + end: 'content-end', + center: 'content-center', + between: 'content-between', + around: 'content-around', + evenly: 'content-evenly', +}; + +export const flexAlignItems = { + start: 'items-start', + end: 'items-end', + center: 'items-center', + baseline: 'items-baseline', + stretch: 'items-stretch', +}; \ No newline at end of file diff --git a/components/FlexBox/FlexBox.tsx b/components/FlexBox/FlexBox.tsx new file mode 100644 index 0000000..527ab9d --- /dev/null +++ b/components/FlexBox/FlexBox.tsx @@ -0,0 +1,45 @@ +import React, { ReactNode, HTMLAttributes, forwardRef } from 'react'; +import { cnb } from 'cnbuilder'; +import * as styles from './FlexBox.styles'; +import * as types from './FlexBox.types'; + +type FlexBoxProps = HTMLAttributes & { + as?: React.ElementType; + direction?: types.FlexDirectionType; + wrap?: types.FlexWrapType; + gap?: boolean; + justifyContent?: types.FlexJustifyContentType; + alignContent?: types.FlexAlignContentType; + alignItems?: types.FlexAlignItemsType; + children?: ReactNode; +}; + +export const FlexBox = forwardRef(({ + as: AsComponent = 'div', + direction, + gap, + wrap, + justifyContent, + alignContent, + alignItems, + children, + className, + ...props +}, ref) => ( + + {children} + +)); \ No newline at end of file diff --git a/components/FlexBox/FlexBox.types.ts b/components/FlexBox/FlexBox.types.ts new file mode 100644 index 0000000..d36f892 --- /dev/null +++ b/components/FlexBox/FlexBox.types.ts @@ -0,0 +1,13 @@ +import * as styles from './FlexBox.styles'; + +export type FlexElementType = 'div' | 'section' | 'article' | 'main' | 'footer' | 'aside' | 'header' | 'nav' | 'form' | 'button' | 'fieldset' | 'ul' | 'ol' | 'li' | 'span'; + +export type FlexDirectionType = keyof typeof styles.flexDirection; + +export type FlexWrapType = keyof typeof styles.flexWrap; + +export type FlexJustifyContentType = keyof typeof styles.flexJustifyContent; + +export type FlexAlignContentType = keyof typeof styles.flexAlignContent; + +export type FlexAlignItemsType = keyof typeof styles.flexAlignItems; \ No newline at end of file diff --git a/components/FlexBox/index.ts b/components/FlexBox/index.ts new file mode 100644 index 0000000..1923262 --- /dev/null +++ b/components/FlexBox/index.ts @@ -0,0 +1,3 @@ +export * from './FlexBox'; +export * from './FlexBox.styles'; +export * from './FlexBox.types'; diff --git a/components/GlobalFooter/GlobalFooter.styles.ts b/components/GlobalFooter/GlobalFooter.styles.ts new file mode 100644 index 0000000..130f8aa --- /dev/null +++ b/components/GlobalFooter/GlobalFooter.styles.ts @@ -0,0 +1,12 @@ +export const root = 'w-full basefont-20 rs-py-1 text-white bg-cardinal-red'; +export const outerWrapper = 'lg:flex-row'; +export const logoWrapper = 'text-center mt-5 mb-9'; +export const logo = 'type-2'; +export const contentWrapper = 'lg:pl-45 xl:pl-50 text-left sm:text-center lg:text-left grow'; +export const menusWrapper = 'sm:flex-col sm:items-center lg:items-start mb-10'; +export const stanfordMenu = 'list-unstyled mb-10 sm:mb-4 mr-19 sm:mr-0 p-0 text-11 md:text-11 2xl:text-12 flex flex-col sm:flex-row'; +export const legalMenu = 'list-unstyled mb-10 sm:mb-0 ml-19 sm:ml-0 p-0 text-11 sm:text-11 md:text-11 xl:text-11 flex flex-col sm:flex-row sm:link-regular'; +export const listItem = 'sm:mr-10 md:mr-20 lg:mr-27'; +export const link = 'text-white no-underline hocus:underline hocus:text-white'; +export const copyright = 'text-11 sm:text-12 text-center lg:text-left'; +export const copyrightText = 'whitespace-no-wrap'; \ No newline at end of file diff --git a/components/GlobalFooter/GlobalFooter.tsx b/components/GlobalFooter/GlobalFooter.tsx new file mode 100644 index 0000000..a46dc36 --- /dev/null +++ b/components/GlobalFooter/GlobalFooter.tsx @@ -0,0 +1,127 @@ +import { StanfordLogo } from '@/components/Logo'; +import { Container } from '@/components/Container'; +import { FlexBox } from '@/components/FlexBox'; +import * as styles from './GlobalFooter.styles'; + +export const GlobalFooter = () => ( + + +
+ +
+
+ + + + +
+ © Stanford University. +   Stanford, California 94305. +
+
+
+
+); \ No newline at end of file diff --git a/components/GlobalFooter/index.ts b/components/GlobalFooter/index.ts new file mode 100644 index 0000000..f314fba --- /dev/null +++ b/components/GlobalFooter/index.ts @@ -0,0 +1 @@ +export * from './GlobalFooter'; \ No newline at end of file diff --git a/components/Logo/LogoLockup.styles.ts b/components/Logo/LogoLockup.styles.ts new file mode 100644 index 0000000..eb12271 --- /dev/null +++ b/components/Logo/LogoLockup.styles.ts @@ -0,0 +1,14 @@ +export const root = 'no-underline inline-block font-normal'; +export const contentWrapper = 'flex-col sm:flex-row items-start sm:items-center'; +export const logo = 'text-19 sm:text-[1.43em] leading-half mt-[0.27em]'; +export const bar = 'hidden sm:block w-1 h-1em mx-03em'; +export const text = 'text-15 sm:text-[1.05em] mt-03em -ml-01em sm:ml-0'; +export const textColors = { + default: 'text-gc-black', + white: 'text-white', +}; +export type LogoTextColorType = keyof typeof textColors; +export const barColors = { + default: 'bg-gc-black', + white: 'bg-white', +}; \ No newline at end of file diff --git a/components/Logo/LogoLockup.tsx b/components/Logo/LogoLockup.tsx new file mode 100644 index 0000000..458a605 --- /dev/null +++ b/components/Logo/LogoLockup.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { cnb } from 'cnbuilder'; +import Link from 'next/link'; +import { FlexBox } from '@/components/FlexBox'; +import { StanfordLogo } from './StanfordLogo'; +import * as styles from './LogoLockup.styles'; + +/** + * Stanford Department Branding Component with the Stanford wordmark logo and department name. + */ +type LogoLockupProps = { + text: string; + isLink?: boolean; + color?: styles.LogoTextColorType; + className?: string; +} + +export const LogoLockup = ({ + text, + isLink, + color = 'default', + className, + ...rest +}: LogoLockupProps) => { + const LockupContent = ( + + +