main
Samuel Gbafa 2 months ago
parent b21e00fc79
commit 81db6207da
  1. 80
      index.html
  2. 8
      public/favicon.svg
  3. 48
      public/manifest.json
  4. 92
      public/og-image.html
  5. 31
      public/og-image.svg
  6. 46
      scripts/generate-og-image.js
  7. 32
      src/components/layout/DashboardLayout.tsx
  8. 58
      src/pages/ContactViewPage.tsx
  9. 17
      src/pages/OnboardingPage.tsx

@ -2,9 +2,85 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<!-- Primary Meta Tags -->
<title>Personal Network Manager - Build and manage your professional network</title>
<meta name="title" content="Personal Network Manager - Build and manage your professional network" />
<meta name="description" content="A modern platform to import, organize, and grow your professional network. Connect with LinkedIn, manage contacts, and expand your reach with QR invitations." />
<meta name="keywords" content="networking, professional network, contact management, LinkedIn integration, QR codes, social connections, career growth" />
<meta name="author" content="Personal Network Manager" />
<meta name="robots" content="index, follow" />
<meta name="language" content="English" />
<!-- Theme and PWA -->
<meta name="theme-color" content="#1976d2" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Network Manager" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://nao-pnm-ui.pages.dev/" />
<meta property="og:title" content="Personal Network Manager - Build and manage your professional network" />
<meta property="og:description" content="A modern platform to import, organize, and grow your professional network. Connect with LinkedIn, manage contacts, and expand your reach with QR invitations." />
<meta property="og:image" content="https://nao-pnm-ui.pages.dev/og-image.svg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:site_name" content="Personal Network Manager" />
<meta property="og:locale" content="en_US" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://nao-pnm-ui.pages.dev/" />
<meta property="twitter:title" content="Personal Network Manager - Build and manage your professional network" />
<meta property="twitter:description" content="A modern platform to import, organize, and grow your professional network. Connect with LinkedIn, manage contacts, and expand your reach with QR invitations." />
<meta property="twitter:image" content="https://nao-pnm-ui.pages.dev/og-image.svg" />
<meta property="twitter:image:alt" content="Personal Network Manager - Professional networking made simple" />
<!-- Additional Meta Tags -->
<meta name="application-name" content="Personal Network Manager" />
<meta name="category" content="social" />
<meta name="coverage" content="worldwide" />
<meta name="distribution" content="global" />
<meta name="rating" content="general" />
<meta name="revisit-after" content="1 days" />
<!-- Canonical URL -->
<link rel="canonical" href="https://nao-pnm-ui.pages.dev/" />
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json" />
<!-- Preconnect for performance -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Personal Network Manager",
"description": "A modern platform to import, organize, and grow your professional network",
"url": "https://nao-pnm-ui.pages.dev/",
"applicationCategory": "BusinessApplication",
"operatingSystem": "Web",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"featureList": [
"LinkedIn integration",
"Contact management",
"QR code invitations",
"Professional networking"
],
"screenshot": "https://nao-pnm-ui.pages.dev/og-image.svg"
}
</script>
</head>
<body>
<div id="root"></div>

@ -0,0 +1,8 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="6" fill="#1976d2"/>
<circle cx="12" cy="12" r="3" fill="white"/>
<circle cx="20" cy="12" r="3" fill="white"/>
<circle cx="16" cy="20" r="3" fill="white"/>
<line x1="12" y1="15" x2="16" y2="17" stroke="white" stroke-width="2"/>
<line x1="20" y1="15" x2="16" y2="17" stroke="white" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 446 B

@ -0,0 +1,48 @@
{
"name": "Personal Network Manager",
"short_name": "Network Manager",
"description": "Build and manage your professional network with ease",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#1976d2",
"orientation": "portrait-primary",
"categories": ["social", "productivity", "business"],
"lang": "en",
"dir": "ltr",
"scope": "/",
"icons": [
{
"src": "/favicon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any maskable"
},
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"screenshots": [
{
"src": "/og-image.svg",
"sizes": "1200x630",
"type": "image/svg+xml",
"form_factor": "wide"
}
],
"features": [
"LinkedIn integration",
"Contact management",
"QR code invitations",
"Professional networking"
]
}

@ -0,0 +1,92 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
width: 1200px;
height: 630px;
display: flex;
align-items: center;
justify-content: center;
}
.container {
text-align: center;
color: white;
padding: 60px;
}
.logo {
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
margin: 0 auto 30px;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
font-weight: bold;
}
h1 {
font-size: 48px;
font-weight: 700;
margin: 0 0 20px 0;
line-height: 1.2;
}
.tagline {
font-size: 24px;
font-weight: 400;
margin: 0 0 40px 0;
opacity: 0.9;
line-height: 1.4;
}
.features {
display: flex;
justify-content: center;
gap: 40px;
margin-top: 40px;
}
.feature {
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
opacity: 0.9;
}
.icon {
width: 24px;
height: 24px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="logo">🌐</div>
<h1>Personal Network Manager</h1>
<p class="tagline">Build and manage your professional network with ease</p>
<div class="features">
<div class="feature">
<div class="icon">📊</div>
<span>LinkedIn Integration</span>
</div>
<div class="feature">
<div class="icon">👥</div>
<span>Contact Management</span>
</div>
<div class="feature">
<div class="icon">📱</div>
<span>QR Invitations</span>
</div>
</div>
</div>
</body>
</html>

@ -0,0 +1,31 @@
<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1976d2;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1565c0;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="1200" height="630" fill="url(#grad)"/>
<!-- Logo placeholder -->
<rect x="560" y="180" width="80" height="80" rx="16" fill="rgba(255,255,255,0.2)"/>
<text x="600" y="235" text-anchor="middle" fill="white" font-family="Arial, sans-serif" font-size="36" font-weight="bold">🌐</text>
<!-- Title -->
<text x="600" y="320" text-anchor="middle" fill="white" font-family="Arial, sans-serif" font-size="48" font-weight="bold">Personal Network Manager</text>
<!-- Tagline -->
<text x="600" y="370" text-anchor="middle" fill="rgba(255,255,255,0.9)" font-family="Arial, sans-serif" font-size="24">Build and manage your professional network with ease</text>
<!-- Features -->
<g transform="translate(300, 430)">
<text x="0" y="0" fill="rgba(255,255,255,0.9)" font-family="Arial, sans-serif" font-size="18">📊 LinkedIn Integration</text>
<text x="250" y="0" fill="rgba(255,255,255,0.9)" font-family="Arial, sans-serif" font-size="18">👥 Contact Management</text>
<text x="500" y="0" fill="rgba(255,255,255,0.9)" font-family="Arial, sans-serif" font-size="18">📱 QR Invitations</text>
</g>
<!-- URL -->
<text x="600" y="550" text-anchor="middle" fill="rgba(255,255,255,0.7)" font-family="Arial, sans-serif" font-size="16">nao-pnm-ui.pages.dev</text>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -0,0 +1,46 @@
// Simple script to generate OG image
// This creates a placeholder - in production you'd use a proper image generation service
const fs = require('fs');
const path = require('path');
// Create a simple SVG that can be used as OG image
const svgContent = `
<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1976d2;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1565c0;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="1200" height="630" fill="url(#grad)"/>
<!-- Logo placeholder -->
<rect x="560" y="180" width="80" height="80" rx="16" fill="rgba(255,255,255,0.2)"/>
<text x="600" y="235" text-anchor="middle" fill="white" font-family="Arial, sans-serif" font-size="36" font-weight="bold">🌐</text>
<!-- Title -->
<text x="600" y="320" text-anchor="middle" fill="white" font-family="Arial, sans-serif" font-size="48" font-weight="bold">Personal Network Manager</text>
<!-- Tagline -->
<text x="600" y="370" text-anchor="middle" fill="rgba(255,255,255,0.9)" font-family="Arial, sans-serif" font-size="24">Build and manage your professional network with ease</text>
<!-- Features -->
<g transform="translate(300, 430)">
<text x="0" y="0" fill="rgba(255,255,255,0.9)" font-family="Arial, sans-serif" font-size="18">📊 LinkedIn Integration</text>
<text x="250" y="0" fill="rgba(255,255,255,0.9)" font-family="Arial, sans-serif" font-size="18">👥 Contact Management</text>
<text x="500" y="0" fill="rgba(255,255,255,0.9)" font-family="Arial, sans-serif" font-size="18">📱 QR Invitations</text>
</g>
<!-- URL -->
<text x="600" y="550" text-anchor="middle" fill="rgba(255,255,255,0.7)" font-family="Arial, sans-serif" font-size="16">nao-pnm-ui.pages.dev</text>
</svg>
`;
// Write the SVG file
fs.writeFileSync(path.join(__dirname, '../public/og-image.svg'), svgContent);
console.log('OG image generated successfully!');
console.log('Note: For production, convert this SVG to PNG for better social media compatibility');

@ -48,7 +48,7 @@ interface DashboardLayoutProps {
const DashboardLayout = ({ children }: DashboardLayoutProps) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('lg'));
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [mobileOpen, setMobileOpen] = useState(false);
const [profileMenuAnchor, setProfileMenuAnchor] = useState<null | HTMLElement>(null);
const location = useLocation();
@ -173,12 +173,13 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
<AppBar
position="fixed"
sx={{
width: { lg: `calc(100% - ${drawerWidth}px)` },
ml: { lg: `${drawerWidth}px` },
width: { md: `calc(100% - ${drawerWidth}px)` },
ml: { md: `${drawerWidth}px` },
backgroundColor: 'background.paper',
borderBottom: 1,
borderColor: 'divider',
boxShadow: 'none',
zIndex: theme.zIndex.drawer + 1,
}}
>
<Toolbar sx={{ justifyContent: 'space-between' }}>
@ -188,10 +189,22 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { lg: 'none' } }}
sx={{ mr: 2, display: { md: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography
variant="h6"
noWrap
component="div"
sx={{
display: { xs: 'block', md: 'none' },
fontWeight: 600,
color: 'text.primary'
}}
>
Network Manager
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
@ -225,7 +238,7 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
<Box
component="nav"
sx={{ width: { lg: drawerWidth }, flexShrink: { lg: 0 } }}
sx={{ width: { md: drawerWidth }, flexShrink: { md: 0 } }}
>
<Drawer
variant={isMobile ? 'temporary' : 'permanent'}
@ -240,6 +253,7 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
width: drawerWidth,
borderRight: 1,
borderColor: 'divider',
zIndex: isMobile ? theme.zIndex.drawer : theme.zIndex.drawer - 1,
},
}}
>
@ -251,13 +265,17 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
component="main"
sx={{
flexGrow: 1,
width: { lg: `calc(100% - ${drawerWidth}px)` },
width: { md: `calc(100% - ${drawerWidth}px)` },
minHeight: '100vh',
backgroundColor: 'background.default',
}}
>
<Toolbar />
<Box sx={{ p: 3, height: 'calc(100vh - 64px)', overflow: 'auto' }}>
<Box sx={{
p: { xs: 2, md: 3 },
height: 'calc(100vh - 64px)',
overflow: 'auto'
}}>
{children}
</Box>
</Box>

@ -1,7 +1,6 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Container,
Typography,
Box,
Paper,
@ -105,28 +104,34 @@ const ContactViewPage = () => {
if (isLoading) {
return (
<Container maxWidth="md" sx={{ py: 3 }}>
<Box sx={{ height: '100%' }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
<Skeleton variant="circular" width={40} height={40} />
<Skeleton variant="text" width={200} height={40} sx={{ ml: 2 }} />
</Box>
<Paper sx={{ p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
<Skeleton variant="circular" width={120} height={120} />
<Box sx={{ ml: 3, flex: 1 }}>
<Paper sx={{ p: { xs: 2, md: 3 } }}>
<Box sx={{
display: 'flex',
alignItems: 'center',
mb: 3,
flexDirection: { xs: 'column', sm: 'row' },
textAlign: { xs: 'center', sm: 'left' }
}}>
<Skeleton variant="circular" width={120} height={120} sx={{ mb: { xs: 2, sm: 0 } }} />
<Box sx={{ ml: { xs: 0, sm: 3 }, flex: 1 }}>
<Skeleton variant="text" width={200} height={40} />
<Skeleton variant="text" width={300} height={24} />
<Skeleton variant="text" width={250} height={24} />
</Box>
</Box>
</Paper>
</Container>
</Box>
);
}
if (error || !contact) {
return (
<Container maxWidth="md" sx={{ py: 3 }}>
<Box sx={{ height: '100%' }}>
<Button
startIcon={<ArrowBack />}
onClick={handleBack}
@ -137,14 +142,14 @@ const ContactViewPage = () => {
<Alert severity="error">
{error || 'Contact not found'}
</Alert>
</Container>
</Box>
);
}
const sourceDetails = getSourceDetails(contact.source);
return (
<Container maxWidth="md" sx={{ py: 3 }}>
<Box sx={{ height: '100%' }}>
<Button
startIcon={<ArrowBack />}
onClick={handleBack}
@ -153,12 +158,24 @@ const ContactViewPage = () => {
Back to Contacts
</Button>
<Paper sx={{ p: 3, mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', mb: 3 }}>
<Paper sx={{ p: { xs: 2, md: 3 }, mb: 3 }}>
<Box sx={{
display: 'flex',
alignItems: 'flex-start',
mb: 3,
flexDirection: { xs: 'column', sm: 'row' },
textAlign: { xs: 'center', sm: 'left' }
}}>
<Avatar
src={contact.profileImage}
alt={contact.name}
sx={{ width: 120, height: 120, mr: 3 }}
sx={{
width: { xs: 100, sm: 120 },
height: { xs: 100, sm: 120 },
mr: { xs: 0, sm: 3 },
mb: { xs: 2, sm: 0 },
mx: { xs: 'auto', sm: 0 }
}}
>
{contact.name.charAt(0)}
</Avatar>
@ -184,13 +201,18 @@ const ContactViewPage = () => {
/>
</Box>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', mb: 2 }}>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', mb: 2, justifyContent: { xs: 'center', sm: 'flex-start' } }}>
{contact.tags?.map((tag) => (
<Chip key={tag} label={tag} size="small" />
))}
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
<Box sx={{
display: 'flex',
gap: 1,
flexWrap: 'wrap',
justifyContent: { xs: 'center', sm: 'flex-start' }
}}>
<Button variant="outlined" startIcon={<Edit />} size="small">
Edit
</Button>
@ -209,7 +231,7 @@ const ContactViewPage = () => {
<Grid container spacing={3}>
<Grid size={{ xs: 12, md: 6 }}>
<Card variant="outlined">
<CardContent>
<CardContent sx={{ p: { xs: 2, md: 3 } }}>
<Typography variant="h6" gutterBottom>
Contact Information
</Typography>
@ -286,7 +308,7 @@ const ContactViewPage = () => {
<Grid size={{ xs: 12, md: 6 }}>
<Card variant="outlined">
<CardContent>
<CardContent sx={{ p: { xs: 2, md: 3 } }}>
<Typography variant="h6" gutterBottom>
Additional Information
</Typography>
@ -330,7 +352,7 @@ const ContactViewPage = () => {
</Grid>
</Grid>
</Paper>
</Container>
</Box>
);
};

@ -1,7 +1,6 @@
import { useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import {
Container,
Typography,
Box,
Paper,
@ -66,7 +65,7 @@ const OnboardingPage = () => {
const progress = ((state.currentStep + 1) / state.totalSteps) * 100;
return (
<Container maxWidth="md" sx={{ py: 4 }}>
<Box sx={{ height: '100%', maxWidth: 'md', mx: 'auto' }}>
<Box sx={{ textAlign: 'center', mb: 4 }}>
<Typography variant="h3" component="h1" gutterBottom>
Welcome to Your Network
@ -81,7 +80,7 @@ const OnboardingPage = () => {
)}
</Box>
<Paper sx={{ p: 3, mb: 3 }}>
<Paper sx={{ p: { xs: 2, md: 3 }, mb: 3 }}>
<Box sx={{ mb: 3 }}>
<LinearProgress variant="determinate" value={progress} sx={{ mb: 2 }} />
<Typography variant="body2" color="text.secondary" align="center">
@ -101,11 +100,18 @@ const OnboardingPage = () => {
{steps[state.currentStep]?.component}
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 4 }}>
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
mt: 4,
flexDirection: { xs: 'column', sm: 'row' },
gap: { xs: 2, sm: 0 }
}}>
<Button
onClick={handleBack}
startIcon={<ArrowBack />}
variant="outlined"
sx={{ width: { xs: '100%', sm: 'auto' } }}
>
{state.currentStep === 0 ? 'Cancel' : 'Back'}
</Button>
@ -115,12 +121,13 @@ const OnboardingPage = () => {
endIcon={<ArrowForward />}
variant="contained"
disabled={isNextDisabled()}
sx={{ width: { xs: '100%', sm: 'auto' } }}
>
{state.currentStep === state.totalSteps - 1 ? 'Complete' : 'Next'}
</Button>
</Box>
</Paper>
</Container>
</Box>
);
};

Loading…
Cancel
Save