feat: add BusinessCard component and related subcomponents for displaying business card information
- Implemented BusinessCard component to aggregate various card sections. - Created CardHeader, CardProfile, CardBio, CardContactInfo, CardFooter components for structured layout. - Added styles for each component to ensure responsive design. - Introduced SaveContactButton for downloading vCard and QRCodeDisplay for generating QR codes. - Configured context for managing business card data with BusinessCardProvider and BusinessCardContext. - Added local configuration files for easy customization of business card data. - Established types for business card and social links to ensure type safety. - Set up TypeScript configuration for the project. - Integrated Vite for development and build processes.
This commit is contained in:
commit
629da34064
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
# Git Setup Guide
|
||||||
|
|
||||||
|
## Pushing Your Project to Git
|
||||||
|
|
||||||
|
### Initial Setup
|
||||||
|
|
||||||
|
1. **Initialize Git** (if not already done):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git init
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Add a Remote** (replace with your repository URL):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git remote add origin https://github.com/yourusername/yourrepo.git
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create Initial Commit**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Initial commit: Virtual Business Card"
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Push to Remote**:
|
||||||
|
```bash
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local Configuration Safety
|
||||||
|
|
||||||
|
✅ **What Gets Pushed to Git:**
|
||||||
|
|
||||||
|
- Default business card config: `src/config/businessCard.config.ts`
|
||||||
|
- All source code and components
|
||||||
|
- Configuration and build files
|
||||||
|
- Documentation
|
||||||
|
|
||||||
|
❌ **What Stays Local (Never Pushed):**
|
||||||
|
|
||||||
|
- Your personal business card config: `src/config/businessCard.config.local.ts`
|
||||||
|
- Node modules: `node_modules/`
|
||||||
|
- Build output: `dist/`
|
||||||
|
- Other files matching `*.local` pattern
|
||||||
|
|
||||||
|
### How `.gitignore` Protects Your Data
|
||||||
|
|
||||||
|
The `.gitignore` file already includes:
|
||||||
|
|
||||||
|
```
|
||||||
|
*.local
|
||||||
|
```
|
||||||
|
|
||||||
|
This means any file ending with `.local` is automatically excluded from git. Your personal business card configuration in `businessCard.config.local.ts` will never be tracked or pushed.
|
||||||
|
|
||||||
|
## Workflow for Sharing the Project
|
||||||
|
|
||||||
|
### For You (Developer)
|
||||||
|
|
||||||
|
1. Create `src/config/businessCard.config.local.ts` with your personal info
|
||||||
|
2. Make changes to your local config as needed
|
||||||
|
3. Push code changes to git (your personal config stays local)
|
||||||
|
|
||||||
|
### For Others Using Your Repository
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
2. Copy `src/config/businessCard.config.local.example.ts` to `src/config/businessCard.config.local.ts`
|
||||||
|
3. Edit the local config with their own information
|
||||||
|
4. Run `npm install --legacy-peer-deps && npm run dev`
|
||||||
|
5. Their changes to `businessCard.config.local.ts` stay local
|
||||||
|
|
||||||
|
## Checking Git Status
|
||||||
|
|
||||||
|
To verify your personal config is being ignored:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status
|
||||||
|
```
|
||||||
|
|
||||||
|
You should NOT see `src/config/businessCard.config.local.ts` in the output. If you do, your `.gitignore` may need adjustment.
|
||||||
|
|
||||||
|
To also check git tracking:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git check-ignore src/config/businessCard.config.local.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
This should print the file path if it's properly ignored.
|
||||||
|
|
||||||
|
## Example Git Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check what will be committed
|
||||||
|
git status
|
||||||
|
|
||||||
|
# Add all non-ignored files
|
||||||
|
git add .
|
||||||
|
|
||||||
|
# Commit changes
|
||||||
|
git commit -m "Update business card styles"
|
||||||
|
|
||||||
|
# Push to remote
|
||||||
|
git push
|
||||||
|
|
||||||
|
# View git history
|
||||||
|
git log
|
||||||
|
|
||||||
|
# See what files are being tracked
|
||||||
|
git ls-files
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- Always ensure `businessCard.config.local.ts` is in `.gitignore` before pushing
|
||||||
|
- Never commit personal business card data
|
||||||
|
- The example file `businessCard.config.local.example.ts` CAN be committed to help others
|
||||||
|
- Each user/clone should have their own local config file
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
# Local Configuration Setup
|
||||||
|
|
||||||
|
This project supports local configuration for your business card data without committing it to git.
|
||||||
|
|
||||||
|
## How to Use Local Configuration
|
||||||
|
|
||||||
|
### 1. Copy the Example File
|
||||||
|
|
||||||
|
Copy `src/config/businessCard.config.local.example.ts` and rename it to `src/config/businessCard.config.local.ts`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp src/config/businessCard.config.local.example.ts src/config/businessCard.config.local.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Edit Your Local Config
|
||||||
|
|
||||||
|
Open `src/config/businessCard.config.local.ts` and update all the fields with your information:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const localBusinessCard: BusinessCard = {
|
||||||
|
name: 'Your Name',
|
||||||
|
title: 'Your Job Title',
|
||||||
|
company: 'Your Company Name',
|
||||||
|
email: 'your.email@example.com',
|
||||||
|
phone: '+1 (555) 000-0000',
|
||||||
|
website: 'https://yourwebsite.com',
|
||||||
|
image: 'https://your-image-url.com/photo.jpg',
|
||||||
|
bio: 'Your professional bio goes here.',
|
||||||
|
socialLinks: [
|
||||||
|
{
|
||||||
|
platform: 'LinkedIn',
|
||||||
|
url: 'https://linkedin.com/in/yourprofile',
|
||||||
|
icon: 'Linkedin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: 'Twitter',
|
||||||
|
url: 'https://twitter.com/yourhandle',
|
||||||
|
icon: 'Twitter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: 'GitHub',
|
||||||
|
url: 'https://github.com/yourprofile',
|
||||||
|
icon: 'Github',
|
||||||
|
},
|
||||||
|
{ platform: 'Portfolio', url: 'https://yourwebsite.com', icon: 'Globe' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. No Additional Configuration Needed
|
||||||
|
|
||||||
|
The app will automatically detect and load your local config when it starts. No need to modify any other files!
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
- **Default Config**: `src/config/businessCard.config.ts` contains default demo data and **IS tracked by git**
|
||||||
|
- **Local Config**: `src/config/businessCard.config.local.ts` is created by you and **is NOT tracked by git** (added to `.gitignore`)
|
||||||
|
- **On Startup**: The app tries to load your local config; if not found, it falls back to the default
|
||||||
|
- **Your Data is Safe**: All your personal information stays local and never gets pushed to git
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/config/
|
||||||
|
├── businessCard.config.ts # Default config (git-tracked)
|
||||||
|
├── businessCard.config.local.example.ts # Template for you to copy
|
||||||
|
└── businessCard.config.local.ts # Your local config (git-ignored) ← Create this!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
- **Image URLs**: Use absolute URLs (https://...) for profile images
|
||||||
|
- **Phone Format**: Any format works - the phone number becomes a clickable tel: link
|
||||||
|
- **Social Icons**: Use the icon names from lucide-react:
|
||||||
|
- `Linkedin`, `Twitter`, `Github`, `Globe`, `Mail`, `Phone`
|
||||||
|
- **Testing Locally**: `npm run dev` will load your local config automatically
|
||||||
|
|
||||||
|
## Git Safety
|
||||||
|
|
||||||
|
The `.gitignore` file already includes `*.local` entries, so:
|
||||||
|
|
||||||
|
- ✅ Your `businessCard.config.local.ts` will NEVER be committed
|
||||||
|
- ✅ Only `businessCard.config.ts` (with demo data) is tracked by git
|
||||||
|
- ✅ You can safely add personal/sensitive information to your local config
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
# Virtual Business Card 🎴
|
||||||
|
|
||||||
|
A modern, responsive digital business card application built with React, TypeScript, and Vite. Perfect for sharing your professional information with a clean, professional design.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
✨ **Responsive Design** - Works beautifully on all screen sizes (mobile, tablet, desktop)
|
||||||
|
💾 **Save Contact** - Download contact information as .vcf file (vCard format)
|
||||||
|
🎨 **Modern UI** - Clean, professional design with smooth animations
|
||||||
|
⚙️ **Easy Configuration** - Simple config system with local overrides
|
||||||
|
🔒 **Privacy-First** - Local configuration stays local, never pushed to git
|
||||||
|
📱 **Mobile-Optimized** - Touch-friendly buttons and layout
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --legacy-peer-deps
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The app will be available at `http://localhost:5173/`
|
||||||
|
|
||||||
|
### Build for Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Using Default Configuration
|
||||||
|
|
||||||
|
The app comes with demo data in `src/config/businessCard.config.ts`. This is tracked by git and safe to use as a template.
|
||||||
|
|
||||||
|
### Using Local Configuration (Recommended)
|
||||||
|
|
||||||
|
To customize the business card with your own information:
|
||||||
|
|
||||||
|
1. Copy the example config:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp src/config/businessCard.config.local.example.ts src/config/businessCard.config.local.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Edit `src/config/businessCard.config.local.ts` with your information
|
||||||
|
|
||||||
|
3. The app automatically loads your local config on startup
|
||||||
|
|
||||||
|
**Important**: `businessCard.config.local.ts` is git-ignored, so your personal data never gets pushed to git!
|
||||||
|
|
||||||
|
See [LOCAL_CONFIG_SETUP.md](./LOCAL_CONFIG_SETUP.md) for detailed instructions.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/ # UI components
|
||||||
|
│ ├── BusinessCard.tsx
|
||||||
|
│ ├── SocialLinks.tsx
|
||||||
|
│ ├── BusinessCard.module.css
|
||||||
|
│ └── SocialLinks.module.css
|
||||||
|
├── config/ # Configuration files
|
||||||
|
│ ├── businessCard.config.ts # Default config (git-tracked)
|
||||||
|
│ ├── businessCard.config.local.example.ts # Template
|
||||||
|
│ └── configLoader.ts # Config loading logic
|
||||||
|
├── store/ # State management
|
||||||
|
│ ├── BusinessCardContext.tsx
|
||||||
|
│ ├── BusinessCardProvider.tsx
|
||||||
|
│ └── types.ts
|
||||||
|
├── App.tsx
|
||||||
|
├── App.css
|
||||||
|
├── main.tsx
|
||||||
|
└── index.css
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
- `npm run dev` - Start development server
|
||||||
|
- `npm run build` - Build for production
|
||||||
|
- `npm run preview` - Preview production build
|
||||||
|
- `npm run lint` - Run ESLint
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
- **React 19** - UI library
|
||||||
|
- **TypeScript** - Type safety
|
||||||
|
- **Vite 7** - Build tool
|
||||||
|
- **Lucide React** - Icon library
|
||||||
|
- **CSS Modules** - Component styling
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Business Card Data
|
||||||
|
|
||||||
|
Edit `src/config/businessCard.config.local.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const localBusinessCard: BusinessCard = {
|
||||||
|
name: 'Your Name',
|
||||||
|
title: 'Your Title',
|
||||||
|
company: 'Your Company',
|
||||||
|
email: 'your.email@example.com',
|
||||||
|
phone: '+1 (555) 000-0000',
|
||||||
|
website: 'https://yourwebsite.com',
|
||||||
|
image: 'https://your-image-url.com/photo.jpg',
|
||||||
|
bio: 'Your professional bio',
|
||||||
|
socialLinks: [
|
||||||
|
{
|
||||||
|
platform: 'LinkedIn',
|
||||||
|
url: 'https://linkedin.com/in/you',
|
||||||
|
icon: 'Linkedin',
|
||||||
|
},
|
||||||
|
{ platform: 'Twitter', url: 'https://twitter.com/you', icon: 'Twitter' },
|
||||||
|
// ... more links
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
|
||||||
|
All components use CSS Modules. Edit the respective `.module.css` files:
|
||||||
|
|
||||||
|
- `src/components/BusinessCard.module.css` - Card styling
|
||||||
|
- `src/components/SocialLinks.module.css` - Social icons styling
|
||||||
|
- `src/App.css` - App-level styling
|
||||||
|
- `src/index.css` - Global styles
|
||||||
|
|
||||||
|
## Responsive Breakpoints
|
||||||
|
|
||||||
|
The card adapts to all screen sizes:
|
||||||
|
|
||||||
|
- **320px - 479px** - Extra small (phones)
|
||||||
|
- **480px - 767px** - Small (large phones, small tablets)
|
||||||
|
- **768px - 1023px** - Medium (tablets)
|
||||||
|
- **1024px+** - Large (desktops)
|
||||||
|
|
||||||
|
## Browser Support
|
||||||
|
|
||||||
|
- Chrome (latest)
|
||||||
|
- Firefox (latest)
|
||||||
|
- Safari (latest)
|
||||||
|
- Edge (latest)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Feel free to fork and customize this project for your needs!
|
||||||
|
|
||||||
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x'
|
||||||
|
import reactDom from 'eslint-plugin-react-dom'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
// Enable lint rules for React
|
||||||
|
reactX.configs['recommended-typescript'],
|
||||||
|
// Enable lint rules for React DOM
|
||||||
|
reactDom.configs.recommended,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>virtualbusinesscard</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"name": "virtualbusinesscard",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lucide-react": "^0.408.0",
|
||||||
|
"qrcode.react": "^1.0.1",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"react-router-dom": "^6.22.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/react": "^19.2.7",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"typescript-eslint": "^8.48.0",
|
||||||
|
"vite": "^7.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
background: linear-gradient(135deg, #e0e7ef 0%, #91c5e0 100%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra Small Screens (320px - 479px) */
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.content-wrapper {
|
||||||
|
padding: 12px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small Screens (480px - 767px) */
|
||||||
|
@media (min-width: 480px) and (max-width: 767px) {
|
||||||
|
.content-wrapper {
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Medium Screens (768px - 1023px) */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
.content-wrapper {
|
||||||
|
padding: 24px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Large Screens (1024px+) */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.content-wrapper {
|
||||||
|
padding: 40px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { BusinessCard } from './components/BusinessCard';
|
||||||
|
import './App.css';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<div className="content-wrapper">
|
||||||
|
<BusinessCard />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
|
|
@ -0,0 +1,132 @@
|
||||||
|
.cardContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
transform 0.3s ease,
|
||||||
|
box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialSection {
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionSection {
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #e2e8f0;
|
||||||
|
margin: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra Small Screens (320px - 479px) */
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.cardContainer {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialSection {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionSection {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
margin: 0 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small Screens (480px - 767px) */
|
||||||
|
@media (min-width: 480px) and (max-width: 767px) {
|
||||||
|
.cardContainer {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
max-width: 360px;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialSection {
|
||||||
|
padding: 0 18px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionSection {
|
||||||
|
padding: 0 18px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
margin: 0 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Medium Screens (768px - 1023px) */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
.cardContainer {
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
max-width: 390px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialSection {
|
||||||
|
padding: 0 22px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionSection {
|
||||||
|
padding: 0 22px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
margin: 0 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Large Screens (1024px+) */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.cardContainer {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialSection {
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionSection {
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
margin: 0 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { useBusinessCard } from '../store/BusinessCardContext';
|
||||||
|
import { CardHeader } from './Card/CardHeader';
|
||||||
|
import { CardProfile } from './Card/CardProfile';
|
||||||
|
import { CardBio } from './Card/CardBio';
|
||||||
|
import { CardContactInfo } from './Card/CardContactInfo';
|
||||||
|
import { SocialLinks } from './SocialLinks';
|
||||||
|
import { SaveContactButton } from './SaveContactButton';
|
||||||
|
import { CardFooter } from './Card/CardFooter';
|
||||||
|
import styles from './BusinessCard.module.css';
|
||||||
|
|
||||||
|
export function BusinessCard() {
|
||||||
|
const { businessCard } = useBusinessCard();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.cardContainer}>
|
||||||
|
<div className={styles.card}>
|
||||||
|
<CardHeader />
|
||||||
|
<CardProfile businessCard={businessCard} />
|
||||||
|
<CardBio bio={businessCard.bio} />
|
||||||
|
<CardContactInfo businessCard={businessCard} />
|
||||||
|
|
||||||
|
{businessCard.socialLinks.length > 0 && (
|
||||||
|
<div className={styles.socialSection}>
|
||||||
|
<SocialLinks links={businessCard.socialLinks} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.actionSection}>
|
||||||
|
<SaveContactButton businessCard={businessCard} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.divider} />
|
||||||
|
<CardFooter companyName={businessCard.company} personName={businessCard.name} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
.bio {
|
||||||
|
padding: 0 20px;
|
||||||
|
margin: 0 0 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #4a5568;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra Small Screens (320px - 479px) */
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.bio {
|
||||||
|
padding: 0 16px;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small Screens (480px - 767px) */
|
||||||
|
@media (min-width: 480px) and (max-width: 767px) {
|
||||||
|
.bio {
|
||||||
|
padding: 0 18px;
|
||||||
|
margin: 0 0 18px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Medium Screens (768px - 1023px) */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
.bio {
|
||||||
|
padding: 0 22px;
|
||||||
|
margin: 0 0 22px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Large Screens (1024px+) */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.bio {
|
||||||
|
padding: 0 20px;
|
||||||
|
margin: 0 0 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import styles from './CardBio.module.css';
|
||||||
|
|
||||||
|
interface CardBioProps {
|
||||||
|
bio: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardBio({ bio }: CardBioProps) {
|
||||||
|
if (!bio) return null;
|
||||||
|
|
||||||
|
return <p className={styles.bio}>{bio}</p>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
.contactSection {
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contactItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f7fafc;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contactItem:hover {
|
||||||
|
background: #edf2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: #667eea;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: #2d3748;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 13px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:hover {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra Small Screens (320px - 479px) */
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.contactSection {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contactItem {
|
||||||
|
padding: 8px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small Screens (480px - 767px) */
|
||||||
|
@media (min-width: 480px) and (max-width: 767px) {
|
||||||
|
.contactSection {
|
||||||
|
padding: 0 18px 18px;
|
||||||
|
gap: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contactItem {
|
||||||
|
padding: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Medium Screens (768px - 1023px) */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
.contactSection {
|
||||||
|
padding: 0 22px 22px;
|
||||||
|
gap: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contactItem {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Large Screens (1024px+) */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.contactSection {
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contactItem {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { Mail, Phone, Globe } from 'lucide-react';
|
||||||
|
import type { BusinessCard } from '../../store/types';
|
||||||
|
import styles from './CardContactInfo.module.css';
|
||||||
|
|
||||||
|
interface CardContactInfoProps {
|
||||||
|
businessCard: BusinessCard;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardContactInfo({ businessCard }: CardContactInfoProps) {
|
||||||
|
return (
|
||||||
|
<div className={styles.contactSection}>
|
||||||
|
<div className={styles.contactItem}>
|
||||||
|
<Mail size={18} className={styles.icon} />
|
||||||
|
<a href={`mailto:${businessCard.email}`} className={styles.link}>
|
||||||
|
{businessCard.email}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className={styles.contactItem}>
|
||||||
|
<Phone size={18} className={styles.icon} />
|
||||||
|
<a href={`tel:${businessCard.phone}`} className={styles.link}>
|
||||||
|
{businessCard.phone}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{businessCard.website && (
|
||||||
|
<div className={styles.contactItem}>
|
||||||
|
<Globe size={18} className={styles.icon} />
|
||||||
|
<a
|
||||||
|
href={businessCard.website}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={styles.link}
|
||||||
|
>
|
||||||
|
{businessCard.website.replace('https://', '').replace('http://', '')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
.footer {
|
||||||
|
padding: 15px 20px;
|
||||||
|
text-align: center;
|
||||||
|
background: #f7fafc;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #a0aec0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra Small Screens (320px - 479px) */
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.footer {
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small Screens (480px - 767px) */
|
||||||
|
@media (min-width: 480px) and (max-width: 767px) {
|
||||||
|
.footer {
|
||||||
|
padding: 13px 18px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Medium Screens (768px - 1023px) */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
.footer {
|
||||||
|
padding: 14px 22px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Large Screens (1024px+) */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.footer {
|
||||||
|
padding: 15px 20px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import styles from './CardFooter.module.css';
|
||||||
|
|
||||||
|
interface CardFooterProps {
|
||||||
|
companyName: string;
|
||||||
|
personName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardFooter({ personName }: Readonly<CardFooterProps>) {
|
||||||
|
return (
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<p>© 2026 {personName}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
.header {
|
||||||
|
height: 120px;
|
||||||
|
background: linear-gradient(135deg, #e0e7ef 0%, #91c5e0 100%);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra Small Screens (320px - 479px) */
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.header {
|
||||||
|
height: 90px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small Screens (480px - 767px) */
|
||||||
|
@media (min-width: 480px) and (max-width: 767px) {
|
||||||
|
.header {
|
||||||
|
height: 105px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Medium Screens (768px - 1023px) */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
.header {
|
||||||
|
height: 115px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Large Screens (1024px+) */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.header {
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import styles from './CardHeader.module.css';
|
||||||
|
|
||||||
|
export function CardHeader() {
|
||||||
|
return <div className={styles.header} />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
.profileSection {
|
||||||
|
padding: 40px 20px 20px;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileImage {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 5px solid white;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-top: -70px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileImage:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a202c;
|
||||||
|
margin: 15px 0 5px;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #667eea;
|
||||||
|
margin: 5px 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #718096;
|
||||||
|
margin: 5px 0 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra Small Screens (320px - 479px) */
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.profileSection {
|
||||||
|
padding: 35px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileImage {
|
||||||
|
width: 75px;
|
||||||
|
height: 75px;
|
||||||
|
border-width: 4px;
|
||||||
|
margin-top: -50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 12px 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 3px 0;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company {
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 3px 0 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small Screens (480px - 767px) */
|
||||||
|
@media (min-width: 480px) and (max-width: 767px) {
|
||||||
|
.profileSection {
|
||||||
|
padding: 37px 18px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileImage {
|
||||||
|
width: 85px;
|
||||||
|
height: 85px;
|
||||||
|
border-width: 4px;
|
||||||
|
margin-top: -58px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 13px 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 3px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company {
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 3px 0 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Medium Screens (768px - 1023px) */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
.profileSection {
|
||||||
|
padding: 39px 22px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileImage {
|
||||||
|
width: 95px;
|
||||||
|
height: 95px;
|
||||||
|
border-width: 5px;
|
||||||
|
margin-top: -65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 22px;
|
||||||
|
margin: 14px 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company {
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 4px 0 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Large Screens (1024px+) */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.profileSection {
|
||||||
|
padding: 40px 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileImage {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-width: 5px;
|
||||||
|
margin-top: -70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 15px 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company {
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 5px 0 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type { BusinessCard } from '../../store/types';
|
||||||
|
import styles from './CardProfile.module.css';
|
||||||
|
|
||||||
|
interface CardProfileProps {
|
||||||
|
businessCard: BusinessCard;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardProfile({ businessCard }: Readonly<CardProfileProps>) {
|
||||||
|
return (
|
||||||
|
<div className={styles.profileSection}>
|
||||||
|
<img
|
||||||
|
src={businessCard.image}
|
||||||
|
alt={businessCard.name}
|
||||||
|
className={styles.profileImage}
|
||||||
|
/>
|
||||||
|
<h1 className={styles.name}>{businessCard.name}</h1>
|
||||||
|
<p className={styles.title}>{businessCard.title}</p>
|
||||||
|
<p className={styles.company}>{businessCard.company}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 40px 30px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
max-width: 350px;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a202c;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrWrapper {
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition:
|
||||||
|
border-color 0.3s ease,
|
||||||
|
box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrWrapper:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 20px rgba(102, 126, 234, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrWrapper canvas {
|
||||||
|
width: 256px !important;
|
||||||
|
height: 256px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #718096;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadBtn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 20px;
|
||||||
|
background: linear-gradient(135deg, #3f51b5 0%, #5b2c83 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadBtn:hover {
|
||||||
|
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadBtn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrWrapper canvas {
|
||||||
|
width: 200px !important;
|
||||||
|
height: 200px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { useRef } from 'react';
|
||||||
|
// @ts-expect-error - qrcode.react doesn't have official TS types
|
||||||
|
import QRCode from 'qrcode.react';
|
||||||
|
import { Download } from 'lucide-react';
|
||||||
|
import { useBusinessCard } from '../store/BusinessCardContext';
|
||||||
|
import styles from './QRCodeDisplay.module.css';
|
||||||
|
import type { BusinessCard } from '../store/types';
|
||||||
|
|
||||||
|
function generateVCard(businessCard: BusinessCard): string {
|
||||||
|
const lines = [
|
||||||
|
'BEGIN:VCARD',
|
||||||
|
'VERSION:3.0',
|
||||||
|
`FN:${businessCard.name}`,
|
||||||
|
`N:${businessCard.name.split(' ').reverse().join(';')}`,
|
||||||
|
`TITLE:${businessCard.title}`,
|
||||||
|
`ORG:${businessCard.company}`,
|
||||||
|
`EMAIL:${businessCard.email}`,
|
||||||
|
`TEL:${businessCard.phone}`,
|
||||||
|
businessCard.website ? `URL:${businessCard.website}` : null,
|
||||||
|
businessCard.image ? `PHOTO;VALUE=URL:${businessCard.image}` : null,
|
||||||
|
'END:VCARD',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\r\n');
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QRCodeDisplay() {
|
||||||
|
const { businessCard } = useBusinessCard();
|
||||||
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const vCard = generateVCard(businessCard);
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
const canvas = qrRef.current?.querySelector('canvas');
|
||||||
|
if (canvas) {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = canvas.toDataURL('image/png');
|
||||||
|
link.download = `${businessCard.name}-contact.png`;
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<h2 className={styles.title}>Scan to Add Contact</h2>
|
||||||
|
<div className={styles.qrWrapper} ref={qrRef}>
|
||||||
|
<QRCode
|
||||||
|
value={vCard}
|
||||||
|
size={256}
|
||||||
|
level="H"
|
||||||
|
includeMargin
|
||||||
|
fgColor="#000000"
|
||||||
|
bgColor="#ffffff"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className={styles.subtitle}>Scan with any smartphone camera</p>
|
||||||
|
<button onClick={handleDownload} className={styles.downloadBtn} aria-label="Download QR code">
|
||||||
|
<Download size={20} />
|
||||||
|
<span>Download QR Code</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
.saveButton {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
background: linear-gradient(135deg, #e0e7ef 0%, #91c5e0 100%);
|
||||||
|
color: black;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveButton:hover {
|
||||||
|
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveButton:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra Small Screens (320px - 479px) */
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.saveButton {
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small Screens (480px - 767px) */
|
||||||
|
@media (min-width: 480px) and (max-width: 767px) {
|
||||||
|
.saveButton {
|
||||||
|
padding: 11px 18px;
|
||||||
|
font-size: 13px;
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Medium Screens (768px - 1023px) */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
.saveButton {
|
||||||
|
padding: 12px 22px;
|
||||||
|
font-size: 14px;
|
||||||
|
min-height: 43px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Large Screens (1024px+) */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.saveButton {
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Download } from 'lucide-react';
|
||||||
|
import type { BusinessCard } from '../store/types';
|
||||||
|
import styles from './SaveContactButton.module.css';
|
||||||
|
|
||||||
|
interface SaveContactButtonProps {
|
||||||
|
businessCard: BusinessCard;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateVCard(businessCard: BusinessCard): string {
|
||||||
|
const lines = [
|
||||||
|
'BEGIN:VCARD',
|
||||||
|
'VERSION:3.0',
|
||||||
|
`FN:${businessCard.name}`,
|
||||||
|
`N:${businessCard.name.split(' ').reverse().join(';')}`,
|
||||||
|
`TITLE:${businessCard.title}`,
|
||||||
|
`ORG:${businessCard.company}`,
|
||||||
|
`EMAIL:${businessCard.email}`,
|
||||||
|
`TEL:${businessCard.phone}`,
|
||||||
|
businessCard.website ? `URL:${businessCard.website}` : null,
|
||||||
|
businessCard.image ? `PHOTO;VALUE=URL:${businessCard.image}` : null,
|
||||||
|
'END:VCARD',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\r\n');
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveContactButton({ businessCard }: Readonly<SaveContactButtonProps>) {
|
||||||
|
const handleSaveContact = () => {
|
||||||
|
const vCard = generateVCard(businessCard);
|
||||||
|
const blob = new Blob([vCard], { type: 'text/vcard;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `${businessCard.name}.vcf`;
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={handleSaveContact} className={styles.saveButton} aria-label="Save contact">
|
||||||
|
<Download size={20} />
|
||||||
|
<span>Save Contact</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f7fafc;
|
||||||
|
color: #3b4cca;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:hover {
|
||||||
|
background: #3b4cca;
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:active {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra Small Screens (320px - 479px) */
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.container {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
min-width: 36px;
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small Screens (480px - 767px) */
|
||||||
|
@media (min-width: 480px) and (max-width: 767px) {
|
||||||
|
.container {
|
||||||
|
gap: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
min-width: 38px;
|
||||||
|
min-height: 38px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Medium Screens (768px - 1023px) */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
.container {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
width: 39px;
|
||||||
|
height: 39px;
|
||||||
|
min-width: 39px;
|
||||||
|
min-height: 39px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Large Screens (1024px+) */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Github, Linkedin, Twitter, Globe, Mail, Phone } from 'lucide-react';
|
||||||
|
import type { SocialLink } from '../store/types';
|
||||||
|
import styles from './SocialLinks.module.css';
|
||||||
|
|
||||||
|
interface SocialLinksProps {
|
||||||
|
links: SocialLink[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconMap: Record<string, React.ReactNode> = {
|
||||||
|
Github: <Github size={20} />,
|
||||||
|
Linkedin: <Linkedin size={20} />,
|
||||||
|
Twitter: <Twitter size={20} />,
|
||||||
|
Globe: <Globe size={20} />,
|
||||||
|
Mail: <Mail size={20} />,
|
||||||
|
Phone: <Phone size={20} />,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SocialLinks({ links }: Readonly<SocialLinksProps>) {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
{links.map((link) => (
|
||||||
|
<a
|
||||||
|
key={link.platform}
|
||||||
|
href={link.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={styles.link}
|
||||||
|
title={link.platform}
|
||||||
|
aria-label={`Visit ${link.platform}`}
|
||||||
|
>
|
||||||
|
{iconMap[link.icon] || <Globe size={20} />}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import type { BusinessCard } from '../store/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LOCAL CONFIGURATION FILE
|
||||||
|
*
|
||||||
|
* To use this file:
|
||||||
|
* 1. Copy this file and rename it to: businessCard.config.local.ts
|
||||||
|
* 2. Update the values below with your information
|
||||||
|
* 3. The local config will be loaded automatically (this file is git-ignored)
|
||||||
|
* 4. Your changes will NOT be pushed to git
|
||||||
|
*
|
||||||
|
* This file is in .gitignore, so it's safe to add personal/sensitive information here.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const localBusinessCard: BusinessCard = {
|
||||||
|
name: 'Your Name',
|
||||||
|
title: 'Your Job Title',
|
||||||
|
company: 'Your Company Name',
|
||||||
|
email: 'your.email@example.com',
|
||||||
|
phone: '+1 (555) 000-0000',
|
||||||
|
website: 'https://yourwebsite.com',
|
||||||
|
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop',
|
||||||
|
bio: 'Your professional bio goes here.',
|
||||||
|
socialLinks: [
|
||||||
|
{ platform: 'LinkedIn', url: 'https://linkedin.com/in/yourprofile', icon: 'Linkedin' },
|
||||||
|
{ platform: 'Twitter', url: 'https://twitter.com/yourhandle', icon: 'Twitter' },
|
||||||
|
{ platform: 'GitHub', url: 'https://github.com/yourprofile', icon: 'Github' },
|
||||||
|
{ platform: 'Portfolio', url: 'https://yourwebsite.com', icon: 'Globe' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import type { BusinessCard } from '../store/types';
|
||||||
|
import businessCardImage from '../assets/sascha.png';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LOCAL STUB FILE
|
||||||
|
*
|
||||||
|
* This file serves as a stub for local configuration.
|
||||||
|
* To use your own configuration:
|
||||||
|
*
|
||||||
|
* 1. Copy businessCard.config.local.example.ts
|
||||||
|
* 2. Rename the copy to businessCard.config.local.ts (replace this file)
|
||||||
|
* 3. Edit it with your personal information
|
||||||
|
* 4. This file is git-ignored, so your data stays local!
|
||||||
|
*
|
||||||
|
* For now, it just exports the default config.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const localBusinessCard: BusinessCard = {
|
||||||
|
name: 'Sascha Bach',
|
||||||
|
title: 'Web Engineer',
|
||||||
|
company: 'Freelancer',
|
||||||
|
email: 'freelancer@sascha-bach.de',
|
||||||
|
phone: '+49 179 9364867',
|
||||||
|
website: 'https://sascha-bach.de',
|
||||||
|
image: businessCardImage,
|
||||||
|
bio: 'Passionate about Webapps and Accessibility',
|
||||||
|
socialLinks: [
|
||||||
|
{ platform: 'LinkedIn', url: 'https://linkedin.com/in/saschabach', icon: 'Linkedin' },
|
||||||
|
{ platform: 'GitHub', url: 'https://codeberg.org/saschab', icon: 'Github' },
|
||||||
|
{ platform: 'Portfolio', url: 'https://sascha-bach.de', icon: 'Globe' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import type { BusinessCard } from '../store/types';
|
||||||
|
|
||||||
|
// Default business card configuration
|
||||||
|
// To customize locally without pushing to git, copy businessCard.config.local.example.ts
|
||||||
|
// to businessCard.config.local.ts and modify it
|
||||||
|
|
||||||
|
export const defaultBusinessCard: BusinessCard = {
|
||||||
|
name: 'John sas',
|
||||||
|
title: 'Senior Product Designer',
|
||||||
|
company: 'Tech Innovations Inc.',
|
||||||
|
email: 'john.d@techinnovations.com',
|
||||||
|
phone: '+1 (555) 123-4567',
|
||||||
|
website: 'https://johndoe.design',
|
||||||
|
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop',
|
||||||
|
bio: 'Passionate about creating beautiful and functional digital experiences.',
|
||||||
|
socialLinks: [
|
||||||
|
{ platform: 'LinkedIn', url: 'https://linkedin.com/in/johndoe', icon: 'Linkedin' },
|
||||||
|
{ platform: 'Twitter', url: 'https://twitter.com/johndoe', icon: 'Twitter' },
|
||||||
|
{ platform: 'GitHub', url: 'https://github.com/johndoe', icon: 'Github' },
|
||||||
|
{ platform: 'Portfolio', url: 'https://johndoe.design', icon: 'Globe' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { defaultBusinessCard } from './businessCard.config';
|
||||||
|
import { localBusinessCard } from './businessCard.config.local';
|
||||||
|
import type { BusinessCard } from '../store/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the business card configuration.
|
||||||
|
*
|
||||||
|
* How it works:
|
||||||
|
* - businessCard.config.ts: Contains demo/default data (git-tracked)
|
||||||
|
* - businessCard.config.local.ts: Your personal config (git-ignored)
|
||||||
|
*
|
||||||
|
* To customize with your own data:
|
||||||
|
* 1. Edit src/config/businessCard.config.local.ts
|
||||||
|
* 2. Replace the stub content with your information
|
||||||
|
* 3. The app will automatically load your config
|
||||||
|
* 4. Your changes are safe - this file is git-ignored!
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function getBusinessCardConfig(): BusinessCard {
|
||||||
|
// If local config has been customized (not just the default stub), use it
|
||||||
|
// Otherwise, use the default config
|
||||||
|
// We can detect if it's been customized by checking if any values differ
|
||||||
|
if (isLocalConfigCustomized()) {
|
||||||
|
return localBusinessCard;
|
||||||
|
}
|
||||||
|
return defaultBusinessCard;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the local config has been customized
|
||||||
|
* A config is considered customized if it differs from the default
|
||||||
|
*/
|
||||||
|
function isLocalConfigCustomized(): boolean {
|
||||||
|
return JSON.stringify(localBusinessCard) !== JSON.stringify(defaultBusinessCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronous version for initialization
|
||||||
|
*/
|
||||||
|
export function getDefaultBusinessCard(): BusinessCard {
|
||||||
|
return { ...defaultBusinessCard };
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
:root {
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
||||||
|
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color: #1a202c;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 2px solid #667eea;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { StrictMode } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App.tsx';
|
||||||
|
import { BusinessCardProvider } from './store/BusinessCardProvider.tsx';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<BusinessCardProvider>
|
||||||
|
<App />
|
||||||
|
</BusinessCardProvider>
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
import type { BusinessCardContextType } from './types';
|
||||||
|
|
||||||
|
export const BusinessCardContext = createContext<BusinessCardContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function useBusinessCard() {
|
||||||
|
const context = useContext(BusinessCardContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useBusinessCard must be used within BusinessCardProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import type { BusinessCard } from './types';
|
||||||
|
import { BusinessCardContext } from './BusinessCardContext';
|
||||||
|
import { getBusinessCardConfig } from '../config/configLoader';
|
||||||
|
|
||||||
|
interface BusinessCardProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BusinessCardProvider({ children }: Readonly<BusinessCardProviderProps>) {
|
||||||
|
// Load config on initialization
|
||||||
|
const [businessCard] = useState<BusinessCard>(getBusinessCardConfig());
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const updateBusinessCard = (_card: Partial<BusinessCard>) => {
|
||||||
|
// Note: Runtime updates disabled to prevent accidental data loss
|
||||||
|
// To customize your business card, edit src/config/businessCard.config.local.ts
|
||||||
|
console.warn(
|
||||||
|
'To update your business card, edit src/config/businessCard.config.local.ts'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BusinessCardContext.Provider value={{ businessCard, updateBusinessCard }}>
|
||||||
|
{children}
|
||||||
|
</BusinessCardContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
export interface SocialLink {
|
||||||
|
platform: string;
|
||||||
|
url: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BusinessCard {
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
company: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
website: string;
|
||||||
|
image: string;
|
||||||
|
bio: string;
|
||||||
|
socialLinks: SocialLink[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BusinessCardContextType {
|
||||||
|
businessCard: BusinessCard;
|
||||||
|
updateBusinessCard: (card: Partial<BusinessCard>) => void;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react({
|
||||||
|
babel: {
|
||||||
|
plugins: [['babel-plugin-react-compiler']],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue