(null)
const [useElements, setUseElements] = useState(false)
- if (!isStripeConfigured()) {
+ if (!stripeEnabled) {
return (
Online card payments are being configured. You can mail a check or call (818) 491-6884 to donate today.
diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx
index 723db09..4b3d7d8 100644
--- a/src/contexts/AuthContext.tsx
+++ b/src/contexts/AuthContext.tsx
@@ -1,72 +1,49 @@
-import React, { createContext, useContext, useState, ReactNode } from 'react'
+import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'
+import {
+ fetchMe,
+ loginApi,
+ setAuthToken,
+ getAuthToken,
+ type AuthUser,
+} from '../lib/mimApi'
-// Types
-export interface AuthUser {
- id: string
- email: string
- role: 'admin' | 'volunteer' | 'resource'
- name: string
- lastLogin: Date
- permissions: string[]
-}
+export type { AuthUser }
export interface AuthContextType {
user: AuthUser | null
login: (email: string, password: string) => Promise
logout: () => void
isLoading: boolean
+ restoring: boolean
}
-// Create Context
const AuthContext = createContext(null)
-// Auth Provider Component
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [user, setUser] = useState(null)
const [isLoading, setIsLoading] = useState(false)
+ const [restoring, setRestoring] = useState(true)
+
+ useEffect(() => {
+ const token = getAuthToken()
+ if (!token) {
+ setRestoring(false)
+ return
+ }
+ fetchMe()
+ .then(({ user: me }) => setUser(me))
+ .catch(() => setAuthToken(null))
+ .finally(() => setRestoring(false))
+ }, [])
const login = async (email: string, password: string): Promise => {
setIsLoading(true)
-
try {
- // Mock authentication - replace with real API call
- console.log('🔐 Attempting login for:', email)
-
- // Simulate API call
- await new Promise(resolve => setTimeout(resolve, 1000))
-
- // Mock user data based on email
- const mockUsers: Record = {
- 'admin@mim4u.org': {
- id: '1',
- email: 'admin@mim4u.org',
- role: 'admin',
- name: 'Admin User',
- lastLogin: new Date(),
- permissions: ['all']
- },
- 'volunteer@mim4u.org': {
- id: '2',
- email: 'volunteer@mim4u.org',
- role: 'volunteer',
- name: 'Volunteer User',
- lastLogin: new Date(),
- permissions: ['view_requests', 'update_assignments']
- }
- }
-
- const authenticatedUser = mockUsers[email]
- if (authenticatedUser && password === 'demo123') {
- setUser(authenticatedUser)
- localStorage.setItem('authToken', `token-${authenticatedUser.id}`)
- console.log('✅ Login successful')
- return true
- } else {
- console.log('❌ Login failed')
- return false
- }
- } catch (error) {
- console.error('Login error:', error)
+ const { token, user: authenticated } = await loginApi(email, password)
+ setAuthToken(token)
+ setUser(authenticated)
+ return true
+ } catch {
return false
} finally {
setIsLoading(false)
@@ -75,29 +52,20 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
const logout = (): void => {
setUser(null)
- localStorage.removeItem('authToken')
- console.log('👋 User logged out')
- }
-
- const value: AuthContextType = {
- user,
- login,
- logout,
- isLoading
+ setAuthToken(null)
}
return (
-
+
{children}
)
}
-// Custom hook for using auth context
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
-}
\ No newline at end of file
+}
diff --git a/src/hooks/usePublicConfig.ts b/src/hooks/usePublicConfig.ts
new file mode 100644
index 0000000..a22b02f
--- /dev/null
+++ b/src/hooks/usePublicConfig.ts
@@ -0,0 +1,42 @@
+import { useEffect, useState } from 'react'
+import { fetchPublicConfig, isStripeConfigured, type PublicConfig } from '../lib/mimApi'
+
+const FALLBACK: PublicConfig = {
+ donationsEnabled: false,
+ stripeConfigured: false,
+ stripePublishableKey: import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || '',
+}
+
+export function usePublicConfig() {
+ const [config, setConfig] = useState(() => ({
+ ...FALLBACK,
+ stripeConfigured: isStripeConfigured(),
+ donationsEnabled: isStripeConfigured(),
+ }))
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ let cancelled = false
+ fetchPublicConfig()
+ .then((c) => {
+ if (!cancelled) setConfig(c)
+ })
+ .catch(() => {
+ if (!cancelled) {
+ setConfig({
+ donationsEnabled: isStripeConfigured(),
+ stripeConfigured: isStripeConfigured(),
+ stripePublishableKey: import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || '',
+ })
+ }
+ })
+ .finally(() => {
+ if (!cancelled) setLoading(false)
+ })
+ return () => {
+ cancelled = true
+ }
+ }, [])
+
+ return { config, loading, stripeLive: config.donationsEnabled && config.stripeConfigured }
+}
diff --git a/src/index.css b/src/index.css
index 924ae51..d66f7cb 100644
--- a/src/index.css
+++ b/src/index.css
@@ -10,6 +10,10 @@
/* Official MIM brand palette (logo: forest green + gold) */
:root {
+ --mim-header-bg: #023b2b;
+ --mim-header-gold: #d9aa45;
+ --mim-header-logo-gold-filter: brightness(0) saturate(100%) invert(77%) sepia(42%)
+ saturate(561%) hue-rotate(6deg) brightness(96%) contrast(89%);
--mim-brand-green: #1a3c34;
--mim-brand-green-deep: #0f2922;
--mim-brand-green-mid: #2d6b5c;
@@ -135,6 +139,28 @@
/* Nav: symbol on mobile, transparent horizontal lockup on md+ */
.logo-mark--nav {
height: 3.25rem;
+ transform-style: preserve-3d;
+ will-change: transform;
+ animation: logo-nav-float 7s ease-in-out infinite;
+ transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1), filter 0.35s ease;
+ }
+
+ .site-header .logo-mark--nav .logo-mark__img {
+ filter: var(--mim-header-logo-gold-filter)
+ drop-shadow(0 2px 6px rgba(217, 170, 69, 0.28))
+ drop-shadow(0 8px 18px rgba(2, 59, 43, 0.35));
+ }
+
+ .site-header .nav-brand-link {
+ perspective: 720px;
+ transform-style: preserve-3d;
+ }
+
+ .site-header .nav-brand-link:hover .logo-mark--nav,
+ .site-header .nav-brand-link:focus-visible .logo-mark--nav {
+ animation-play-state: paused;
+ transform: rotateY(-10deg) rotateX(5deg) translateZ(10px) scale(1.03);
+ filter: drop-shadow(0 4px 10px rgba(217, 170, 69, 0.42));
}
.logo-mark--nav .logo-mark__img-wrap--symbol,
@@ -246,6 +272,40 @@
object-fit: cover;
}
+ .site-header .navlink {
+ color: rgba(255, 254, 251, 0.9);
+ }
+
+ .site-header .navlink:hover,
+ .site-header .navlink:focus-visible {
+ color: var(--mim-header-gold);
+ }
+
+ .site-header .mim-site-nav button {
+ border-color: rgba(255, 255, 255, 0.18);
+ background-color: rgba(255, 255, 255, 0.08);
+ color: rgba(255, 254, 251, 0.95);
+ }
+
+ .site-header .mim-site-nav button:hover {
+ border-color: rgba(217, 170, 69, 0.45);
+ background-color: rgba(255, 255, 255, 0.14);
+ }
+
+ .site-header .mobile-nav-panel {
+ border-color: rgba(217, 170, 69, 0.22);
+ background-color: var(--mim-header-bg);
+ }
+
+ .site-header .mobile-nav-panel a {
+ color: rgba(255, 254, 251, 0.95);
+ }
+
+ .site-header .mobile-nav-panel a:hover,
+ .site-header .mobile-nav-panel a:focus-visible {
+ color: var(--mim-header-gold);
+ }
+
.nav-brand-link {
display: inline-flex;
align-items: center;
@@ -283,6 +343,10 @@
outline-offset: 3px;
}
+ .site-header .nav-brand-link:focus-visible {
+ outline-color: var(--mim-header-gold);
+ }
+
.footer-brand-block {
display: flex;
flex-direction: column;
@@ -598,6 +662,19 @@
}
}
+ @keyframes logo-nav-float {
+ 0%,
+ 100% {
+ transform: translateY(0) rotateX(0deg) rotateY(0deg);
+ }
+ 35% {
+ transform: translateY(-2px) rotateX(2.5deg) rotateY(-4deg);
+ }
+ 70% {
+ transform: translateY(1px) rotateX(-1.5deg) rotateY(3deg);
+ }
+ }
+
.focus-visible\:ring-2:focus-visible {
outline: 2px solid transparent;
outline-offset: 2px;
@@ -627,10 +704,17 @@
.animate-marquee,
.animate-float,
- .animate-pulse-slow {
+ .animate-pulse-slow,
+ .logo-mark--nav {
animation: none;
}
+ .site-header .nav-brand-link:hover .logo-mark--nav,
+ .site-header .nav-brand-link:focus-visible .logo-mark--nav {
+ transform: none;
+ filter: none;
+ }
+
/* Framer Motion loops/parallax gated in JS via MotionConfig + useMotionSafe */
}
diff --git a/src/lib/mimApi.ts b/src/lib/mimApi.ts
index 905809c..df6a1a1 100644
--- a/src/lib/mimApi.ts
+++ b/src/lib/mimApi.ts
@@ -1,10 +1,22 @@
const API_BASE = (import.meta.env.VITE_API_BASE_URL || '').replace(/\/$/, '')
+const AUTH_TOKEN_KEY = 'mimAuthToken'
function apiUrl(path: string): string {
const p = path.startsWith('/') ? path : `/${path}`
return API_BASE ? `${API_BASE}${p}` : p
}
+export function getAuthToken(): string | null {
+ if (typeof window === 'undefined') return null
+ return localStorage.getItem(AUTH_TOKEN_KEY)
+}
+
+export function setAuthToken(token: string | null) {
+ if (typeof window === 'undefined') return
+ if (token) localStorage.setItem(AUTH_TOKEN_KEY, token)
+ else localStorage.removeItem(AUTH_TOKEN_KEY)
+}
+
export class MimApiError extends Error {
constructor(
message: string,
@@ -16,13 +28,17 @@ export class MimApiError extends Error {
}
}
-export async function mimApiPost(path: string, body: unknown): Promise {
- const res = await fetch(apiUrl(path), {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
- body: JSON.stringify(body),
- })
- const data = await res.json().catch(() => ({}))
+function authHeaders(): Record {
+ const token = getAuthToken()
+ return token ? { Authorization: `Bearer ${token}` } : {}
+}
+
+async function parseJson(res: Response) {
+ return res.json().catch(() => ({}))
+}
+
+async function handleResponse(res: Response): Promise {
+ const data = await parseJson(res)
if (!res.ok) {
throw new MimApiError(
(data as { error?: string }).error || `Request failed (${res.status})`,
@@ -33,6 +49,55 @@ export async function mimApiPost(path: string, body: unknown): Promise {
return data as T
}
+export async function mimApiGet(path: string, auth = false): Promise {
+ const res = await fetch(apiUrl(path), {
+ method: 'GET',
+ headers: { Accept: 'application/json', ...(auth ? authHeaders() : {}) },
+ })
+ return handleResponse(res)
+}
+
+export async function mimApiPost(path: string, body: unknown, auth = false): Promise {
+ const res = await fetch(apiUrl(path), {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ ...(auth ? authHeaders() : {}),
+ },
+ body: JSON.stringify(body),
+ })
+ return handleResponse(res)
+}
+
+export async function mimApiPatch(path: string, body: unknown): Promise {
+ const res = await fetch(apiUrl(path), {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ ...authHeaders(),
+ },
+ body: JSON.stringify(body),
+ })
+ return handleResponse(res)
+}
+
+export interface PublicConfig {
+ donationsEnabled: boolean
+ stripeConfigured: boolean
+ stripePublishableKey: string
+}
+
+export interface AuthUser {
+ id: string
+ email: string
+ role: 'admin' | 'volunteer' | 'resource'
+ name: string
+ lastLogin?: string
+ permissions: string[]
+}
+
export function isStripeConfigured(): boolean {
return Boolean(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY)
}
@@ -41,6 +106,18 @@ export function allowMockDonate(): boolean {
return import.meta.env.VITE_ALLOW_MOCK_DONATE === '1'
}
+export async function fetchPublicConfig(): Promise {
+ return mimApiGet('/api/public/config')
+}
+
+export async function loginApi(email: string, password: string): Promise<{ token: string; user: AuthUser }> {
+ return mimApiPost('/api/auth/login', { email, password })
+}
+
+export async function fetchMe(): Promise<{ user: AuthUser }> {
+ return mimApiGet('/api/auth/me', true)
+}
+
export async function createCheckoutSession(payload: {
amount: number
recurring?: boolean
@@ -65,3 +142,322 @@ export async function submitContactForm(payload: {
}): Promise<{ ok: boolean; id: string; message: string }> {
return mimApiPost('/api/contact', payload)
}
+
+export interface AdminSettings {
+ donationsEnabled: boolean
+ stripePublishableKey: string
+ stripeSecretKey: string
+ stripeWebhookSecret: string
+ stripeConfigured: boolean
+ updatedAt: string | null
+ updatedBy: string | null
+}
+
+export async function fetchAdminSettings(): Promise {
+ return mimApiGet('/api/admin/settings', true)
+}
+
+export async function updateAdminSettings(patch: Partial): Promise {
+ return mimApiPatch('/api/admin/settings', patch)
+}
+
+export async function fetchAdminDashboard() {
+ return mimApiGet<{
+ pendingRequests: number
+ activeVolunteers: number
+ deliveriesToday: number
+ monthlyBudget: number
+ monthlySpent: number
+ monthlyDonationCents: number
+ donationCount: number
+ }>('/api/admin/dashboard', true)
+}
+
+export async function fetchAssistanceRequests(limit = 50) {
+ return mimApiGet<{ data: AssistanceRequestRow[]; total: number }>(`/api/admin/assistance-requests?limit=${limit}`, true)
+}
+
+export interface AssistanceRequestRow {
+ id: string
+ requestType: string
+ student: string
+ school: string
+ need: string
+ priority: string
+ status: string
+ contactName?: string
+ contactEmail?: string
+ contactPhone?: string
+ timeAgo: string
+ ts: string
+}
+
+export async function updateAssistanceRequest(id: string, patch: { status?: string; adminNotes?: string }) {
+ return mimApiPatch(`/api/admin/assistance-requests/${id}`, patch)
+}
+
+export async function fetchDonations(limit = 50) {
+ return mimApiGet<{ data: DonationRow[]; total: number }>(`/api/admin/donations?limit=${limit}`, true)
+}
+
+export interface DonationRow {
+ id: string
+ amountCents: number
+ amountUsd: string
+ email?: string
+ donorName?: string
+ anonymous?: boolean
+ ts: string
+ timeAgo: string
+}
+
+export async function fetchAnalyticsSummary() {
+ return mimApiGet('/api/admin/analytics/summary', true)
+}
+
+export async function fetchAnalyticsActivity() {
+ return mimApiGet<{ feed: ActivityFeedItem[] }>('/api/admin/analytics/activity', true)
+}
+
+export interface ActivityFeedItem {
+ type: string
+ title: string
+ detail: string
+ ts: string
+ timeAgo: string
+}
+
+export async function fetchRecentFeeds() {
+ return mimApiGet<{
+ donations: { id: string; amountUsd: string; email?: string; ts: string; timeAgo: string }[]
+ assistance: { id: string; type: string; student: string; school?: string; status: string; ts: string; timeAgo: string }[]
+ generatedAt: string
+ }>('/api/admin/feeds/recent', true)
+}
+
+export async function fetchStripeStatus() {
+ return mimApiGet<{
+ configured: boolean
+ donationsEnabled: boolean
+ publishableKeySet: boolean
+ livemode: boolean
+ balance: { available: { amount: number; currency: string }[]; pending: { amount: number; currency: string }[] } | null
+ recentCharges: {
+ id: string
+ amount: number
+ currency: string
+ status: string
+ created: number
+ receiptEmail: string | null
+ description: string | null
+ }[]
+ }>('/api/admin/stripe/status', true)
+}
+
+export async function fetchAdvancedAnalytics() {
+ return mimApiGet('/api/admin/analytics/advanced', true)
+}
+
+export async function fetchVolunteerSchedule() {
+ return mimApiGet<{ date: string; tasks: VolunteerTask[] }>('/api/volunteer/schedule', true)
+}
+
+export interface VolunteerTask {
+ id: string
+ time: string
+ task: string
+ location: string
+ students: number | null
+ status: string
+}
+
+export async function fetchVolunteerAssignments() {
+ return mimApiGet<{ data: VolunteerAssignment[] }>('/api/volunteer/assignments', true)
+}
+
+export interface VolunteerAssignment {
+ id: string
+ student: string
+ items: string
+ school: string
+ deadline: string
+ status: string
+}
+
+export async function completeVolunteerAssignment(id: string) {
+ return mimApiPatch(`/api/volunteer/assignments/${id}`, { status: 'completed' })
+}
+
+export async function fetchVolunteerStats() {
+ return mimApiGet<{
+ familiesHelped: number
+ kitsAssembled: number
+ deliveries: number
+ hoursVolunteered: number
+ }>('/api/volunteer/stats', true)
+}
+
+export async function fetchResourceRequests() {
+ return mimApiGet<{
+ data: { id: string; title: string; type: string; status: string; submitted: string; school?: string }[]
+ summary: { pending: number; approved: number; completed: number }
+ }>('/api/resource/requests', true)
+}
+
+export async function fetchDonationImpact() {
+ return mimApiGet<{ totalRaisedUsd: number; donationCount: number; familiesSupported: number }>(
+ '/api/public/donation-impact',
+ )
+}
+
+export async function fetchTrainingModules() {
+ return mimApiGet<{ modules: { id: string; title: string; progress: number; durationMin: number }[] }>(
+ '/api/admin/training/modules',
+ true,
+ )
+}
+
+export interface BrandColor {
+ name: string
+ hex: string
+ role: string
+}
+
+export interface BrandTypography {
+ name: string
+ use: string
+ source?: string
+}
+
+export interface BrandAsset {
+ title: string
+ path: string
+ format: string
+ visible?: boolean
+}
+
+export interface BrandGroup {
+ id: string
+ title: string
+ description: string
+ visible?: boolean
+ assets: BrandAsset[]
+}
+
+export interface BrandManifest {
+ version: string
+ organization: string
+ updated: string
+ published?: boolean
+ colors: BrandColor[]
+ typography: BrandTypography[]
+ kits: { id: string; title: string; path: string; format: string; description?: string }[]
+ groups: BrandGroup[]
+ usageNotes?: string
+ message?: string
+}
+
+export async function fetchPublicBrand(): Promise {
+ return mimApiGet('/api/public/brand')
+}
+
+export async function fetchAdminBrand(): Promise<{
+ manifest: BrandManifest
+ files: { name: string; size: number; mtime: string }[]
+}> {
+ return mimApiGet('/api/admin/brand', true)
+}
+
+export async function saveBrandManifest(manifest: BrandManifest): Promise<{ ok: boolean; manifest: BrandManifest }> {
+ return mimApiPatch('/api/admin/brand', manifest)
+}
+
+export async function uploadBrandFile(file: File): Promise<{
+ ok: boolean
+ filename: string
+ path: string
+ legacyPath: string
+}> {
+ const form = new FormData()
+ form.append('file', file)
+ const token = getAuthToken()
+ const res = await fetch(apiUrl('/api/admin/brand/upload'), {
+ method: 'POST',
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
+ body: form,
+ })
+ return handleResponse(res)
+}
+
+export async function deleteBrandFile(filename: string): Promise<{ ok: boolean }> {
+ const token = getAuthToken()
+ const res = await fetch(apiUrl(`/api/admin/brand/files/${encodeURIComponent(filename)}`), {
+ method: 'DELETE',
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
+ })
+ return handleResponse(res)
+}
+
+export async function rebuildBrandZip(): Promise<{ ok: boolean; path?: string; fileCount?: number; error?: string }> {
+ return mimApiPost('/api/admin/brand/rebuild-zip', {}, true)
+}
+
+export interface AdminQrcode {
+ id: string
+ type?: string
+ title?: string
+ status?: string
+ url?: string
+ shortUrl?: string
+ previewUrl?: string
+ createdAt?: string
+ purpose?: string | null
+ provider?: string
+ imageUrl?: string | null
+ scans?: { total?: number; unique?: number }
+}
+
+export interface AdminQrcodeStatus {
+ provider?: 'qrcode-monkey' | 'qrcg'
+ configured: boolean
+ dynamicTracking?: boolean
+ rapidApiKeySet?: boolean
+ rapidApiSubscribed?: boolean
+ rapidApiNote?: string
+ apiBase?: string
+ accountError?: string
+ presets?: string[]
+ docsUrl?: string
+ brand?: {
+ referenceImage?: string
+ logoUrl?: string
+ colors?: { background?: string; foreground?: string }
+ }
+}
+
+export async function fetchAdminQrcodeStatus(): Promise {
+ return mimApiGet('/api/admin/qrcodes/status', true)
+}
+
+export async function fetchAdminQrcodes(): Promise<{ data: AdminQrcode[]; pagination?: { hasMore?: boolean } }> {
+ return mimApiGet('/api/admin/qrcodes', true)
+}
+
+export async function createAdminQrcode(body: {
+ url: string
+ title: string
+ purpose?: string
+}): Promise {
+ return mimApiPost('/api/admin/qrcodes', body, true)
+}
+
+export async function createAdminQrcodePreset(preset: string): Promise {
+ return mimApiPost(`/api/admin/qrcodes/presets/${encodeURIComponent(preset)}`, {}, true)
+}
+
+export async function updateAdminQrcode(
+ id: string,
+ body: { status?: 'active' | 'paused'; url?: string; title?: string },
+): Promise {
+ return mimApiPatch(`/api/admin/qrcodes/${encodeURIComponent(id)}`, body)
+}
diff --git a/src/pages/BrandPage/BrandAssetsPage.tsx b/src/pages/BrandPage/BrandAssetsPage.tsx
index 94c9c99..f42c0be 100644
--- a/src/pages/BrandPage/BrandAssetsPage.tsx
+++ b/src/pages/BrandPage/BrandAssetsPage.tsx
@@ -1,32 +1,63 @@
+import { useEffect, useState } from 'react'
import { Download, Palette, Printer, Type } from 'lucide-react'
-import manifest from '../../../config/brand-assets.manifest.json'
+import fallbackManifest from '../../../config/brand-assets.manifest.json'
+import { fetchPublicBrand, type BrandManifest } from '../../lib/mimApi'
+import { SEOHead } from '../../components/SEO/SEOHead'
-type BrandManifest = typeof manifest
+function resolveAssetUrl(path: string) {
+ if (path.startsWith('http') || path.startsWith('/api/')) return path
+ return path
+}
export function BrandAssetsPage() {
- const data = manifest as BrandManifest
+ const [data, setData] = useState(fallbackManifest as BrandManifest)
+ const [source, setSource] = useState<'api' | 'static'>('static')
+
+ useEffect(() => {
+ fetchPublicBrand()
+ .then((m) => {
+ if (m.published === false) return
+ setData(m)
+ setSource('api')
+ })
+ .catch(() => {
+ setData(fallbackManifest as BrandManifest)
+ setSource('static')
+ })
+ }, [])
+
+ if (data.published === false) {
+ return (
+
+
+
Brand kit temporarily unavailable
+
{data.message || 'Please contact contact@mim4u.org for logo files.'}
+
+ )
+ }
+
+ const kitPath = data.kits?.[0]?.path || '/brand/MIM4U-Brand-Kit.zip'
return (
+
Press & partners
Brand assets
- Official logos, colors, and favicons for Miracles in Motion Foundation. Use only these
- files; do not recreate or recolor the mark.
+ Official logos, colors, and favicons for {data.organization}. Use only these files; do not recreate or recolor the mark.
+ {source === 'api' && (
+
Live kit · updated {data.updated}
+ )}
-
+
Download complete kit (ZIP)
-