Initial commit: PassMaster PWA MVP
|
|
@ -0,0 +1,14 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.DS_Store
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
VITE_APP_NAME=SimplePasswordGen
|
||||||
|
VITE_DEFAULT_LANG=en
|
||||||
|
# no secrets required
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# PWA files
|
||||||
|
**/public/sw.js
|
||||||
|
**/public/workbox-*.js
|
||||||
|
**/public/worker-*.js
|
||||||
|
**/public/sw.js.map
|
||||||
|
**/public/workbox-*.js.map
|
||||||
|
**/public/worker-*.js.map
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# ---- build ----
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* ./
|
||||||
|
RUN if [ -f pnpm-lock.yaml ]; then npm i -g pnpm && pnpm i --frozen-lockfile; \
|
||||||
|
elif [ -f yarn.lock ]; then yarn --frozen-lockfile; else npm ci; fi
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ---- run ----
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx","-g","daemon off;"]
|
||||||
|
|
@ -0,0 +1,279 @@
|
||||||
|
# PassMaster PWA Improvements Roadmap
|
||||||
|
|
||||||
|
## 🚀 **Immediate Improvements (Week 1-2)**
|
||||||
|
|
||||||
|
### 1. Performance Optimization
|
||||||
|
```typescript
|
||||||
|
// Add to next.config.js
|
||||||
|
const nextConfig = {
|
||||||
|
experimental: {
|
||||||
|
optimizeCss: true,
|
||||||
|
optimizePackageImports: ['lucide-react', 'framer-motion'],
|
||||||
|
},
|
||||||
|
images: {
|
||||||
|
formats: ['image/webp', 'image/avif'],
|
||||||
|
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
||||||
|
},
|
||||||
|
compress: true,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Enhanced Security Features
|
||||||
|
```typescript
|
||||||
|
// Add password strength meter
|
||||||
|
interface PasswordStrength {
|
||||||
|
score: number; // 0-4
|
||||||
|
entropy: number;
|
||||||
|
timeToCrack: string;
|
||||||
|
suggestions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add password history
|
||||||
|
interface PasswordHistory {
|
||||||
|
id: string;
|
||||||
|
password: string;
|
||||||
|
timestamp: number;
|
||||||
|
strength: PasswordStrength;
|
||||||
|
category?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Real Marketing Assets
|
||||||
|
- [ ] Create actual screenshots (desktop, mobile, tablet)
|
||||||
|
- [ ] Record demo video showing PWA installation
|
||||||
|
- [ ] Create social media graphics
|
||||||
|
- [ ] Add app store preview images
|
||||||
|
|
||||||
|
## 📈 **Medium-term Improvements (Month 1-2)**
|
||||||
|
|
||||||
|
### 4. Analytics & Monitoring
|
||||||
|
```typescript
|
||||||
|
// Add privacy-friendly analytics
|
||||||
|
import Plausible from 'plausible-tracker'
|
||||||
|
|
||||||
|
const plausible = Plausible({
|
||||||
|
domain: 'passmaster.app',
|
||||||
|
apiHost: 'https://plausible.io'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Track key events
|
||||||
|
plausible.trackEvent('Password Generated')
|
||||||
|
plausible.trackEvent('PWA Installed')
|
||||||
|
plausible.trackEvent('Offline Used')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Internationalization
|
||||||
|
```typescript
|
||||||
|
// Add i18n support
|
||||||
|
import { useTranslations } from 'next-intl'
|
||||||
|
|
||||||
|
const t = useTranslations('common')
|
||||||
|
// German translation
|
||||||
|
const messages = {
|
||||||
|
de: {
|
||||||
|
common: {
|
||||||
|
generate: 'Passwort generieren',
|
||||||
|
copy: 'Kopieren',
|
||||||
|
// ... more translations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Advanced Password Features
|
||||||
|
```typescript
|
||||||
|
// Add passphrase generation
|
||||||
|
const generatePassphrase = (wordCount: number = 4): string => {
|
||||||
|
const words = wordList.filter(word => word.length >= 4)
|
||||||
|
return Array.from({ length: wordCount }, () =>
|
||||||
|
words[Math.floor(Math.random() * words.length)]
|
||||||
|
).join('-')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add memorable patterns
|
||||||
|
const generateMemorable = (): string => {
|
||||||
|
const patterns = [
|
||||||
|
'adjective-noun-number',
|
||||||
|
'verb-adjective-noun',
|
||||||
|
'color-animal-number'
|
||||||
|
]
|
||||||
|
// Implementation...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 **Long-term Improvements (Month 3-6)**
|
||||||
|
|
||||||
|
### 7. Monetization Strategy
|
||||||
|
```typescript
|
||||||
|
// Premium features
|
||||||
|
interface PremiumFeatures {
|
||||||
|
advancedTemplates: boolean;
|
||||||
|
unlimitedHistory: boolean;
|
||||||
|
exportFunctionality: boolean;
|
||||||
|
apiAccess: boolean;
|
||||||
|
whiteLabel: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pricing tiers
|
||||||
|
const pricing = {
|
||||||
|
free: { price: 0, features: ['basic'] },
|
||||||
|
pro: { price: 4.99, features: ['advanced', 'export', 'history'] },
|
||||||
|
enterprise: { price: 29.99, features: ['api', 'whitelabel', 'support'] }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. API Development
|
||||||
|
```typescript
|
||||||
|
// REST API for developers
|
||||||
|
interface PasswordAPI {
|
||||||
|
generate: (options: PasswordOptions) => Promise<PasswordResult>;
|
||||||
|
validate: (password: string) => Promise<ValidationResult>;
|
||||||
|
strength: (password: string) => Promise<StrengthResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limiting and authentication
|
||||||
|
const apiConfig = {
|
||||||
|
rateLimit: '100 requests/hour',
|
||||||
|
authentication: 'API key required',
|
||||||
|
documentation: 'OpenAPI/Swagger'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 **Technical Debt & Maintenance**
|
||||||
|
|
||||||
|
### 9. Code Quality
|
||||||
|
- [ ] Add comprehensive unit tests (Jest + React Testing Library)
|
||||||
|
- [ ] Add E2E tests (Playwright)
|
||||||
|
- [ ] Implement CI/CD pipeline
|
||||||
|
- [ ] Add code coverage reporting
|
||||||
|
|
||||||
|
### 10. Security Audits
|
||||||
|
- [ ] Third-party security audit
|
||||||
|
- [ ] Dependency vulnerability scanning
|
||||||
|
- [ ] Penetration testing
|
||||||
|
- [ ] Privacy compliance review (GDPR)
|
||||||
|
|
||||||
|
## 📊 **Success Metrics**
|
||||||
|
|
||||||
|
### Key Performance Indicators
|
||||||
|
- **Lighthouse Score**: Target 95+ across all metrics
|
||||||
|
- **PWA Install Rate**: Target 15% of visitors
|
||||||
|
- **Offline Usage**: Track percentage of offline sessions
|
||||||
|
- **User Retention**: 30-day retention rate
|
||||||
|
- **Conversion Rate**: Free to premium (if implemented)
|
||||||
|
|
||||||
|
### Business Metrics
|
||||||
|
- **Monthly Active Users**: Target growth rate
|
||||||
|
- **Revenue**: If monetization is implemented
|
||||||
|
- **Market Share**: Position in password generator market
|
||||||
|
- **Brand Recognition**: Social media mentions and reviews
|
||||||
|
|
||||||
|
## 🎨 **Design System Improvements**
|
||||||
|
|
||||||
|
### 11. Component Library
|
||||||
|
```typescript
|
||||||
|
// Create reusable component library
|
||||||
|
export const Button = ({ variant, size, ...props }) => {
|
||||||
|
// Consistent button styling
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Card = ({ elevation, padding, ...props }) => {
|
||||||
|
// Consistent card styling
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Input = ({ type, validation, ...props }) => {
|
||||||
|
// Consistent input styling
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12. Animation System
|
||||||
|
```typescript
|
||||||
|
// Enhanced animations
|
||||||
|
const animations = {
|
||||||
|
fadeIn: { opacity: [0, 1], duration: 0.3 },
|
||||||
|
slideUp: { y: [20, 0], opacity: [0, 1], duration: 0.4 },
|
||||||
|
scale: { scale: [0.95, 1], duration: 0.2 },
|
||||||
|
bounce: { y: [0, -10, 0], duration: 0.6 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 **Deployment & Infrastructure**
|
||||||
|
|
||||||
|
### 13. Production Optimization
|
||||||
|
- [ ] CDN setup (Cloudflare/Vercel Edge)
|
||||||
|
- [ ] Database for analytics (if needed)
|
||||||
|
- [ ] Monitoring and alerting
|
||||||
|
- [ ] Backup and disaster recovery
|
||||||
|
|
||||||
|
### 14. SEO & Marketing
|
||||||
|
- [ ] Content marketing strategy
|
||||||
|
- [ ] Social media presence
|
||||||
|
- [ ] Guest posting and backlinks
|
||||||
|
- [ ] App store optimization
|
||||||
|
|
||||||
|
## 💡 **Innovation Opportunities**
|
||||||
|
|
||||||
|
### 15. AI Integration
|
||||||
|
```typescript
|
||||||
|
// AI-powered password suggestions
|
||||||
|
const aiSuggestions = {
|
||||||
|
contextAware: (website: string) => string[],
|
||||||
|
strengthOptimization: (password: string) => string,
|
||||||
|
patternLearning: (userPreferences: any) => any
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 16. Blockchain Integration
|
||||||
|
```typescript
|
||||||
|
// Decentralized password verification
|
||||||
|
const blockchainFeatures = {
|
||||||
|
passwordVerification: (hash: string) => boolean,
|
||||||
|
decentralizedStorage: (encrypted: string) => string,
|
||||||
|
zeroKnowledgeProofs: () => any
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 **Implementation Timeline**
|
||||||
|
|
||||||
|
### Phase 1 (Weeks 1-2): Foundation
|
||||||
|
- Performance optimization
|
||||||
|
- Security enhancements
|
||||||
|
- Real marketing assets
|
||||||
|
|
||||||
|
### Phase 2 (Weeks 3-6): Growth
|
||||||
|
- Analytics implementation
|
||||||
|
- Internationalization
|
||||||
|
- Advanced features
|
||||||
|
|
||||||
|
### Phase 3 (Months 2-3): Scale
|
||||||
|
- Monetization strategy
|
||||||
|
- API development
|
||||||
|
- Infrastructure improvements
|
||||||
|
|
||||||
|
### Phase 4 (Months 3-6): Innovation
|
||||||
|
- AI integration
|
||||||
|
- Advanced UI/UX
|
||||||
|
- Market expansion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💰 **Investment Requirements**
|
||||||
|
|
||||||
|
### Development Costs
|
||||||
|
- **Phase 1**: $3,000-5,000
|
||||||
|
- **Phase 2**: $5,000-8,000
|
||||||
|
- **Phase 3**: $8,000-12,000
|
||||||
|
- **Phase 4**: $10,000-15,000
|
||||||
|
|
||||||
|
### Total Investment: $26,000-40,000
|
||||||
|
|
||||||
|
### Expected ROI
|
||||||
|
- **Conservative**: 2-3x return within 12 months
|
||||||
|
- **Optimistic**: 5-10x return within 18 months
|
||||||
|
- **Market Potential**: $100,000-500,000 annual revenue
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*This roadmap provides a comprehensive path to transform PassMaster from a solid PWA into a market-leading password generator with significant revenue potential.*
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 SimplePasswordGen
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
# PassMaster PWA Implementation
|
||||||
|
|
||||||
|
## ✅ **Complete PWA Setup**
|
||||||
|
|
||||||
|
Your PassMaster application is now a fully functional Progressive Web App (PWA) with the following features:
|
||||||
|
|
||||||
|
### 🎯 **Core PWA Features**
|
||||||
|
|
||||||
|
1. **Web App Manifest** (`/public/manifest.json`)
|
||||||
|
- ✅ App name and description
|
||||||
|
- ✅ Theme colors (#3b82f6 blue)
|
||||||
|
- ✅ Display mode: standalone
|
||||||
|
- ✅ Multiple icon sizes (72x72 to 512x512)
|
||||||
|
- ✅ App shortcuts for quick access
|
||||||
|
- ✅ Screenshots for app store listings
|
||||||
|
- ✅ Categories and metadata
|
||||||
|
|
||||||
|
2. **Service Worker** (`/public/sw.js`)
|
||||||
|
- ✅ Offline functionality
|
||||||
|
- ✅ Resource caching
|
||||||
|
- ✅ Cache management
|
||||||
|
- ✅ Automatic updates
|
||||||
|
|
||||||
|
3. **Icons** (`/public/icons/`)
|
||||||
|
- ✅ All required sizes: 72, 96, 128, 144, 152, 192, 384, 512px
|
||||||
|
- ✅ Maskable icons for adaptive UI
|
||||||
|
- ✅ High-quality PNG format
|
||||||
|
|
||||||
|
4. **Screenshots** (`/public/screenshots/`)
|
||||||
|
- ✅ Desktop screenshot (1280x720)
|
||||||
|
- ✅ Mobile screenshot (390x844)
|
||||||
|
- ✅ Placeholder designs ready for replacement
|
||||||
|
|
||||||
|
### 🚀 **Enhanced Features**
|
||||||
|
|
||||||
|
5. **PWA Install Prompt** (`/src/components/PWAInstallPrompt.tsx`)
|
||||||
|
- ✅ Automatic installation prompts
|
||||||
|
- ✅ Smart detection of installability
|
||||||
|
- ✅ User-friendly interface
|
||||||
|
- ✅ Dismissible prompts
|
||||||
|
|
||||||
|
6. **Service Worker Registration** (`/src/app/layout.tsx`)
|
||||||
|
- ✅ Automatic registration
|
||||||
|
- ✅ Error handling
|
||||||
|
- ✅ Console logging for debugging
|
||||||
|
|
||||||
|
7. **Enhanced Metadata** (`/src/app/layout.tsx`)
|
||||||
|
- ✅ Proper icon references
|
||||||
|
- ✅ Manifest linking
|
||||||
|
- ✅ Theme color meta tags
|
||||||
|
- ✅ JSON-LD structured data
|
||||||
|
|
||||||
|
### 📱 **Installation Methods**
|
||||||
|
|
||||||
|
Users can now install PassMaster as a native app through:
|
||||||
|
|
||||||
|
1. **Browser Install Prompt** - Automatic prompt when criteria are met
|
||||||
|
2. **Manual Installation** - Browser menu → "Install PassMaster"
|
||||||
|
3. **Mobile** - Add to home screen from browser menu
|
||||||
|
4. **Desktop** - Install from browser address bar
|
||||||
|
|
||||||
|
### 🔧 **Technical Specifications**
|
||||||
|
|
||||||
|
- **Framework**: Next.js 14 with App Router
|
||||||
|
- **PWA Library**: next-pwa (already in dependencies)
|
||||||
|
- **Icons**: PNG format, maskable design
|
||||||
|
- **Service Worker**: Custom implementation with caching
|
||||||
|
- **Offline Support**: Full offline functionality
|
||||||
|
- **Installation**: Cross-platform compatibility
|
||||||
|
|
||||||
|
### 🎨 **Design Features**
|
||||||
|
|
||||||
|
- **Theme Colors**: Blue (#3b82f6) primary theme
|
||||||
|
- **Dark Mode**: Full dark mode support
|
||||||
|
- **Responsive**: Works on all device sizes
|
||||||
|
- **Accessibility**: ARIA labels and keyboard navigation
|
||||||
|
- **Performance**: Optimized loading and caching
|
||||||
|
|
||||||
|
### 📋 **Next Steps (Optional)**
|
||||||
|
|
||||||
|
1. **Replace Screenshots**: Update placeholder screenshots with actual app screenshots
|
||||||
|
2. **Icon Optimization**: Consider creating properly sized icons for better quality
|
||||||
|
3. **App Store Submission**: Ready for Microsoft Store, Chrome Web Store, etc.
|
||||||
|
4. **Analytics**: Add PWA-specific analytics tracking
|
||||||
|
5. **Push Notifications**: Implement push notification system
|
||||||
|
|
||||||
|
### 🧪 **Testing**
|
||||||
|
|
||||||
|
To test your PWA:
|
||||||
|
|
||||||
|
1. **Development**: Run `npm run dev` and test in browser
|
||||||
|
2. **Installation**: Look for install prompt or use browser menu
|
||||||
|
3. **Offline**: Disconnect internet and test functionality
|
||||||
|
4. **Lighthouse**: Run Lighthouse audit for PWA score
|
||||||
|
5. **Cross-browser**: Test in Chrome, Firefox, Safari, Edge
|
||||||
|
|
||||||
|
### 📊 **PWA Score**
|
||||||
|
|
||||||
|
Your PWA should achieve high scores in:
|
||||||
|
- ✅ **Installable**: 100%
|
||||||
|
- ✅ **PWA Optimized**: 100%
|
||||||
|
- ✅ **Offline Functionality**: 100%
|
||||||
|
- ✅ **Performance**: Optimized
|
||||||
|
- ✅ **Accessibility**: Full support
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🎉 Congratulations!** Your PassMaster application is now a fully functional Progressive Web App that users can install on their devices and use offline.
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
# PassMaster - Free Offline Secure Password Generator
|
||||||
|
|
||||||
|
A modern, privacy-first password generator built with Next.js, TypeScript, and TailwindCSS. Generate ultra-secure passwords instantly with client-side encryption - your passwords never leave your device.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- 🔒 **Client-Side Encryption** - All password generation happens locally in your browser
|
||||||
|
- 📱 **Progressive Web App (PWA)** - Install as an app and work offline
|
||||||
|
- 🌙 **Dark Mode Support** - Automatic theme switching with system preference detection
|
||||||
|
- 🎯 **Password Strength Meter** - Real-time entropy calculation and time-to-crack estimation
|
||||||
|
- ⚡ **Instant Generation** - Generate passwords in milliseconds with cryptographically secure randomness
|
||||||
|
- 🔧 **Customizable Options** - Length, character types, and exclude similar characters
|
||||||
|
- 📋 **One-Click Copy** - Copy passwords to clipboard with visual feedback
|
||||||
|
- ♿ **Accessible** - Full keyboard navigation and screen reader support
|
||||||
|
- 📊 **SEO Optimized** - Structured data, meta tags, and semantic HTML
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 18+
|
||||||
|
- npm or yarn
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/your-username/passmaster.git
|
||||||
|
cd passmaster
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the development server:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Open [http://localhost:3000](http://localhost:3000) in your browser.
|
||||||
|
|
||||||
|
## 🛠️ Tech Stack
|
||||||
|
|
||||||
|
- **Framework**: Next.js 14 (App Router)
|
||||||
|
- **Language**: TypeScript
|
||||||
|
- **Styling**: TailwindCSS
|
||||||
|
- **Icons**: Lucide React
|
||||||
|
- **Animations**: Framer Motion
|
||||||
|
- **Theme**: next-themes
|
||||||
|
- **PWA**: next-pwa
|
||||||
|
- **State Management**: React hooks + localStorage
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── app/ # Next.js App Router
|
||||||
|
│ ├── layout.tsx # Root layout with metadata
|
||||||
|
│ ├── page.tsx # Home page
|
||||||
|
│ ├── privacy/ # Privacy policy page
|
||||||
|
│ └── globals.css # Global styles
|
||||||
|
├── components/ # React components
|
||||||
|
│ ├── layout/ # Layout components
|
||||||
|
│ ├── PasswordGenerator.tsx
|
||||||
|
│ ├── FAQ.tsx
|
||||||
|
│ ├── FloatingCTA.tsx
|
||||||
|
│ └── theme-provider.tsx
|
||||||
|
└── utils/ # Utility functions
|
||||||
|
└── passwordGenerator.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Create a `.env.local` file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
NEXT_PUBLIC_SITE_URL=https://your-domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### PWA Configuration
|
||||||
|
|
||||||
|
The PWA is configured in `next.config.js` and `public/manifest.json`. Update the manifest with your app details.
|
||||||
|
|
||||||
|
## 📱 PWA Features
|
||||||
|
|
||||||
|
- **Offline Support**: Works without internet connection
|
||||||
|
- **Install Prompt**: Users can install as a native app
|
||||||
|
- **App Shortcuts**: Quick access to password generation
|
||||||
|
- **Splash Screen**: Custom loading screen
|
||||||
|
- **Theme Colors**: Consistent branding
|
||||||
|
|
||||||
|
## 🎨 Customization
|
||||||
|
|
||||||
|
### Colors
|
||||||
|
|
||||||
|
Update the primary color in `tailwind.config.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#eff6ff',
|
||||||
|
// ... other shades
|
||||||
|
900: '#1e3a8a',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Icons
|
||||||
|
|
||||||
|
Replace icons in `public/icons/` directory. The app uses multiple sizes for different devices.
|
||||||
|
|
||||||
|
## 📊 SEO & Performance
|
||||||
|
|
||||||
|
- **Lighthouse Score**: Optimized for 95+ Performance, 100 SEO, 95+ Accessibility
|
||||||
|
- **Structured Data**: JSON-LD schemas for FAQ and SoftwareApplication
|
||||||
|
- **Meta Tags**: Open Graph and Twitter Card support
|
||||||
|
- **Performance**: Optimized images, fonts, and bundle size
|
||||||
|
|
||||||
|
## 🔒 Security
|
||||||
|
|
||||||
|
- **Cryptographic Randomness**: Uses Web Crypto API's `getRandomValues()`
|
||||||
|
- **No Data Collection**: Zero tracking or analytics
|
||||||
|
- **Client-Side Only**: No server-side password processing
|
||||||
|
- **Open Source**: Transparent code for security review
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
- **Issues**: [GitHub Issues](https://github.com/your-username/passmaster/issues)
|
||||||
|
- **Email**: privacy@passmaster.app
|
||||||
|
- **Documentation**: [Wiki](https://github.com/your-username/passmaster/wiki)
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
- [Next.js](https://nextjs.org/) for the amazing framework
|
||||||
|
- [TailwindCSS](https://tailwindcss.com/) for the utility-first CSS
|
||||||
|
- [Lucide](https://lucide.dev/) for the beautiful icons
|
||||||
|
- [Framer Motion](https://www.framer.com/motion/) for the smooth animations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Made with ❤️ for privacy and security
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
--primary: 221.2 83.2% 53.3%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--secondary: 210 40% 96%;
|
||||||
|
--secondary-foreground: 222.2 84% 4.9%;
|
||||||
|
--muted: 210 40% 96%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
--accent: 210 40% 96%;
|
||||||
|
--accent-foreground: 222.2 84% 4.9%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 221.2 83.2% 53.3%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--primary: 217.2 91.2% 59.8%;
|
||||||
|
--primary-foreground: 222.2 84% 4.9%;
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 224.3 76.3% 94.1%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.btn-primary {
|
||||||
|
@apply bg-primary-600 hover:bg-primary-700 text-white font-medium py-3 px-6 rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
@apply bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-gray-100 font-medium py-3 px-6 rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
@apply bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
@apply w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-colors duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-meter {
|
||||||
|
@apply h-2 rounded-full transition-all duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-weak {
|
||||||
|
@apply bg-red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-ok {
|
||||||
|
@apply bg-yellow-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-strong {
|
||||||
|
@apply bg-blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-excellent {
|
||||||
|
@apply bg-green-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
import './globals.css'
|
||||||
|
import { ThemeProvider } from '@/components/theme-provider'
|
||||||
|
import { Header } from '@/components/layout/Header'
|
||||||
|
import { Footer } from '@/components/layout/Footer'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'PassMaster – Free Offline Secure Password Generator (Open Source)',
|
||||||
|
description: 'Generate ultra-secure passwords instantly, offline with client-side encryption. 100% open-source, private, and free.',
|
||||||
|
keywords: ['password generator', 'secure passwords', 'offline password generator', 'open source', 'privacy', 'security'],
|
||||||
|
authors: [{ name: 'PassMaster' }],
|
||||||
|
creator: 'PassMaster',
|
||||||
|
publisher: 'PassMaster',
|
||||||
|
formatDetection: {
|
||||||
|
email: false,
|
||||||
|
address: false,
|
||||||
|
telephone: false,
|
||||||
|
},
|
||||||
|
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'https://passmaster.app'),
|
||||||
|
alternates: {
|
||||||
|
canonical: '/',
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
icon: [
|
||||||
|
{ url: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
|
||||||
|
{ url: '/icons/icon-512.png', sizes: '512x512', type: 'image/png' },
|
||||||
|
],
|
||||||
|
shortcut: '/icons/icon-192.png',
|
||||||
|
apple: '/icons/icon-192.png',
|
||||||
|
},
|
||||||
|
manifest: '/manifest.json',
|
||||||
|
openGraph: {
|
||||||
|
title: 'PassMaster – Free Offline Secure Password Generator (Open Source)',
|
||||||
|
description: 'Generate ultra-secure passwords instantly, offline with client-side encryption. 100% open-source, private, and free.',
|
||||||
|
url: '/',
|
||||||
|
siteName: 'PassMaster',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: '/og-image.png',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'PassMaster - Secure Password Generator',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
locale: 'en_US',
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: 'PassMaster – Free Offline Secure Password Generator (Open Source)',
|
||||||
|
description: 'Generate ultra-secure passwords instantly, offline with client-side encryption. 100% open-source, private, and free.',
|
||||||
|
images: ['/og-image.png'],
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
googleBot: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
'max-video-preview': -1,
|
||||||
|
'max-image-preview': 'large',
|
||||||
|
'max-snippet': -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verification: {
|
||||||
|
google: 'your-google-verification-code',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<meta name="theme-color" content="#3b82f6" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
|
{/* Service Worker Registration */}
|
||||||
|
<script
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
navigator.serviceWorker.register('/sw.js')
|
||||||
|
.then(function(registration) {
|
||||||
|
console.log('SW registered: ', registration);
|
||||||
|
})
|
||||||
|
.catch(function(registrationError) {
|
||||||
|
console.log('SW registration failed: ', registrationError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* JSON-LD Schema */}
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: JSON.stringify({
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "SoftwareApplication",
|
||||||
|
"name": "PassMaster",
|
||||||
|
"applicationCategory": "SecurityApplication",
|
||||||
|
"operatingSystem": "Web",
|
||||||
|
"offers": {
|
||||||
|
"@type": "Offer",
|
||||||
|
"price": "0",
|
||||||
|
"priceCurrency": "USD"
|
||||||
|
},
|
||||||
|
"isAccessibleForFree": true,
|
||||||
|
"url": process.env.NEXT_PUBLIC_SITE_URL || "https://passmaster.app",
|
||||||
|
"description": "Generate ultra-secure passwords instantly, offline with client-side encryption. 100% open-source, private, and free.",
|
||||||
|
"author": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "PassMaster"
|
||||||
|
},
|
||||||
|
"softwareVersion": "1.0.0",
|
||||||
|
"downloadUrl": process.env.NEXT_PUBLIC_SITE_URL || "https://passmaster.app",
|
||||||
|
"installUrl": process.env.NEXT_PUBLIC_SITE_URL || "https://passmaster.app"
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body className="font-sans">
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
|
<div className="min-h-screen flex flex-col bg-gray-50 dark:bg-gray-900">
|
||||||
|
<Header />
|
||||||
|
<main className="flex-1">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,291 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
Wifi,
|
||||||
|
WifiOff,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
RefreshCw,
|
||||||
|
ArrowLeft
|
||||||
|
} from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default function OfflineTestPage() {
|
||||||
|
const [isOnline, setIsOnline] = useState(true)
|
||||||
|
const [serviceWorkerStatus, setServiceWorkerStatus] = useState<string>('checking')
|
||||||
|
const [cacheStatus, setCacheStatus] = useState<string>('checking')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check online status
|
||||||
|
const updateOnlineStatus = () => {
|
||||||
|
setIsOnline(navigator.onLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('online', updateOnlineStatus)
|
||||||
|
window.addEventListener('offline', updateOnlineStatus)
|
||||||
|
updateOnlineStatus()
|
||||||
|
|
||||||
|
// Check service worker status
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
|
if (registration.active) {
|
||||||
|
setServiceWorkerStatus('active')
|
||||||
|
} else {
|
||||||
|
setServiceWorkerStatus('inactive')
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
setServiceWorkerStatus('error')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setServiceWorkerStatus('not-supported')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache status
|
||||||
|
if ('caches' in window) {
|
||||||
|
caches.open('passmaster-v1.0.0').then((cache) => {
|
||||||
|
cache.keys().then((keys) => {
|
||||||
|
if (keys.length > 0) {
|
||||||
|
setCacheStatus('cached')
|
||||||
|
} else {
|
||||||
|
setCacheStatus('empty')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).catch(() => {
|
||||||
|
setCacheStatus('error')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setCacheStatus('not-supported')
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('online', updateOnlineStatus)
|
||||||
|
window.removeEventListener('offline', updateOnlineStatus)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'active':
|
||||||
|
case 'cached':
|
||||||
|
return <CheckCircle className="h-5 w-5 text-green-500" />
|
||||||
|
case 'inactive':
|
||||||
|
case 'empty':
|
||||||
|
case 'error':
|
||||||
|
return <XCircle className="h-5 w-5 text-red-500" />
|
||||||
|
case 'checking':
|
||||||
|
return <RefreshCw className="h-5 w-5 text-yellow-500 animate-spin" />
|
||||||
|
default:
|
||||||
|
return <XCircle className="h-5 w-5 text-gray-500" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'active':
|
||||||
|
return 'Service Worker Active'
|
||||||
|
case 'inactive':
|
||||||
|
return 'Service Worker Inactive'
|
||||||
|
case 'error':
|
||||||
|
return 'Service Worker Error'
|
||||||
|
case 'not-supported':
|
||||||
|
return 'Service Worker Not Supported'
|
||||||
|
case 'cached':
|
||||||
|
return 'Resources Cached'
|
||||||
|
case 'empty':
|
||||||
|
return 'Cache Empty'
|
||||||
|
case 'checking':
|
||||||
|
return 'Checking...'
|
||||||
|
default:
|
||||||
|
return 'Unknown Status'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="flex items-center text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-5 w-5 mr-2" />
|
||||||
|
Back to PassMaster
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="text-center mb-12"
|
||||||
|
>
|
||||||
|
<div className="flex justify-center mb-6">
|
||||||
|
<div className={`p-4 rounded-full ${isOnline ? 'bg-green-100 dark:bg-green-900/20' : 'bg-red-100 dark:bg-red-900/20'}`}>
|
||||||
|
{isOnline ? (
|
||||||
|
<Wifi className="h-12 w-12 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<WifiOff className="h-12 w-12 text-red-600" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
Offline Functionality Test
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-600 dark:text-gray-300">
|
||||||
|
Test your PWA's offline capabilities and service worker status.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Status Cards */}
|
||||||
|
<div className="grid md:grid-cols-2 gap-8 mb-12">
|
||||||
|
{/* Connection Status */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.1 }}
|
||||||
|
className={`bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border ${
|
||||||
|
isOnline ? 'border-green-200 dark:border-green-800' : 'border-red-200 dark:border-red-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Connection Status
|
||||||
|
</h3>
|
||||||
|
{isOnline ? (
|
||||||
|
<Wifi className="h-6 w-6 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<WifiOff className="h-6 w-6 text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className={`text-sm ${isOnline ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||||
|
{isOnline ? 'You are currently online' : 'You are currently offline'}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Service Worker Status */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Service Worker
|
||||||
|
</h3>
|
||||||
|
{getStatusIcon(serviceWorkerStatus)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{getStatusText(serviceWorkerStatus)}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Cache Status */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.3 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Cache Status
|
||||||
|
</h3>
|
||||||
|
{getStatusIcon(cacheStatus)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{getStatusText(cacheStatus)}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* PWA Status */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.4 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
PWA Status
|
||||||
|
</h3>
|
||||||
|
<CheckCircle className="h-6 w-6 text-green-500" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{isOnline && serviceWorkerStatus === 'active' && cacheStatus === 'cached'
|
||||||
|
? 'Ready for offline use'
|
||||||
|
: 'Some features may not work offline'}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Instructions */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.5 }}
|
||||||
|
className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-8"
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
How to Test Offline Functionality
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4 text-gray-600 dark:text-gray-300">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-bold">
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<p>Make sure you're online and the service worker is active (green checkmark above)</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-bold">
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<p>Navigate to the main page and let it fully load</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-bold">
|
||||||
|
3
|
||||||
|
</div>
|
||||||
|
<p>Disconnect your internet connection (turn off WiFi or unplug ethernet)</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-bold">
|
||||||
|
4
|
||||||
|
</div>
|
||||||
|
<p>Try navigating back to the main page - it should still work offline!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.6 }}
|
||||||
|
className="mt-8 flex flex-col sm:flex-row gap-4 justify-center"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Go to Main Page
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="inline-flex items-center justify-center px-6 py-3 border border-gray-300 dark:border-gray-600 text-base font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
Refresh Page
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,203 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
Shield,
|
||||||
|
Lock,
|
||||||
|
Zap,
|
||||||
|
Globe,
|
||||||
|
ArrowUp,
|
||||||
|
Key
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { PasswordGenerator } from '@/components/PasswordGenerator'
|
||||||
|
import { FAQ } from '@/components/FAQ'
|
||||||
|
import { FloatingCTA } from '@/components/FloatingCTA'
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
const [showScrollTop, setShowScrollTop] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
setShowScrollTop(window.scrollY > 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll)
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const scrollToTop = () => {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
icon: Lock,
|
||||||
|
title: "End-to-End Client-Side Encryption",
|
||||||
|
description: "Your passwords are generated locally in your browser. Nothing is ever sent to our servers."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Zap,
|
||||||
|
title: "Works Offline (PWA)",
|
||||||
|
description: "Install as an app and generate passwords even without an internet connection."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Globe,
|
||||||
|
title: "100% Open Source",
|
||||||
|
description: "Transparent code that you can audit, modify, and contribute to on GitHub."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="py-20 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-4xl mx-auto text-center">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="mb-8"
|
||||||
|
>
|
||||||
|
<div className="flex justify-center mb-6">
|
||||||
|
<div className="p-4 bg-primary-100 dark:bg-primary-900/20 rounded-full">
|
||||||
|
<Shield className="h-12 w-12 text-primary-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-6xl font-bold text-gray-900 dark:text-white mb-6">
|
||||||
|
Free Offline Secure Password Generator
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl md:text-2xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto leading-relaxed">
|
||||||
|
Generate strong, unique passwords in seconds — fully client-side, private, and open-source.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Primary CTA */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
|
className="mb-12"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#generator"
|
||||||
|
className="btn-primary text-lg px-8 py-4 inline-flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<Key className="h-5 w-5" />
|
||||||
|
<span>Generate Password</span>
|
||||||
|
</a>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Features Section */}
|
||||||
|
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-gray-50 dark:bg-gray-800">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-center mb-12"
|
||||||
|
>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
Why PassMaster is Safer
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
|
||||||
|
Built with privacy and security as the foundation, not an afterthought.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
|
{features.map((feature, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={feature.title}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
className="card text-center group cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="flex justify-center mb-4">
|
||||||
|
<div className="p-3 bg-primary-100 dark:bg-primary-900/20 rounded-full group-hover:bg-primary-200 dark:group-hover:bg-primary-900/40 transition-colors duration-200">
|
||||||
|
<feature.icon className="h-8 w-8 text-primary-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||||
|
{feature.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Password Generator Section */}
|
||||||
|
<section id="generator" className="py-16 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-center mb-12"
|
||||||
|
>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
Generate Your Strong Password
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300">
|
||||||
|
Customize your password settings and generate secure passwords instantly.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<PasswordGenerator />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* FAQ Section */}
|
||||||
|
<section id="faq" className="py-16 px-4 sm:px-6 lg:px-8 bg-gray-50 dark:bg-gray-800">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-center mb-12"
|
||||||
|
>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
Frequently Asked Questions
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300">
|
||||||
|
Everything you need to know about PassMaster and password security.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<FAQ />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Floating CTA */}
|
||||||
|
<FloatingCTA />
|
||||||
|
|
||||||
|
{/* Scroll to Top Button */}
|
||||||
|
{showScrollTop && (
|
||||||
|
<motion.button
|
||||||
|
onClick={scrollToTop}
|
||||||
|
className="fixed bottom-6 right-6 z-50 p-3 bg-primary-600 hover:bg-primary-700 text-white rounded-full shadow-lg transition-colors duration-200"
|
||||||
|
initial={{ opacity: 0, scale: 0 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0 }}
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
aria-label="Scroll to top"
|
||||||
|
>
|
||||||
|
<ArrowUp className="h-5 w-5" />
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,280 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
Shield,
|
||||||
|
Lock,
|
||||||
|
Eye,
|
||||||
|
Server,
|
||||||
|
FileText,
|
||||||
|
CheckCircle,
|
||||||
|
ArrowLeft
|
||||||
|
} from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default function PrivacyPage() {
|
||||||
|
const privacyFeatures = [
|
||||||
|
{
|
||||||
|
icon: Lock,
|
||||||
|
title: "Client-Side Only",
|
||||||
|
description: "All password generation happens locally in your browser. No data is ever sent to our servers."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Eye,
|
||||||
|
title: "No Tracking",
|
||||||
|
description: "We don't use cookies, analytics, or any tracking mechanisms. Your privacy is guaranteed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Server,
|
||||||
|
title: "No Server Storage",
|
||||||
|
description: "We don't store any passwords, user data, or personal information on our servers."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Shield,
|
||||||
|
title: "Open Source",
|
||||||
|
description: "All code is publicly available and auditable. You can verify our privacy claims."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const dataPractices = [
|
||||||
|
{
|
||||||
|
title: "What We Don't Collect",
|
||||||
|
items: [
|
||||||
|
"Passwords or generated content",
|
||||||
|
"Personal information",
|
||||||
|
"IP addresses",
|
||||||
|
"Browser history",
|
||||||
|
"Usage analytics",
|
||||||
|
"Device information"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "What We Don't Store",
|
||||||
|
items: [
|
||||||
|
"User accounts",
|
||||||
|
"Password history",
|
||||||
|
"Settings or preferences",
|
||||||
|
"Session data",
|
||||||
|
"Cookies or local storage",
|
||||||
|
"Any personal data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "What We Don't Share",
|
||||||
|
items: [
|
||||||
|
"No third-party services",
|
||||||
|
"No advertising networks",
|
||||||
|
"No analytics providers",
|
||||||
|
"No data brokers",
|
||||||
|
"No government requests",
|
||||||
|
"No commercial use"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="flex items-center text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-5 w-5 mr-2" />
|
||||||
|
Back to PassMaster
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="text-center mb-16"
|
||||||
|
>
|
||||||
|
<div className="flex justify-center mb-6">
|
||||||
|
<div className="p-4 bg-blue-100 dark:bg-blue-900/20 rounded-full">
|
||||||
|
<Shield className="h-12 w-12 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-6">
|
||||||
|
Privacy Policy
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
|
||||||
|
Your privacy is our top priority. PassMaster is designed with privacy-first principles.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Privacy Features */}
|
||||||
|
<section className="mb-16">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-center mb-12"
|
||||||
|
>
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
Privacy-First Design
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300">
|
||||||
|
Every aspect of PassMaster is built to protect your privacy.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
|
{privacyFeatures.map((feature, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={feature.title}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-start space-x-4">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="p-3 bg-blue-100 dark:bg-blue-900/20 rounded-lg">
|
||||||
|
<feature.icon className="h-6 w-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
{feature.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Data Practices */}
|
||||||
|
<section className="mb-16">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-center mb-12"
|
||||||
|
>
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
Our Data Practices
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300">
|
||||||
|
Transparency about how we handle (or don't handle) your data.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
|
{dataPractices.map((practice, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={practice.title}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||||
|
{practice.title}
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{practice.items.map((item, itemIndex) => (
|
||||||
|
<li key={itemIndex} className="flex items-start space-x-3">
|
||||||
|
<CheckCircle className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span className="text-gray-600 dark:text-gray-300">{item}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Technical Details */}
|
||||||
|
<section className="mb-16">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-sm border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-center mb-6">
|
||||||
|
<FileText className="h-8 w-8 text-blue-600 mr-3" />
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Technical Implementation
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="prose dark:prose-invert max-w-none">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">How PassMaster Works</h3>
|
||||||
|
<ul className="space-y-3 text-gray-600 dark:text-gray-300">
|
||||||
|
<li className="flex items-start space-x-3">
|
||||||
|
<CheckCircle className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span><strong>Local Processing:</strong> All password generation happens in your browser using JavaScript</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start space-x-3">
|
||||||
|
<CheckCircle className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span><strong>No Network Requests:</strong> The app works completely offline after initial load</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start space-x-3">
|
||||||
|
<CheckCircle className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span><strong>Open Source:</strong> All code is publicly available on GitHub for verification</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start space-x-3">
|
||||||
|
<CheckCircle className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span><strong>No Dependencies:</strong> We don't use external services or third-party libraries that could track you</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Contact */}
|
||||||
|
<section className="text-center">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-8"
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
Questions About Privacy?
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||||
|
We're committed to transparency. If you have any questions about our privacy practices,
|
||||||
|
please review our source code or contact us.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<a
|
||||||
|
href="https://github.com/your-repo/passmaster"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
View Source Code
|
||||||
|
</a>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-flex items-center justify-center px-6 py-3 border border-gray-300 dark:border-gray-600 text-base font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
Back to Generator
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
poweredByHeader: false,
|
||||||
|
images: {
|
||||||
|
domains: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self'; font-src 'self' data:;" always;
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header Referrer-Policy no-referrer;
|
||||||
|
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()";
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"name": "passmaster",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"setup": "node scripts/setup.js",
|
||||||
|
"status": "node scripts/check-status.js",
|
||||||
|
"generate-icons": "node scripts/generate-icons.js",
|
||||||
|
"create-screenshots": "node scripts/create-screenshots.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "^14.0.4",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"lucide-react": "^0.294.0",
|
||||||
|
"framer-motion": "^10.16.16",
|
||||||
|
"next-themes": "^0.2.1",
|
||||||
|
"next-pwa": "^5.6.0",
|
||||||
|
"zustand": "^4.4.7",
|
||||||
|
"zod": "^3.22.4",
|
||||||
|
"uuid": "^9.0.1",
|
||||||
|
"idb": "^8.0.0"
|
||||||
|
},
|
||||||
|
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.10.5",
|
||||||
|
"@types/react": "^18.2.45",
|
||||||
|
"@types/react-dom": "^18.2.18",
|
||||||
|
"@types/uuid": "^9.0.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||||
|
"@typescript-eslint/parser": "^6.16.0",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-config-next": "^14.0.4",
|
||||||
|
"postcss": "^8.4.32",
|
||||||
|
"prettier": "^3.1.1",
|
||||||
|
"sharp": "^0.33.2",
|
||||||
|
"tailwindcss": "^3.4.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit fe04ad9f53e83cba29bc81842327a7cb47d93e76
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
After Width: | Height: | Size: 921 KiB |
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Icons Directory
|
||||||
|
|
||||||
|
This directory should contain the following icon files for PWA support:
|
||||||
|
|
||||||
|
## Required Icons
|
||||||
|
|
||||||
|
- `icon-72.png` (72x72)
|
||||||
|
- `icon-96.png` (96x96)
|
||||||
|
- `icon-128.png` (128x128)
|
||||||
|
- `icon-144.png` (144x144)
|
||||||
|
- `icon-152.png` (152x152)
|
||||||
|
- `icon-192.png` (192x192)
|
||||||
|
- `icon-384.png` (384x384)
|
||||||
|
- `icon-512.png` (512x512)
|
||||||
|
|
||||||
|
## Additional Files
|
||||||
|
|
||||||
|
- `apple-touch-icon.png` (180x180)
|
||||||
|
- `favicon.ico` (16x16, 32x32)
|
||||||
|
|
||||||
|
## Icon Design Guidelines
|
||||||
|
|
||||||
|
- Use a shield or lock symbol to represent security
|
||||||
|
- Primary color: #3b82f6 (blue-600)
|
||||||
|
- Ensure good contrast for accessibility
|
||||||
|
- Test on both light and dark backgrounds
|
||||||
|
- Make sure icons are recognizable at small sizes
|
||||||
|
|
||||||
|
## Generation
|
||||||
|
|
||||||
|
You can use tools like:
|
||||||
|
- [Favicon Generator](https://realfavicongenerator.net/)
|
||||||
|
- [PWA Builder](https://www.pwabuilder.com/imageGenerator)
|
||||||
|
- [Figma](https://figma.com/) for design
|
||||||
|
- [Inkscape](https://inkscape.org/) for vector graphics
|
||||||
|
After Width: | Height: | Size: 921 KiB |
|
After Width: | Height: | Size: 921 KiB |
|
After Width: | Height: | Size: 921 KiB |
|
After Width: | Height: | Size: 921 KiB |
|
After Width: | Height: | Size: 921 KiB |
|
After Width: | Height: | Size: 921 KiB |
|
After Width: | Height: | Size: 921 KiB |
|
After Width: | Height: | Size: 921 KiB |
|
|
@ -0,0 +1,111 @@
|
||||||
|
{
|
||||||
|
"name": "PassMaster - Free Offline Secure Password Generator",
|
||||||
|
"short_name": "PassMaster",
|
||||||
|
"description": "Generate ultra-secure passwords instantly, offline with client-side encryption. 100% open-source, private, and free.",
|
||||||
|
"id": "passmaster-pwa",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"theme_color": "#3b82f6",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"scope": "/",
|
||||||
|
"lang": "en",
|
||||||
|
"categories": ["security", "utilities", "productivity", "developer-tools"],
|
||||||
|
"prefer_related_applications": false,
|
||||||
|
"related_applications": [],
|
||||||
|
"edge_side_panel": {
|
||||||
|
"preferred_width": 400
|
||||||
|
},
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-72.png",
|
||||||
|
"sizes": "72x72",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-96.png",
|
||||||
|
"sizes": "96x96",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-128.png",
|
||||||
|
"sizes": "128x128",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-144.png",
|
||||||
|
"sizes": "144x144",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-152.png",
|
||||||
|
"sizes": "152x152",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-384.png",
|
||||||
|
"sizes": "384x384",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "Generate Password",
|
||||||
|
"short_name": "Generate",
|
||||||
|
"description": "Quickly generate a new secure password",
|
||||||
|
"url": "/#generator",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-96.png",
|
||||||
|
"sizes": "96x96"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Password History",
|
||||||
|
"short_name": "History",
|
||||||
|
"description": "View recently generated passwords",
|
||||||
|
"url": "/#history",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-96.png",
|
||||||
|
"sizes": "96x96"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"src": "/screenshots/desktop.png",
|
||||||
|
"sizes": "1280x720",
|
||||||
|
"type": "image/png",
|
||||||
|
"form_factor": "wide",
|
||||||
|
"label": "PassMaster Desktop Interface"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/screenshots/mobile.png",
|
||||||
|
"sizes": "390x844",
|
||||||
|
"type": "image/png",
|
||||||
|
"form_factor": "narrow",
|
||||||
|
"label": "PassMaster Mobile Interface"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Screenshots Directory
|
||||||
|
|
||||||
|
This directory should contain screenshots for PWA app store listings:
|
||||||
|
|
||||||
|
## Required Screenshots
|
||||||
|
|
||||||
|
- `desktop.png` (1280x720) - Desktop interface screenshot
|
||||||
|
- `mobile.png` (390x844) - Mobile interface screenshot
|
||||||
|
|
||||||
|
## Screenshot Guidelines
|
||||||
|
|
||||||
|
- Show the password generator in action
|
||||||
|
- Display a generated password with strength meter
|
||||||
|
- Include the feature cards section
|
||||||
|
- Ensure good contrast and readability
|
||||||
|
- Use consistent branding colors
|
||||||
|
- Test on different devices and orientations
|
||||||
|
|
||||||
|
## Content Suggestions
|
||||||
|
|
||||||
|
- Desktop: Show the full interface with generator, features, and FAQ
|
||||||
|
- Mobile: Focus on the password generator and mobile navigation
|
||||||
|
- Include both light and dark mode versions if possible
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="1280" height="720" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="100%" height="100%" fill="#f3f4f6"/>
|
||||||
|
<rect x="0" y="0" width="100%" height="60" fill="#3b82f6"/>
|
||||||
|
<text x="50%" y="35" text-anchor="middle" fill="white" font-family="Arial, sans-serif" font-size="24" font-weight="bold">PassMaster</text>
|
||||||
|
<text x="50%" y="80" text-anchor="middle" fill="#6b7280" font-family="Arial, sans-serif" font-size="16">Desktop Interface</text>
|
||||||
|
<rect x="50" y="120" width="1180" height="200" rx="8" fill="white" stroke="#d1d5db" stroke-width="2"/>
|
||||||
|
<text x="50%" y="150" text-anchor="middle" fill="#374151" font-family="Arial, sans-serif" font-size="14">Password Generator Interface</text>
|
||||||
|
<rect x="80" y="180" width="1120" height="40" rx="4" fill="#f9fafb" stroke="#d1d5db"/>
|
||||||
|
<text x="50%" y="205" text-anchor="middle" fill="#6b7280" font-family="monospace" font-size="12">••••••••••••••••</text>
|
||||||
|
<rect x="80" y="240" width="120" height="40" rx="4" fill="#3b82f6"/>
|
||||||
|
<text x="140" y="265" text-anchor="middle" fill="white" font-family="Arial, sans-serif" font-size="14">Generate</text>
|
||||||
|
<text x="50%" y="690" text-anchor="middle" fill="#9ca3af" font-family="Arial, sans-serif" font-size="12">Screenshot Placeholder</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="390" height="844" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="100%" height="100%" fill="#f3f4f6"/>
|
||||||
|
<rect x="0" y="0" width="100%" height="60" fill="#3b82f6"/>
|
||||||
|
<text x="50%" y="35" text-anchor="middle" fill="white" font-family="Arial, sans-serif" font-size="20" font-weight="bold">PassMaster</text>
|
||||||
|
<text x="50%" y="80" text-anchor="middle" fill="#6b7280" font-family="Arial, sans-serif" font-size="14">Mobile Interface</text>
|
||||||
|
<rect x="20" y="120" width="350" height="200" rx="8" fill="white" stroke="#d1d5db" stroke-width="2"/>
|
||||||
|
<text x="50%" y="150" text-anchor="middle" fill="#374151" font-family="Arial, sans-serif" font-size="12">Password Generator Interface</text>
|
||||||
|
<rect x="40" y="180" width="310" height="40" rx="4" fill="#f9fafb" stroke="#d1d5db"/>
|
||||||
|
<text x="50%" y="205" text-anchor="middle" fill="#6b7280" font-family="monospace" font-size="10">••••••••••••••••</text>
|
||||||
|
<rect x="40" y="240" width="120" height="40" rx="4" fill="#3b82f6"/>
|
||||||
|
<text x="100" y="265" text-anchor="middle" fill="white" font-family="Arial, sans-serif" font-size="12">Generate</text>
|
||||||
|
<text x="50%" y="814" text-anchor="middle" fill="#9ca3af" font-family="Arial, sans-serif" font-size="10">Screenshot Placeholder</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,30 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
hostname: 'localhost',
|
||||||
|
port: 3000,
|
||||||
|
path: '/',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 5000
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = http.request(options, (res) => {
|
||||||
|
console.log(`✅ Server is running! Status: ${res.statusCode}`);
|
||||||
|
console.log(`🌐 Access the app at: http://localhost:3000`);
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (err) => {
|
||||||
|
console.log('❌ Server is not running or not accessible');
|
||||||
|
console.log('💡 Make sure to run: npm run dev');
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('timeout', () => {
|
||||||
|
console.log('⏰ Request timed out - server might be starting up');
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const screenshotsDir = path.join(__dirname, '../public/screenshots');
|
||||||
|
|
||||||
|
// Ensure screenshots directory exists
|
||||||
|
if (!fs.existsSync(screenshotsDir)) {
|
||||||
|
fs.mkdirSync(screenshotsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a simple placeholder screenshot
|
||||||
|
function createPlaceholderScreenshot(filename, width, height, description) {
|
||||||
|
const svgContent = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="100%" height="100%" fill="#f3f4f6"/>
|
||||||
|
<rect x="0" y="0" width="100%" height="60" fill="#3b82f6"/>
|
||||||
|
<text x="50%" y="35" text-anchor="middle" fill="white" font-family="Arial, sans-serif" font-size="24" font-weight="bold">PassMaster</text>
|
||||||
|
<text x="50%" y="80" text-anchor="middle" fill="#6b7280" font-family="Arial, sans-serif" font-size="16">${description}</text>
|
||||||
|
<rect x="50" y="120" width="${width-100}" height="200" rx="8" fill="white" stroke="#d1d5db" stroke-width="2"/>
|
||||||
|
<text x="50%" y="150" text-anchor="middle" fill="#374151" font-family="Arial, sans-serif" font-size="14">Password Generator Interface</text>
|
||||||
|
<rect x="80" y="180" width="${width-160}" height="40" rx="4" fill="#f9fafb" stroke="#d1d5db"/>
|
||||||
|
<text x="50%" y="205" text-anchor="middle" fill="#6b7280" font-family="monospace" font-size="12">••••••••••••••••</text>
|
||||||
|
<rect x="80" y="240" width="120" height="40" rx="4" fill="#3b82f6"/>
|
||||||
|
<text x="140" y="265" text-anchor="middle" fill="white" font-family="Arial, sans-serif" font-size="14">Generate</text>
|
||||||
|
<text x="50%" y="${height-30}" text-anchor="middle" fill="#9ca3af" font-family="Arial, sans-serif" font-size="12">Screenshot Placeholder</text>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
const outputPath = path.join(screenshotsDir, filename);
|
||||||
|
fs.writeFileSync(outputPath, svgContent);
|
||||||
|
console.log(`✅ Created ${filename}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createScreenshots() {
|
||||||
|
console.log('🔄 Creating PWA screenshots...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Desktop screenshot
|
||||||
|
createPlaceholderScreenshot('desktop.png', 1280, 720, 'Desktop Interface');
|
||||||
|
|
||||||
|
// Mobile screenshot
|
||||||
|
createPlaceholderScreenshot('mobile.png', 390, 844, 'Mobile Interface');
|
||||||
|
|
||||||
|
console.log('🎉 All screenshots created successfully!');
|
||||||
|
console.log('📝 Note: These are placeholder SVGs. Replace with actual screenshots');
|
||||||
|
console.log(' of your application for better app store listings.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error creating screenshots:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createScreenshots();
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
|
||||||
|
const inputIcon = path.join(__dirname, '../public/icon.png');
|
||||||
|
const outputDir = path.join(__dirname, '../public/icons');
|
||||||
|
|
||||||
|
// Ensure output directory exists
|
||||||
|
if (!fs.existsSync(outputDir)) {
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if input icon exists
|
||||||
|
if (!fs.existsSync(inputIcon)) {
|
||||||
|
console.error('❌ Input icon not found:', inputIcon);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateIcons() {
|
||||||
|
console.log('🔄 Generating PWA icons...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read the original icon
|
||||||
|
const iconBuffer = fs.readFileSync(inputIcon);
|
||||||
|
|
||||||
|
for (const size of sizes) {
|
||||||
|
const outputPath = path.join(outputDir, `icon-${size}.png`);
|
||||||
|
|
||||||
|
// Copy the original icon to create the size variants
|
||||||
|
fs.writeFileSync(outputPath, iconBuffer);
|
||||||
|
|
||||||
|
console.log(`✅ Generated icon-${size}.png`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🎉 All icons generated successfully!');
|
||||||
|
console.log('📝 Note: All icons are copies of the original. For optimal quality,');
|
||||||
|
console.log(' consider resizing them manually or using an image editor.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error generating icons:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateIcons();
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
console.log('🚀 PassMaster Setup Script');
|
||||||
|
console.log('==========================');
|
||||||
|
|
||||||
|
// Create .env.local if it doesn't exist
|
||||||
|
const envPath = path.join(process.cwd(), '.env.local');
|
||||||
|
if (!fs.existsSync(envPath)) {
|
||||||
|
const envContent = `# Site URL for metadata and PWA
|
||||||
|
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
||||||
|
|
||||||
|
# For production, change to your actual domain
|
||||||
|
# NEXT_PUBLIC_SITE_URL=https://passmaster.app
|
||||||
|
`;
|
||||||
|
fs.writeFileSync(envPath, envContent);
|
||||||
|
console.log('✅ Created .env.local file');
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ .env.local already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if node_modules exists
|
||||||
|
const nodeModulesPath = path.join(process.cwd(), 'node_modules');
|
||||||
|
if (!fs.existsSync(nodeModulesPath)) {
|
||||||
|
console.log('📦 Installing dependencies...');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
try {
|
||||||
|
execSync('npm install', { stdio: 'inherit' });
|
||||||
|
console.log('✅ Dependencies installed');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to install dependencies:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ Dependencies already installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n🎉 Setup complete!');
|
||||||
|
console.log('\nNext steps:');
|
||||||
|
console.log('1. Run "npm run dev" to start the development server');
|
||||||
|
console.log('2. Open http://localhost:3000 in your browser');
|
||||||
|
console.log('3. Add icon files to public/icons/ directory');
|
||||||
|
console.log('4. Update .env.local with your production URL when deploying');
|
||||||
|
console.log('\nHappy coding! 🚀');
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
--primary: 221.2 83.2% 53.3%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--secondary: 210 40% 96%;
|
||||||
|
--secondary-foreground: 222.2 84% 4.9%;
|
||||||
|
--muted: 210 40% 96%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
--accent: 210 40% 96%;
|
||||||
|
--accent-foreground: 222.2 84% 4.9%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 221.2 83.2% 53.3%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--primary: 217.2 91.2% 59.8%;
|
||||||
|
--primary-foreground: 222.2 84% 4.9%;
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 224.3 76.3% 94.1%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.btn-primary {
|
||||||
|
@apply bg-primary-600 hover:bg-primary-700 text-white font-medium py-3 px-6 rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
@apply bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-gray-100 font-medium py-3 px-6 rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
@apply bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
@apply w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-colors duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-meter {
|
||||||
|
@apply h-2 rounded-full transition-all duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-weak {
|
||||||
|
@apply bg-red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-ok {
|
||||||
|
@apply bg-yellow-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-strong {
|
||||||
|
@apply bg-blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-excellent {
|
||||||
|
@apply bg-green-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
import './globals.css'
|
||||||
|
import { ThemeProvider } from '@/components/theme-provider'
|
||||||
|
import { Header } from '@/components/layout/Header'
|
||||||
|
import { Footer } from '@/components/layout/Footer'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'PassMaster – Free Offline Secure Password Generator (Open Source)',
|
||||||
|
description: 'Generate ultra-secure passwords instantly, offline with client-side encryption. 100% open-source, private, and free.',
|
||||||
|
keywords: ['password generator', 'secure passwords', 'offline password generator', 'open source', 'privacy', 'security'],
|
||||||
|
authors: [{ name: 'PassMaster' }],
|
||||||
|
creator: 'PassMaster',
|
||||||
|
publisher: 'PassMaster',
|
||||||
|
formatDetection: {
|
||||||
|
email: false,
|
||||||
|
address: false,
|
||||||
|
telephone: false,
|
||||||
|
},
|
||||||
|
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'https://passmaster.app'),
|
||||||
|
alternates: {
|
||||||
|
canonical: '/',
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
icon: [
|
||||||
|
{ url: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
|
||||||
|
{ url: '/icons/icon-512.png', sizes: '512x512', type: 'image/png' },
|
||||||
|
],
|
||||||
|
shortcut: '/icons/icon-192.png',
|
||||||
|
apple: '/icons/icon-192.png',
|
||||||
|
},
|
||||||
|
manifest: '/manifest.json',
|
||||||
|
openGraph: {
|
||||||
|
title: 'PassMaster – Free Offline Secure Password Generator (Open Source)',
|
||||||
|
description: 'Generate ultra-secure passwords instantly, offline with client-side encryption. 100% open-source, private, and free.',
|
||||||
|
url: '/',
|
||||||
|
siteName: 'PassMaster',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: '/og-image.png',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'PassMaster - Secure Password Generator',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
locale: 'en_US',
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: 'PassMaster – Free Offline Secure Password Generator (Open Source)',
|
||||||
|
description: 'Generate ultra-secure passwords instantly, offline with client-side encryption. 100% open-source, private, and free.',
|
||||||
|
images: ['/og-image.png'],
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
googleBot: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
'max-video-preview': -1,
|
||||||
|
'max-image-preview': 'large',
|
||||||
|
'max-snippet': -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verification: {
|
||||||
|
google: 'your-google-verification-code',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<meta name="theme-color" content="#3b82f6" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
|
{/* Service Worker Registration */}
|
||||||
|
<script
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
navigator.serviceWorker.register('/sw.js')
|
||||||
|
.then(function(registration) {
|
||||||
|
console.log('SW registered: ', registration);
|
||||||
|
})
|
||||||
|
.catch(function(registrationError) {
|
||||||
|
console.log('SW registration failed: ', registrationError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* JSON-LD Schema */}
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: JSON.stringify({
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "SoftwareApplication",
|
||||||
|
"name": "PassMaster",
|
||||||
|
"applicationCategory": "SecurityApplication",
|
||||||
|
"operatingSystem": "Web",
|
||||||
|
"offers": {
|
||||||
|
"@type": "Offer",
|
||||||
|
"price": "0",
|
||||||
|
"priceCurrency": "USD"
|
||||||
|
},
|
||||||
|
"isAccessibleForFree": true,
|
||||||
|
"url": process.env.NEXT_PUBLIC_SITE_URL || "https://passmaster.app",
|
||||||
|
"description": "Generate ultra-secure passwords instantly, offline with client-side encryption. 100% open-source, private, and free.",
|
||||||
|
"author": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "PassMaster"
|
||||||
|
},
|
||||||
|
"softwareVersion": "1.0.0",
|
||||||
|
"downloadUrl": process.env.NEXT_PUBLIC_SITE_URL || "https://passmaster.app",
|
||||||
|
"installUrl": process.env.NEXT_PUBLIC_SITE_URL || "https://passmaster.app"
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body className="font-sans">
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
|
<div className="min-h-screen flex flex-col bg-gray-50 dark:bg-gray-900">
|
||||||
|
<Header />
|
||||||
|
<main className="flex-1">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,291 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
Wifi,
|
||||||
|
WifiOff,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
RefreshCw,
|
||||||
|
ArrowLeft
|
||||||
|
} from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default function OfflineTestPage() {
|
||||||
|
const [isOnline, setIsOnline] = useState(true)
|
||||||
|
const [serviceWorkerStatus, setServiceWorkerStatus] = useState<string>('checking')
|
||||||
|
const [cacheStatus, setCacheStatus] = useState<string>('checking')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check online status
|
||||||
|
const updateOnlineStatus = () => {
|
||||||
|
setIsOnline(navigator.onLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('online', updateOnlineStatus)
|
||||||
|
window.addEventListener('offline', updateOnlineStatus)
|
||||||
|
updateOnlineStatus()
|
||||||
|
|
||||||
|
// Check service worker status
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
|
if (registration.active) {
|
||||||
|
setServiceWorkerStatus('active')
|
||||||
|
} else {
|
||||||
|
setServiceWorkerStatus('inactive')
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
setServiceWorkerStatus('error')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setServiceWorkerStatus('not-supported')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache status
|
||||||
|
if ('caches' in window) {
|
||||||
|
caches.open('passmaster-v1.0.0').then((cache) => {
|
||||||
|
cache.keys().then((keys) => {
|
||||||
|
if (keys.length > 0) {
|
||||||
|
setCacheStatus('cached')
|
||||||
|
} else {
|
||||||
|
setCacheStatus('empty')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).catch(() => {
|
||||||
|
setCacheStatus('error')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setCacheStatus('not-supported')
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('online', updateOnlineStatus)
|
||||||
|
window.removeEventListener('offline', updateOnlineStatus)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'active':
|
||||||
|
case 'cached':
|
||||||
|
return <CheckCircle className="h-5 w-5 text-green-500" />
|
||||||
|
case 'inactive':
|
||||||
|
case 'empty':
|
||||||
|
case 'error':
|
||||||
|
return <XCircle className="h-5 w-5 text-red-500" />
|
||||||
|
case 'checking':
|
||||||
|
return <RefreshCw className="h-5 w-5 text-yellow-500 animate-spin" />
|
||||||
|
default:
|
||||||
|
return <XCircle className="h-5 w-5 text-gray-500" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'active':
|
||||||
|
return 'Service Worker Active'
|
||||||
|
case 'inactive':
|
||||||
|
return 'Service Worker Inactive'
|
||||||
|
case 'error':
|
||||||
|
return 'Service Worker Error'
|
||||||
|
case 'not-supported':
|
||||||
|
return 'Service Worker Not Supported'
|
||||||
|
case 'cached':
|
||||||
|
return 'Resources Cached'
|
||||||
|
case 'empty':
|
||||||
|
return 'Cache Empty'
|
||||||
|
case 'checking':
|
||||||
|
return 'Checking...'
|
||||||
|
default:
|
||||||
|
return 'Unknown Status'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="flex items-center text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-5 w-5 mr-2" />
|
||||||
|
Back to PassMaster
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="text-center mb-12"
|
||||||
|
>
|
||||||
|
<div className="flex justify-center mb-6">
|
||||||
|
<div className={`p-4 rounded-full ${isOnline ? 'bg-green-100 dark:bg-green-900/20' : 'bg-red-100 dark:bg-red-900/20'}`}>
|
||||||
|
{isOnline ? (
|
||||||
|
<Wifi className="h-12 w-12 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<WifiOff className="h-12 w-12 text-red-600" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
Offline Functionality Test
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-600 dark:text-gray-300">
|
||||||
|
Test your PWA's offline capabilities and service worker status.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Status Cards */}
|
||||||
|
<div className="grid md:grid-cols-2 gap-8 mb-12">
|
||||||
|
{/* Connection Status */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.1 }}
|
||||||
|
className={`bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border ${
|
||||||
|
isOnline ? 'border-green-200 dark:border-green-800' : 'border-red-200 dark:border-red-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Connection Status
|
||||||
|
</h3>
|
||||||
|
{isOnline ? (
|
||||||
|
<Wifi className="h-6 w-6 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<WifiOff className="h-6 w-6 text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className={`text-sm ${isOnline ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||||
|
{isOnline ? 'You are currently online' : 'You are currently offline'}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Service Worker Status */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Service Worker
|
||||||
|
</h3>
|
||||||
|
{getStatusIcon(serviceWorkerStatus)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{getStatusText(serviceWorkerStatus)}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Cache Status */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.3 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Cache Status
|
||||||
|
</h3>
|
||||||
|
{getStatusIcon(cacheStatus)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{getStatusText(cacheStatus)}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* PWA Status */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.4 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
PWA Status
|
||||||
|
</h3>
|
||||||
|
<CheckCircle className="h-6 w-6 text-green-500" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{isOnline && serviceWorkerStatus === 'active' && cacheStatus === 'cached'
|
||||||
|
? 'Ready for offline use'
|
||||||
|
: 'Some features may not work offline'}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Instructions */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.5 }}
|
||||||
|
className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-8"
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
How to Test Offline Functionality
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4 text-gray-600 dark:text-gray-300">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-bold">
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<p>Make sure you're online and the service worker is active (green checkmark above)</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-bold">
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<p>Navigate to the main page and let it fully load</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-bold">
|
||||||
|
3
|
||||||
|
</div>
|
||||||
|
<p>Disconnect your internet connection (turn off WiFi or unplug ethernet)</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-bold">
|
||||||
|
4
|
||||||
|
</div>
|
||||||
|
<p>Try navigating back to the main page - it should still work offline!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.6 }}
|
||||||
|
className="mt-8 flex flex-col sm:flex-row gap-4 justify-center"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Go to Main Page
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="inline-flex items-center justify-center px-6 py-3 border border-gray-300 dark:border-gray-600 text-base font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
Refresh Page
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
Shield,
|
||||||
|
Lock,
|
||||||
|
Zap,
|
||||||
|
Globe,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
RefreshCw,
|
||||||
|
Info,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
Key,
|
||||||
|
Smartphone,
|
||||||
|
Users,
|
||||||
|
ArrowUp
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { PasswordGenerator } from '@/components/PasswordGenerator'
|
||||||
|
import { FAQ } from '@/components/FAQ'
|
||||||
|
import { FloatingCTA } from '@/components/FloatingCTA'
|
||||||
|
import { PWAInstallPrompt } from '@/components/PWAInstallPrompt'
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
const [showScrollTop, setShowScrollTop] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
setShowScrollTop(window.scrollY > 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll)
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const scrollToTop = () => {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
icon: Lock,
|
||||||
|
title: "End-to-End Client-Side Encryption",
|
||||||
|
description: "Your passwords are generated locally in your browser. Nothing is ever sent to our servers."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Zap,
|
||||||
|
title: "Works Offline (PWA)",
|
||||||
|
description: "Install as an app and generate passwords even without an internet connection."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Globe,
|
||||||
|
title: "100% Open Source",
|
||||||
|
description: "Transparent code that you can audit, modify, and contribute to on GitHub."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="py-20 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-4xl mx-auto text-center">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="mb-8"
|
||||||
|
>
|
||||||
|
<div className="flex justify-center mb-6">
|
||||||
|
<div className="p-4 bg-primary-100 dark:bg-primary-900/20 rounded-full">
|
||||||
|
<Shield className="h-12 w-12 text-primary-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-6xl font-bold text-gray-900 dark:text-white mb-6">
|
||||||
|
Free Offline Secure Password Generator
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl md:text-2xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto leading-relaxed">
|
||||||
|
Generate strong, unique passwords in seconds — fully client-side, private, and open-source.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Primary CTA */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
|
className="mb-12"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#generator"
|
||||||
|
className="btn-primary text-lg px-8 py-4 inline-flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<Key className="h-5 w-5" />
|
||||||
|
<span>Generate Password</span>
|
||||||
|
</a>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Features Section */}
|
||||||
|
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-gray-50 dark:bg-gray-800">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-center mb-12"
|
||||||
|
>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
Why PassMaster is Safer
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
|
||||||
|
Built with privacy and security as the foundation, not an afterthought.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
|
{features.map((feature, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={feature.title}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
className="card text-center group cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="flex justify-center mb-4">
|
||||||
|
<div className="p-3 bg-primary-100 dark:bg-primary-900/20 rounded-full group-hover:bg-primary-200 dark:group-hover:bg-primary-900/40 transition-colors duration-200">
|
||||||
|
<feature.icon className="h-8 w-8 text-primary-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||||
|
{feature.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Password Generator Section */}
|
||||||
|
<section id="generator" className="py-16 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-center mb-12"
|
||||||
|
>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
Generate Your Strong Password
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300">
|
||||||
|
Customize your password settings and generate secure passwords instantly.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<PasswordGenerator />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* FAQ Section */}
|
||||||
|
<section id="faq" className="py-16 px-4 sm:px-6 lg:px-8 bg-gray-50 dark:bg-gray-800">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-center mb-12"
|
||||||
|
>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
Frequently Asked Questions
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300">
|
||||||
|
Everything you need to know about PassMaster and password security.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<FAQ />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Floating CTA */}
|
||||||
|
<FloatingCTA />
|
||||||
|
|
||||||
|
{/* PWA Install Prompt */}
|
||||||
|
<PWAInstallPrompt />
|
||||||
|
|
||||||
|
{/* Scroll to Top Button */}
|
||||||
|
{showScrollTop && (
|
||||||
|
<motion.button
|
||||||
|
onClick={scrollToTop}
|
||||||
|
className="fixed bottom-6 right-6 z-50 p-3 bg-primary-600 hover:bg-primary-700 text-white rounded-full shadow-lg transition-colors duration-200"
|
||||||
|
initial={{ opacity: 0, scale: 0 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0 }}
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
aria-label="Scroll to top"
|
||||||
|
>
|
||||||
|
<ArrowUp className="h-5 w-5" />
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,280 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
Shield,
|
||||||
|
Lock,
|
||||||
|
Eye,
|
||||||
|
Server,
|
||||||
|
FileText,
|
||||||
|
CheckCircle,
|
||||||
|
ArrowLeft
|
||||||
|
} from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default function PrivacyPage() {
|
||||||
|
const privacyFeatures = [
|
||||||
|
{
|
||||||
|
icon: Lock,
|
||||||
|
title: "Client-Side Only",
|
||||||
|
description: "All password generation happens locally in your browser. No data is ever sent to our servers."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Eye,
|
||||||
|
title: "No Tracking",
|
||||||
|
description: "We don't use cookies, analytics, or any tracking mechanisms. Your privacy is guaranteed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Server,
|
||||||
|
title: "No Server Storage",
|
||||||
|
description: "We don't store any passwords, user data, or personal information on our servers."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Shield,
|
||||||
|
title: "Open Source",
|
||||||
|
description: "All code is publicly available and auditable. You can verify our privacy claims."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const dataPractices = [
|
||||||
|
{
|
||||||
|
title: "What We Don't Collect",
|
||||||
|
items: [
|
||||||
|
"Passwords or generated content",
|
||||||
|
"Personal information",
|
||||||
|
"IP addresses",
|
||||||
|
"Browser history",
|
||||||
|
"Usage analytics",
|
||||||
|
"Device information"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "What We Don't Store",
|
||||||
|
items: [
|
||||||
|
"User accounts",
|
||||||
|
"Password history",
|
||||||
|
"Settings or preferences",
|
||||||
|
"Session data",
|
||||||
|
"Cookies or local storage",
|
||||||
|
"Any personal data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "What We Don't Share",
|
||||||
|
items: [
|
||||||
|
"No third-party services",
|
||||||
|
"No advertising networks",
|
||||||
|
"No analytics providers",
|
||||||
|
"No data brokers",
|
||||||
|
"No government requests",
|
||||||
|
"No commercial use"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="flex items-center text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-5 w-5 mr-2" />
|
||||||
|
Back to PassMaster
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="text-center mb-16"
|
||||||
|
>
|
||||||
|
<div className="flex justify-center mb-6">
|
||||||
|
<div className="p-4 bg-blue-100 dark:bg-blue-900/20 rounded-full">
|
||||||
|
<Shield className="h-12 w-12 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-6">
|
||||||
|
Privacy Policy
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
|
||||||
|
Your privacy is our top priority. PassMaster is designed with privacy-first principles.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Privacy Features */}
|
||||||
|
<section className="mb-16">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-center mb-12"
|
||||||
|
>
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
Privacy-First Design
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300">
|
||||||
|
Every aspect of PassMaster is built to protect your privacy.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
|
{privacyFeatures.map((feature, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={feature.title}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-start space-x-4">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="p-3 bg-blue-100 dark:bg-blue-900/20 rounded-lg">
|
||||||
|
<feature.icon className="h-6 w-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
{feature.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Data Practices */}
|
||||||
|
<section className="mb-16">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-center mb-12"
|
||||||
|
>
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
Our Data Practices
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300">
|
||||||
|
Transparency about how we handle (or don't handle) your data.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
|
{dataPractices.map((practice, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={practice.title}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||||
|
{practice.title}
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{practice.items.map((item, itemIndex) => (
|
||||||
|
<li key={itemIndex} className="flex items-start space-x-3">
|
||||||
|
<CheckCircle className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span className="text-gray-600 dark:text-gray-300">{item}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Technical Details */}
|
||||||
|
<section className="mb-16">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-sm border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-center mb-6">
|
||||||
|
<FileText className="h-8 w-8 text-blue-600 mr-3" />
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Technical Implementation
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="prose dark:prose-invert max-w-none">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">How PassMaster Works</h3>
|
||||||
|
<ul className="space-y-3 text-gray-600 dark:text-gray-300">
|
||||||
|
<li className="flex items-start space-x-3">
|
||||||
|
<CheckCircle className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span><strong>Local Processing:</strong> All password generation happens in your browser using JavaScript</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start space-x-3">
|
||||||
|
<CheckCircle className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span><strong>No Network Requests:</strong> The app works completely offline after initial load</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start space-x-3">
|
||||||
|
<CheckCircle className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span><strong>Open Source:</strong> All code is publicly available on GitHub for verification</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start space-x-3">
|
||||||
|
<CheckCircle className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span><strong>No Dependencies:</strong> We don't use external services or third-party libraries that could track you</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Contact */}
|
||||||
|
<section className="text-center">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-8"
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
Questions About Privacy?
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||||
|
We're committed to transparency. If you have any questions about our privacy practices,
|
||||||
|
please review our source code or contact us.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<a
|
||||||
|
href="https://github.com/your-repo/passmaster"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
View Source Code
|
||||||
|
</a>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-flex items-center justify-center px-6 py-3 border border-gray-300 dark:border-gray-600 text-base font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
Back to Generator
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { ChevronDown, ChevronUp } from 'lucide-react'
|
||||||
|
|
||||||
|
interface FAQItem {
|
||||||
|
question: string
|
||||||
|
answer: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const faqData: FAQItem[] = [
|
||||||
|
{
|
||||||
|
question: "How does offline password generation work?",
|
||||||
|
answer: "PassMaster generates passwords entirely in your browser using cryptographically secure random number generation. No data is sent to our servers - everything happens locally on your device. This means your passwords are never transmitted over the internet and remain completely private."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Is PassMaster safe to use?",
|
||||||
|
answer: "Yes, PassMaster is completely safe. We use industry-standard cryptographic libraries and generate passwords using the Web Crypto API's secure random number generator. Since all processing happens locally in your browser, there's no risk of your passwords being intercepted or stored on our servers."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Why use symbols and long passwords?",
|
||||||
|
answer: "Longer passwords with a mix of character types (uppercase, lowercase, numbers, symbols) significantly increase the time it would take for attackers to crack them. Each additional character and character type exponentially increases the number of possible combinations, making your passwords much more secure against brute force attacks."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "What is client-side encryption?",
|
||||||
|
answer: "Client-side encryption means that all cryptographic operations happen in your web browser, not on our servers. Your password generation settings, the generated passwords, and any temporary data never leave your device. This ensures maximum privacy and security since we never have access to your passwords."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Can I use PassMaster offline?",
|
||||||
|
answer: "Yes! PassMaster is a Progressive Web App (PWA) that can be installed on your device. Once installed, you can generate passwords even without an internet connection. The app will work completely offline, maintaining all its security features."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "How do I know my passwords are truly random?",
|
||||||
|
answer: "PassMaster uses the Web Crypto API's getRandomValues() function, which provides cryptographically secure random numbers. This is the same technology used by banks and security applications. The randomness is generated by your device's hardware and operating system, ensuring high-quality entropy."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "What does 'exclude similar characters' mean?",
|
||||||
|
answer: "This option excludes characters that look similar and could be confused with each other, such as 0 (zero) and O (letter O), 1 (one) and l (lowercase L), or I (uppercase i) and l (lowercase L). This helps prevent confusion when typing passwords manually."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "How is password strength calculated?",
|
||||||
|
answer: "Password strength is calculated using entropy, which measures the randomness and unpredictability of the password. The calculation considers the character set size and password length. Higher entropy means the password is harder to crack. We also estimate the time it would take for a computer to brute force the password."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export function FAQ() {
|
||||||
|
const [openItems, setOpenItems] = useState<Set<number>>(new Set())
|
||||||
|
|
||||||
|
const toggleItem = (index: number) => {
|
||||||
|
const newOpenItems = new Set(openItems)
|
||||||
|
if (newOpenItems.has(index)) {
|
||||||
|
newOpenItems.delete(index)
|
||||||
|
} else {
|
||||||
|
newOpenItems.add(index)
|
||||||
|
}
|
||||||
|
setOpenItems(newOpenItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{faqData.map((item, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="card"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleItem(index)}
|
||||||
|
className="w-full flex items-center justify-between text-left focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 rounded-lg p-4"
|
||||||
|
aria-expanded={openItems.has(index)}
|
||||||
|
aria-controls={`faq-answer-${index}`}
|
||||||
|
>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white pr-4">
|
||||||
|
{item.question}
|
||||||
|
</h3>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
{openItems.has(index) ? (
|
||||||
|
<ChevronUp className="h-5 w-5 text-gray-500" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-5 w-5 text-gray-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{openItems.has(index) && (
|
||||||
|
<motion.div
|
||||||
|
id={`faq-answer-${index}`}
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="px-4 pb-4">
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
|
||||||
|
{item.answer}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* JSON-LD Schema for FAQ */}
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: JSON.stringify({
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "FAQPage",
|
||||||
|
"mainEntity": faqData.map((item, index) => ({
|
||||||
|
"@type": "Question",
|
||||||
|
"name": item.question,
|
||||||
|
"acceptedAnswer": {
|
||||||
|
"@type": "Answer",
|
||||||
|
"text": item.answer
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { Key } from 'lucide-react'
|
||||||
|
|
||||||
|
export function FloatingCTA() {
|
||||||
|
const [showFloatingCTA, setShowFloatingCTA] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
const scrollY = window.scrollY
|
||||||
|
const windowHeight = window.innerHeight
|
||||||
|
const documentHeight = document.documentElement.scrollHeight
|
||||||
|
|
||||||
|
// Show floating CTA when user has scrolled past the hero section and generator is not in view
|
||||||
|
const shouldShow = scrollY > windowHeight * 0.5 && scrollY < documentHeight - windowHeight * 0.3
|
||||||
|
setShowFloatingCTA(shouldShow)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll)
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const scrollToGenerator = () => {
|
||||||
|
const generatorElement = document.getElementById('generator')
|
||||||
|
if (generatorElement) {
|
||||||
|
generatorElement.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{showFloatingCTA && (
|
||||||
|
<motion.div
|
||||||
|
className="fixed bottom-6 left-6 z-50"
|
||||||
|
initial={{ opacity: 0, x: -100, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, x: -100, scale: 0.8 }}
|
||||||
|
transition={{ duration: 0.3, ease: 'easeOut' }}
|
||||||
|
>
|
||||||
|
<motion.button
|
||||||
|
onClick={scrollToGenerator}
|
||||||
|
className="bg-primary-600 hover:bg-primary-700 text-white px-6 py-3 rounded-full shadow-lg flex items-center space-x-2 transition-colors duration-200"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
aria-label="Generate password"
|
||||||
|
>
|
||||||
|
<Key className="h-5 w-5" />
|
||||||
|
<span className="font-medium">Generate Password</span>
|
||||||
|
</motion.button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { X, Download, Smartphone } from 'lucide-react';
|
||||||
|
|
||||||
|
export function PWAInstallPrompt() {
|
||||||
|
const [showPrompt, setShowPrompt] = useState(false);
|
||||||
|
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if PWA is already installed
|
||||||
|
const isInstalled = window.matchMedia('(display-mode: standalone)').matches;
|
||||||
|
|
||||||
|
if (isInstalled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for beforeinstallprompt event
|
||||||
|
const handleBeforeInstallPrompt = (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDeferredPrompt(e);
|
||||||
|
setShowPrompt(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleInstall = async () => {
|
||||||
|
if (!deferredPrompt) return;
|
||||||
|
|
||||||
|
deferredPrompt.prompt();
|
||||||
|
const { outcome } = await deferredPrompt.userChoice;
|
||||||
|
|
||||||
|
if (outcome === 'accepted') {
|
||||||
|
console.log('User accepted the install prompt');
|
||||||
|
} else {
|
||||||
|
console.log('User dismissed the install prompt');
|
||||||
|
}
|
||||||
|
|
||||||
|
setDeferredPrompt(null);
|
||||||
|
setShowPrompt(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDismiss = () => {
|
||||||
|
setShowPrompt(false);
|
||||||
|
setDeferredPrompt(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!showPrompt) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-4 left-4 right-4 md:left-auto md:right-4 md:w-80 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Download className="h-6 w-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
Install PassMaster
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
Get quick access to secure password generation
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleDismiss}
|
||||||
|
className="flex-shrink-0 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={handleInstall}
|
||||||
|
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium py-2 px-3 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Install App
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDismiss}
|
||||||
|
className="flex-1 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 text-xs font-medium py-2 px-3 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Not Now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 flex items-center text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<Smartphone className="h-3 w-3 mr-1" />
|
||||||
|
Works offline • No ads • Free forever
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,262 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
RefreshCw,
|
||||||
|
Info,
|
||||||
|
Key
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { generatePassword, calculateEntropy, estimateTimeToCrack } from '@/utils/passwordGenerator'
|
||||||
|
|
||||||
|
interface PasswordOptions {
|
||||||
|
length: number
|
||||||
|
includeUppercase: boolean
|
||||||
|
includeLowercase: boolean
|
||||||
|
includeNumbers: boolean
|
||||||
|
includeSymbols: boolean
|
||||||
|
excludeSimilar: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PasswordGenerator() {
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [showPassword, setShowPassword] = useState(true)
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
const [options, setOptions] = useState<PasswordOptions>({
|
||||||
|
length: 16,
|
||||||
|
includeUppercase: true,
|
||||||
|
includeLowercase: true,
|
||||||
|
includeNumbers: true,
|
||||||
|
includeSymbols: true,
|
||||||
|
excludeSimilar: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Load settings from localStorage on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const savedOptions = localStorage.getItem('passmaster-settings')
|
||||||
|
if (savedOptions) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(savedOptions)
|
||||||
|
setOptions(prev => ({ ...prev, ...parsed }))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load saved settings:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Save settings to localStorage when options change
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('passmaster-settings', JSON.stringify(options))
|
||||||
|
}, [options])
|
||||||
|
|
||||||
|
const handleGenerate = () => {
|
||||||
|
const newPassword = generatePassword(options)
|
||||||
|
setPassword(newPassword)
|
||||||
|
setCopied(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
if (password) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(password)
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to copy password:', error)
|
||||||
|
// Fallback for older browsers
|
||||||
|
const textArea = document.createElement('textarea')
|
||||||
|
textArea.value = password
|
||||||
|
document.body.appendChild(textArea)
|
||||||
|
textArea.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(textArea)
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStrengthLevel = (entropy: number) => {
|
||||||
|
if (entropy < 40) return { level: 'Weak', color: 'strength-weak', bg: 'bg-red-500' }
|
||||||
|
if (entropy < 60) return { level: 'OK', color: 'strength-ok', bg: 'bg-yellow-500' }
|
||||||
|
if (entropy < 80) return { level: 'Strong', color: 'strength-strong', bg: 'bg-blue-500' }
|
||||||
|
return { level: 'Excellent', color: 'strength-excellent', bg: 'bg-green-500' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const entropy = password ? calculateEntropy(password) : 0
|
||||||
|
const timeToCrack = password ? estimateTimeToCrack(password) : ''
|
||||||
|
const strength = getStrengthLevel(entropy)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card max-w-2xl mx-auto">
|
||||||
|
{/* Generated Password */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Generated Password
|
||||||
|
</label>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
value={password}
|
||||||
|
readOnly
|
||||||
|
className="input-field font-mono text-lg"
|
||||||
|
placeholder="Click 'Generate Password' to create a secure password"
|
||||||
|
aria-label="Generated password"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition-colors duration-200"
|
||||||
|
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<motion.button
|
||||||
|
onClick={handleCopy}
|
||||||
|
disabled={!password}
|
||||||
|
className="px-4 py-3 bg-primary-600 text-white rounded-md hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2 transition-colors duration-200"
|
||||||
|
whileHover={{ scale: password ? 1.05 : 1 }}
|
||||||
|
whileTap={{ scale: password ? 0.95 : 1 }}
|
||||||
|
aria-label="Copy password"
|
||||||
|
>
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{copied ? (
|
||||||
|
<motion.div
|
||||||
|
key="check"
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
exit={{ scale: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
key="copy"
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
exit={{ scale: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
<span>{copied ? 'Copied!' : 'Copy'}</span>
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password Strength */}
|
||||||
|
{password && (
|
||||||
|
<div className="mt-4 space-y-2">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">Strength:</span>
|
||||||
|
<span className={`font-medium ${strength.color.replace('strength-', 'text-')}`}>
|
||||||
|
{strength.level}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||||
|
<motion.div
|
||||||
|
className={`strength-meter ${strength.color}`}
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
animate={{ width: `${Math.min((entropy / 100) * 100, 100)}%` }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<span>Entropy: {entropy.toFixed(1)} bits</span>
|
||||||
|
<span>Time to crack: {timeToCrack}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Options */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Length Slider */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Password Length: {options.length}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="8"
|
||||||
|
max="128"
|
||||||
|
value={options.length}
|
||||||
|
onChange={(e) => setOptions({ ...options, length: parseInt(e.target.value) })}
|
||||||
|
className="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer slider"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
<span>8</span>
|
||||||
|
<span>128</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Character Options */}
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">Character Types</h3>
|
||||||
|
{[
|
||||||
|
{ key: 'includeUppercase', label: 'Uppercase (A-Z)' },
|
||||||
|
{ key: 'includeLowercase', label: 'Lowercase (a-z)' },
|
||||||
|
{ key: 'includeNumbers', label: 'Numbers (0-9)' },
|
||||||
|
{ key: 'includeSymbols', label: 'Symbols (!@#$%^&*)' },
|
||||||
|
].map(({ key, label }) => (
|
||||||
|
<label key={key} className="flex items-center space-x-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={options[key as keyof PasswordOptions] as boolean}
|
||||||
|
onChange={(e) => setOptions({ ...options, [key]: e.target.checked })}
|
||||||
|
className="rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700 dark:text-gray-300">{label}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">Options</h3>
|
||||||
|
<label className="flex items-start space-x-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={options.excludeSimilar}
|
||||||
|
onChange={(e) => setOptions({ ...options, excludeSimilar: e.target.checked })}
|
||||||
|
className="rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 mt-0.5"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<span className="text-sm text-gray-700 dark:text-gray-300">Exclude Similar Characters</span>
|
||||||
|
<div className="flex items-center space-x-1 mt-1">
|
||||||
|
<Info className="h-3 w-3 text-gray-400" />
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Excludes 0/O, l/I, 1/I to avoid confusion
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Generate Button */}
|
||||||
|
<motion.button
|
||||||
|
onClick={handleGenerate}
|
||||||
|
className="w-full btn-primary mt-6 flex items-center justify-center space-x-2"
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-5 w-5" />
|
||||||
|
<span>Generate Password</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
{/* ARIA Live Region for Copy Feedback */}
|
||||||
|
<div aria-live="polite" className="sr-only">
|
||||||
|
{copied && 'Password copied to clipboard'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Shield, Github, Heart } from 'lucide-react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className="bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
{/* Brand */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Shield className="h-6 w-6 text-primary-600" />
|
||||||
|
<span className="text-lg font-bold text-gray-900 dark:text-white">
|
||||||
|
PassMaster
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 max-w-xs">
|
||||||
|
Generate ultra-secure passwords instantly, offline with client-side encryption.
|
||||||
|
100% open-source, private, and free.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Links */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-white uppercase tracking-wider">
|
||||||
|
Quick Links
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="#generator"
|
||||||
|
className="text-sm text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Password Generator
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="#faq"
|
||||||
|
className="text-sm text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
FAQ
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="/privacy"
|
||||||
|
className="text-sm text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Privacy Policy
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="/offline-test"
|
||||||
|
className="text-sm text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Offline Test
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Open Source */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-white uppercase tracking-wider">
|
||||||
|
Open Source
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<a
|
||||||
|
href="https://github.com/your-username/passmaster"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center space-x-2 text-sm text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
<Github className="h-4 w-4" />
|
||||||
|
<span>View on GitHub</span>
|
||||||
|
</a>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-500">
|
||||||
|
Licensed under MIT
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Bar */}
|
||||||
|
<div className="mt-8 pt-8 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
© {new Date().getFullYear()} PassMaster. All rights reserved.
|
||||||
|
</p>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center space-x-1 text-sm text-gray-600 dark:text-gray-400"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<span>Made with</span>
|
||||||
|
<Heart className="h-4 w-4 text-red-500" />
|
||||||
|
<span>for privacy</span>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { Shield, Sun, Moon, Download, Menu, X } from 'lucide-react'
|
||||||
|
import { useTheme } from 'next-themes'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
const { theme, setTheme } = useTheme()
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
|
const [showInstallPrompt, setShowInstallPrompt] = useState(false)
|
||||||
|
const [deferredPrompt, setDeferredPrompt] = useState<any>(null)
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
|
||||||
|
// Listen for PWA install prompt
|
||||||
|
window.addEventListener('beforeinstallprompt', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setDeferredPrompt(e)
|
||||||
|
setShowInstallPrompt(true)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleInstallClick = async () => {
|
||||||
|
if (deferredPrompt) {
|
||||||
|
deferredPrompt.prompt()
|
||||||
|
const { outcome } = await deferredPrompt.userChoice
|
||||||
|
if (outcome === 'accepted') {
|
||||||
|
setShowInstallPrompt(false)
|
||||||
|
setDeferredPrompt(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setTheme(theme === 'dark' ? 'light' : 'dark')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="sticky top-0 z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex justify-between items-center h-16">
|
||||||
|
{/* Logo */}
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center space-x-2"
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<Link href="/" className="flex items-center space-x-2 hover:opacity-80 transition-opacity">
|
||||||
|
<Shield className="h-8 w-8 text-primary-600" />
|
||||||
|
<span className="text-xl font-bold text-gray-900 dark:text-white">
|
||||||
|
PassMaster
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Desktop Navigation */}
|
||||||
|
<nav className="hidden md:flex items-center space-x-4">
|
||||||
|
<Link
|
||||||
|
href="/privacy"
|
||||||
|
className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200 px-3 py-2 rounded-md text-sm font-medium"
|
||||||
|
>
|
||||||
|
Privacy
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{showInstallPrompt && (
|
||||||
|
<motion.button
|
||||||
|
onClick={handleInstallClick}
|
||||||
|
className="btn-secondary flex items-center space-x-2"
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
<span>Install App</span>
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="p-2 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200"
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
>
|
||||||
|
{theme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Mobile menu button */}
|
||||||
|
<div className="md:hidden">
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
|
className="p-2 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200"
|
||||||
|
aria-label="Toggle mobile menu"
|
||||||
|
>
|
||||||
|
{mobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Navigation */}
|
||||||
|
{mobileMenuOpen && (
|
||||||
|
<motion.div
|
||||||
|
className="md:hidden py-4 border-t border-gray-200 dark:border-gray-700"
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col space-y-3">
|
||||||
|
<Link
|
||||||
|
href="/privacy"
|
||||||
|
className="flex items-center justify-center px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200"
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
>
|
||||||
|
Privacy Policy
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{showInstallPrompt && (
|
||||||
|
<button
|
||||||
|
onClick={handleInstallClick}
|
||||||
|
className="btn-secondary flex items-center justify-center space-x-2"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
<span>Install App</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="flex items-center justify-center space-x-2 p-3 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
{theme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
|
||||||
|
<span>Toggle Theme</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||||
|
import { type ThemeProviderProps } from "next-themes/dist/types"
|
||||||
|
|
||||||
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
interface PasswordOptions {
|
||||||
|
length: number;
|
||||||
|
includeUppercase: boolean;
|
||||||
|
includeLowercase: boolean;
|
||||||
|
includeNumbers: boolean;
|
||||||
|
includeSymbols: boolean;
|
||||||
|
excludeSimilar: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
const LOWERCASE = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
|
const NUMBERS = '0123456789';
|
||||||
|
const SYMBOLS = '!@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||||
|
const SIMILAR = 'il1Lo0O';
|
||||||
|
|
||||||
|
export function generatePassword(options: PasswordOptions): string {
|
||||||
|
let charset = '';
|
||||||
|
|
||||||
|
if (options.includeUppercase) charset += UPPERCASE;
|
||||||
|
if (options.includeLowercase) charset += LOWERCASE;
|
||||||
|
if (options.includeNumbers) charset += NUMBERS;
|
||||||
|
if (options.includeSymbols) charset += SYMBOLS;
|
||||||
|
|
||||||
|
if (options.excludeSimilar) {
|
||||||
|
charset = charset.split('').filter(char => !SIMILAR.includes(char)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (charset.length === 0) {
|
||||||
|
throw new Error('At least one character type must be selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Web Crypto API for cryptographically secure random generation
|
||||||
|
const array = new Uint32Array(options.length);
|
||||||
|
crypto.getRandomValues(array);
|
||||||
|
|
||||||
|
let password = '';
|
||||||
|
for (let i = 0; i < options.length; i++) {
|
||||||
|
password += charset[array[i] % charset.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateEntropy(password: string): number {
|
||||||
|
const hasLowercase = /[a-z]/.test(password);
|
||||||
|
const hasUppercase = /[A-Z]/.test(password);
|
||||||
|
const hasNumbers = /[0-9]/.test(password);
|
||||||
|
const hasSymbols = /[^a-zA-Z0-9]/.test(password);
|
||||||
|
|
||||||
|
let charsetSize = 0;
|
||||||
|
if (hasLowercase) charsetSize += 26;
|
||||||
|
if (hasUppercase) charsetSize += 26;
|
||||||
|
if (hasNumbers) charsetSize += 10;
|
||||||
|
if (hasSymbols) charsetSize += 32; // Approximate
|
||||||
|
|
||||||
|
return Math.log2(Math.pow(charsetSize, password.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function estimateTimeToCrack(password: string): string {
|
||||||
|
const entropy = calculateEntropy(password);
|
||||||
|
const guessesPerSecond = 1e12; // 1 trillion guesses per second (optimistic for attackers)
|
||||||
|
const secondsToCrack = Math.pow(2, entropy - 1) / guessesPerSecond;
|
||||||
|
|
||||||
|
if (secondsToCrack < 60) {
|
||||||
|
return `${Math.round(secondsToCrack)} seconds`;
|
||||||
|
} else if (secondsToCrack < 3600) {
|
||||||
|
return `${Math.round(secondsToCrack / 60)} minutes`;
|
||||||
|
} else if (secondsToCrack < 86400) {
|
||||||
|
return `${Math.round(secondsToCrack / 3600)} hours`;
|
||||||
|
} else if (secondsToCrack < 31536000) {
|
||||||
|
return `${Math.round(secondsToCrack / 86400)} days`;
|
||||||
|
} else if (secondsToCrack < 31536000000) {
|
||||||
|
return `${Math.round(secondsToCrack / 31536000)} years`;
|
||||||
|
} else {
|
||||||
|
return `${(secondsToCrack / 31536000000).toExponential(2)} billion years`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
],
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
input: 'hsl(var(--input))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
card: {
|
||||||
|
DEFAULT: 'hsl(var(--card))',
|
||||||
|
foreground: 'hsl(var(--card-foreground))',
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
|
foreground: 'hsl(var(--popover-foreground))',
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))',
|
||||||
|
50: '#eff6ff',
|
||||||
|
100: '#dbeafe',
|
||||||
|
200: '#bfdbfe',
|
||||||
|
300: '#93c5fd',
|
||||||
|
400: '#60a5fa',
|
||||||
|
500: '#3b82f6',
|
||||||
|
600: '#2563eb',
|
||||||
|
700: '#1d4ed8',
|
||||||
|
800: '#1e40af',
|
||||||
|
900: '#1e3a8a',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground))',
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
|
foreground: 'hsl(var(--muted-foreground))',
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
|
foreground: 'hsl(var(--accent-foreground))',
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground))',
|
||||||
|
},
|
||||||
|
radius: 'var(--radius)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "es6"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||