portfolify 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/generate.d.ts +7 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +129 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/portfolio.d.ts +12 -0
- package/dist/commands/portfolio.d.ts.map +1 -0
- package/dist/commands/portfolio.js +292 -0
- package/dist/commands/portfolio.js.map +1 -0
- package/dist/generator/index.d.ts +3 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +572 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/portfolio-generator.d.ts +4 -0
- package/dist/generator/portfolio-generator.d.ts.map +1 -0
- package/dist/generator/portfolio-generator.js +1577 -0
- package/dist/generator/portfolio-generator.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/index.d.ts +81 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +330 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/portfolio-prompts.d.ts +60 -0
- package/dist/prompts/portfolio-prompts.d.ts.map +1 -0
- package/dist/prompts/portfolio-prompts.js +635 -0
- package/dist/prompts/portfolio-prompts.js.map +1 -0
- package/dist/themes/index.d.ts +15 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +64 -0
- package/dist/themes/index.js.map +1 -0
- package/dist/utils/file.d.ts +5 -0
- package/dist/utils/file.d.ts.map +1 -0
- package/dist/utils/file.js +33 -0
- package/dist/utils/file.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +22 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/validator.d.ts +27 -0
- package/dist/utils/validator.d.ts.map +1 -0
- package/dist/utils/validator.js +322 -0
- package/dist/utils/validator.js.map +1 -0
- package/package.json +66 -0
- package/templates/index.html +16 -0
- package/templates/package.json +37 -0
- package/templates/postcss.config.js +6 -0
- package/templates/src/App.tsx +56 -0
- package/templates/src/components/Blog.tsx +119 -0
- package/templates/src/components/Footer.tsx +206 -0
- package/templates/src/components/Hero.tsx +182 -0
- package/templates/src/components/Projects.tsx +130 -0
- package/templates/src/components/SEO.tsx +38 -0
- package/templates/src/components/Skills.tsx +107 -0
- package/templates/src/components/ThemeToggle.tsx +39 -0
- package/templates/src/config/portfolio.json +61 -0
- package/templates/src/content/blog/welcome.mdx +29 -0
- package/templates/src/lib/blog.ts +63 -0
- package/templates/src/lib/utils.ts +6 -0
- package/templates/src/main.tsx +13 -0
- package/templates/src/styles/globals.css +123 -0
- package/templates/tailwind.config.js +65 -0
- package/templates/tsconfig.json +37 -0
- package/templates/tsconfig.node.json +12 -0
- package/templates/vite.config.ts +24 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { motion } from 'framer-motion';
|
|
2
|
+
import { ExternalLink, Github } from 'lucide-react';
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
|
|
5
|
+
interface Project {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
tech: string[];
|
|
9
|
+
github?: string;
|
|
10
|
+
demo?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ProjectsProps {
|
|
14
|
+
projects: Project[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function Projects({ projects }: ProjectsProps) {
|
|
18
|
+
const containerVariants = {
|
|
19
|
+
hidden: { opacity: 0 },
|
|
20
|
+
visible: {
|
|
21
|
+
opacity: 1,
|
|
22
|
+
transition: {
|
|
23
|
+
staggerChildren: 0.2,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const cardVariants = {
|
|
29
|
+
hidden: { opacity: 0, y: 50 },
|
|
30
|
+
visible: {
|
|
31
|
+
opacity: 1,
|
|
32
|
+
y: 0,
|
|
33
|
+
transition: { duration: 0.6, ease: 'easeOut' },
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<section id="projects" className="py-20 px-4">
|
|
39
|
+
<div className="max-w-6xl mx-auto">
|
|
40
|
+
<motion.div
|
|
41
|
+
initial={{ opacity: 0, y: 20 }}
|
|
42
|
+
whileInView={{ opacity: 1, y: 0 }}
|
|
43
|
+
viewport={{ once: true }}
|
|
44
|
+
transition={{ duration: 0.6 }}
|
|
45
|
+
className="text-center mb-16"
|
|
46
|
+
>
|
|
47
|
+
<h2 className="text-4xl md:text-5xl font-bold mb-4">
|
|
48
|
+
Featured <span className="gradient-text">Projects</span>
|
|
49
|
+
</h2>
|
|
50
|
+
<p className="text-muted-foreground text-lg">
|
|
51
|
+
Some of my recent work
|
|
52
|
+
</p>
|
|
53
|
+
</motion.div>
|
|
54
|
+
|
|
55
|
+
<motion.div
|
|
56
|
+
variants={containerVariants}
|
|
57
|
+
initial="hidden"
|
|
58
|
+
whileInView="visible"
|
|
59
|
+
viewport={{ once: true }}
|
|
60
|
+
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
|
61
|
+
>
|
|
62
|
+
{projects.map((project, index) => (
|
|
63
|
+
<motion.div
|
|
64
|
+
key={index}
|
|
65
|
+
variants={cardVariants}
|
|
66
|
+
whileHover={{ scale: 1.05, y: -10 }}
|
|
67
|
+
className={cn(
|
|
68
|
+
'glass rounded-xl p-6 hover:glow-effect transition-all duration-300',
|
|
69
|
+
'border border-border/50 hover:border-primary/50'
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
<h3 className="text-2xl font-bold mb-3 gradient-text">
|
|
73
|
+
{project.name}
|
|
74
|
+
</h3>
|
|
75
|
+
|
|
76
|
+
<p className="text-muted-foreground mb-4 line-clamp-3">
|
|
77
|
+
{project.description}
|
|
78
|
+
</p>
|
|
79
|
+
|
|
80
|
+
<div className="flex flex-wrap gap-2 mb-6">
|
|
81
|
+
{project.tech.map((tech, i) => (
|
|
82
|
+
<span
|
|
83
|
+
key={i}
|
|
84
|
+
className="px-3 py-1 text-xs font-medium rounded-full bg-primary/10 text-primary border border-primary/20"
|
|
85
|
+
>
|
|
86
|
+
{tech}
|
|
87
|
+
</span>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="flex gap-3">
|
|
92
|
+
{project.github && (
|
|
93
|
+
<a
|
|
94
|
+
href={project.github}
|
|
95
|
+
target="_blank"
|
|
96
|
+
rel="noopener noreferrer"
|
|
97
|
+
className={cn(
|
|
98
|
+
'flex items-center gap-2 px-4 py-2 rounded-lg',
|
|
99
|
+
'bg-muted hover:bg-muted/80 transition-colors',
|
|
100
|
+
'text-sm font-medium'
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
<Github className="w-4 h-4" />
|
|
104
|
+
Code
|
|
105
|
+
</a>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{project.demo && (
|
|
109
|
+
<a
|
|
110
|
+
href={project.demo}
|
|
111
|
+
target="_blank"
|
|
112
|
+
rel="noopener noreferrer"
|
|
113
|
+
className={cn(
|
|
114
|
+
'flex items-center gap-2 px-4 py-2 rounded-lg',
|
|
115
|
+
'bg-gradient-to-r from-primary to-secondary',
|
|
116
|
+
'text-sm font-medium hover:opacity-90 transition-opacity'
|
|
117
|
+
)}
|
|
118
|
+
>
|
|
119
|
+
<ExternalLink className="w-4 h-4" />
|
|
120
|
+
Demo
|
|
121
|
+
</a>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
</motion.div>
|
|
125
|
+
))}
|
|
126
|
+
</motion.div>
|
|
127
|
+
</div>
|
|
128
|
+
</section>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Helmet } from 'react-helmet-async';
|
|
2
|
+
|
|
3
|
+
interface SEOProps {
|
|
4
|
+
title: string;
|
|
5
|
+
description: string;
|
|
6
|
+
name: string;
|
|
7
|
+
type?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function SEO({ title, description, name, type = 'website' }: SEOProps) {
|
|
11
|
+
return (
|
|
12
|
+
<Helmet>
|
|
13
|
+
{/* Standard metadata tags */}
|
|
14
|
+
<title>{title}</title>
|
|
15
|
+
<meta name="description" content={description} />
|
|
16
|
+
|
|
17
|
+
{/* Open Graph tags */}
|
|
18
|
+
<meta property="og:type" content={type} />
|
|
19
|
+
<meta property="og:title" content={title} />
|
|
20
|
+
<meta property="og:description" content={description} />
|
|
21
|
+
|
|
22
|
+
{/* Twitter Card tags */}
|
|
23
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
24
|
+
<meta name="twitter:title" content={title} />
|
|
25
|
+
<meta name="twitter:description" content={description} />
|
|
26
|
+
|
|
27
|
+
{/* JSON-LD structured data */}
|
|
28
|
+
<script type="application/ld+json">
|
|
29
|
+
{JSON.stringify({
|
|
30
|
+
'@context': 'https://schema.org',
|
|
31
|
+
'@type': 'Person',
|
|
32
|
+
name: name,
|
|
33
|
+
description: description,
|
|
34
|
+
})}
|
|
35
|
+
</script>
|
|
36
|
+
</Helmet>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { motion } from 'framer-motion';
|
|
2
|
+
import {
|
|
3
|
+
Code2,
|
|
4
|
+
Database,
|
|
5
|
+
Palette,
|
|
6
|
+
Terminal,
|
|
7
|
+
Wrench,
|
|
8
|
+
Zap,
|
|
9
|
+
} from 'lucide-react';
|
|
10
|
+
import { cn } from '@/lib/utils';
|
|
11
|
+
|
|
12
|
+
interface SkillsProps {
|
|
13
|
+
skills: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Icon mapping for common skills
|
|
17
|
+
const getSkillIcon = (skill: string) => {
|
|
18
|
+
const lowerSkill = skill.toLowerCase();
|
|
19
|
+
|
|
20
|
+
if (lowerSkill.includes('react') || lowerSkill.includes('vue') || lowerSkill.includes('angular')) {
|
|
21
|
+
return Code2;
|
|
22
|
+
}
|
|
23
|
+
if (lowerSkill.includes('node') || lowerSkill.includes('express') || lowerSkill.includes('api')) {
|
|
24
|
+
return Terminal;
|
|
25
|
+
}
|
|
26
|
+
if (lowerSkill.includes('database') || lowerSkill.includes('sql') || lowerSkill.includes('mongo')) {
|
|
27
|
+
return Database;
|
|
28
|
+
}
|
|
29
|
+
if (lowerSkill.includes('css') || lowerSkill.includes('tailwind') || lowerSkill.includes('design')) {
|
|
30
|
+
return Palette;
|
|
31
|
+
}
|
|
32
|
+
if (lowerSkill.includes('typescript') || lowerSkill.includes('javascript')) {
|
|
33
|
+
return Zap;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return Wrench;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default function Skills({ skills }: SkillsProps) {
|
|
40
|
+
const containerVariants = {
|
|
41
|
+
hidden: { opacity: 0 },
|
|
42
|
+
visible: {
|
|
43
|
+
opacity: 1,
|
|
44
|
+
transition: {
|
|
45
|
+
staggerChildren: 0.1,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const itemVariants = {
|
|
51
|
+
hidden: { opacity: 0, scale: 0.8 },
|
|
52
|
+
visible: {
|
|
53
|
+
opacity: 1,
|
|
54
|
+
scale: 1,
|
|
55
|
+
transition: { duration: 0.4, ease: 'easeOut' },
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<section className="py-20 px-4 bg-muted/30">
|
|
61
|
+
<div className="max-w-6xl mx-auto">
|
|
62
|
+
<motion.div
|
|
63
|
+
initial={{ opacity: 0, y: 20 }}
|
|
64
|
+
whileInView={{ opacity: 1, y: 0 }}
|
|
65
|
+
viewport={{ once: true }}
|
|
66
|
+
transition={{ duration: 0.6 }}
|
|
67
|
+
className="text-center mb-16"
|
|
68
|
+
>
|
|
69
|
+
<h2 className="text-4xl md:text-5xl font-bold mb-4">
|
|
70
|
+
Skills & <span className="gradient-text">Technologies</span>
|
|
71
|
+
</h2>
|
|
72
|
+
<p className="text-muted-foreground text-lg">
|
|
73
|
+
Tools and technologies I work with
|
|
74
|
+
</p>
|
|
75
|
+
</motion.div>
|
|
76
|
+
|
|
77
|
+
<motion.div
|
|
78
|
+
variants={containerVariants}
|
|
79
|
+
initial="hidden"
|
|
80
|
+
whileInView="visible"
|
|
81
|
+
viewport={{ once: true }}
|
|
82
|
+
className="flex flex-wrap justify-center gap-4"
|
|
83
|
+
>
|
|
84
|
+
{skills.map((skill, index) => {
|
|
85
|
+
const Icon = getSkillIcon(skill);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<motion.div
|
|
89
|
+
key={index}
|
|
90
|
+
variants={itemVariants}
|
|
91
|
+
whileHover={{ scale: 1.1, rotate: 5 }}
|
|
92
|
+
className={cn(
|
|
93
|
+
'glass rounded-xl px-6 py-4 flex items-center gap-3',
|
|
94
|
+
'hover:glow-effect transition-all duration-300',
|
|
95
|
+
'border border-border/50 hover:border-primary/50'
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
<Icon className="w-5 h-5 text-primary" />
|
|
99
|
+
<span className="font-medium text-foreground">{skill}</span>
|
|
100
|
+
</motion.div>
|
|
101
|
+
);
|
|
102
|
+
})}
|
|
103
|
+
</motion.div>
|
|
104
|
+
</div>
|
|
105
|
+
</section>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { motion } from 'framer-motion';
|
|
2
|
+
import { Sun, Moon } from 'lucide-react';
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
|
|
5
|
+
interface ThemeToggleProps {
|
|
6
|
+
isDarkMode: boolean;
|
|
7
|
+
toggleTheme: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function ThemeToggle({ isDarkMode, toggleTheme }: ThemeToggleProps) {
|
|
11
|
+
return (
|
|
12
|
+
<motion.button
|
|
13
|
+
initial={{ opacity: 0, y: -20 }}
|
|
14
|
+
animate={{ opacity: 1, y: 0 }}
|
|
15
|
+
transition={{ delay: 0.5, duration: 0.3 }}
|
|
16
|
+
onClick={toggleTheme}
|
|
17
|
+
className={cn(
|
|
18
|
+
'fixed top-6 right-6 z-50',
|
|
19
|
+
'p-3 rounded-full glass',
|
|
20
|
+
'hover:glow-effect transition-all duration-300',
|
|
21
|
+
'hover:scale-110 active:scale-95',
|
|
22
|
+
'border border-border/50 hover:border-primary/50'
|
|
23
|
+
)}
|
|
24
|
+
aria-label={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
25
|
+
>
|
|
26
|
+
<motion.div
|
|
27
|
+
initial={false}
|
|
28
|
+
animate={{ rotate: isDarkMode ? 0 : 180 }}
|
|
29
|
+
transition={{ duration: 0.3 }}
|
|
30
|
+
>
|
|
31
|
+
{isDarkMode ? (
|
|
32
|
+
<Sun className="w-5 h-5 text-yellow-400" />
|
|
33
|
+
) : (
|
|
34
|
+
<Moon className="w-5 h-5 text-primary" />
|
|
35
|
+
)}
|
|
36
|
+
</motion.div>
|
|
37
|
+
</motion.button>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "John Doe",
|
|
3
|
+
"role": "Full Stack Developer",
|
|
4
|
+
"bio": "Passionate developer building amazing web experiences with modern technologies.",
|
|
5
|
+
"skills": [
|
|
6
|
+
"React",
|
|
7
|
+
"TypeScript",
|
|
8
|
+
"Node.js",
|
|
9
|
+
"Tailwind CSS"
|
|
10
|
+
],
|
|
11
|
+
"projects": [
|
|
12
|
+
{
|
|
13
|
+
"name": "Example Project",
|
|
14
|
+
"description": "A cool project built with React and TypeScript",
|
|
15
|
+
"tech": [
|
|
16
|
+
"React",
|
|
17
|
+
"TypeScript",
|
|
18
|
+
"Vite"
|
|
19
|
+
],
|
|
20
|
+
"github": "https://github.com/username/project",
|
|
21
|
+
"demo": "https://example.com"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"enableBlog": true,
|
|
25
|
+
"enableDarkMode": true,
|
|
26
|
+
"defaultDarkMode": true,
|
|
27
|
+
"social": {
|
|
28
|
+
"github": "username",
|
|
29
|
+
"linkedin": "username",
|
|
30
|
+
"twitter": "username",
|
|
31
|
+
"email": "hello@example.com",
|
|
32
|
+
"instagram": "",
|
|
33
|
+
"behance": "",
|
|
34
|
+
"dribbble": "",
|
|
35
|
+
"youtube": "",
|
|
36
|
+
"tiktok": "",
|
|
37
|
+
"phone": ""
|
|
38
|
+
},
|
|
39
|
+
"theme": "modern-dark",
|
|
40
|
+
"themeConfig": {
|
|
41
|
+
"name": "Modern Developer",
|
|
42
|
+
"value": "modern-dark",
|
|
43
|
+
"category": "Developer",
|
|
44
|
+
"colors": {
|
|
45
|
+
"primary": "220 90% 56%",
|
|
46
|
+
"secondary": "280 80% 60%",
|
|
47
|
+
"accent": "340 82% 52%",
|
|
48
|
+
"background": "224 71% 4%",
|
|
49
|
+
"foreground": "213 31% 91%",
|
|
50
|
+
"muted": "223 47% 11%",
|
|
51
|
+
"backgroundLight": "0 0% 100%",
|
|
52
|
+
"foregroundLight": "224 71% 4%",
|
|
53
|
+
"mutedLight": "220 14% 96%"
|
|
54
|
+
},
|
|
55
|
+
"defaultRole": "Full Stack Developer",
|
|
56
|
+
"defaultSkills": ["React", "Node.js", "TypeScript", "Python", "AWS", "Docker"],
|
|
57
|
+
"emoji": "💻"
|
|
58
|
+
},
|
|
59
|
+
"siteUrl": "https://example.com",
|
|
60
|
+
"portfolioType": "Developer"
|
|
61
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Welcome to My Blog"
|
|
3
|
+
date: "2026-01-23"
|
|
4
|
+
excerpt: "This is my first blog post using MDX. Learn how to customize and add more posts."
|
|
5
|
+
tags: ["welcome", "intro", "mdx"]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Welcome to My Blog!
|
|
9
|
+
|
|
10
|
+
This is a sample blog post written in **MDX**. You can use all the power of Markdown with React components.
|
|
11
|
+
|
|
12
|
+
## Getting Started
|
|
13
|
+
|
|
14
|
+
To add a new blog post:
|
|
15
|
+
|
|
16
|
+
1. Create a new `.mdx` file in `src/content/blog/`
|
|
17
|
+
2. Add frontmatter with title, date, excerpt, and tags
|
|
18
|
+
3. Write your content using Markdown and JSX
|
|
19
|
+
|
|
20
|
+
## Code Example
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
const greeting = "Hello, World!";
|
|
24
|
+
console.log(greeting);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## What's Next?
|
|
28
|
+
|
|
29
|
+
Start customizing your portfolio and adding more content. Happy blogging! 🚀
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface BlogPost {
|
|
4
|
+
slug: string;
|
|
5
|
+
title: string;
|
|
6
|
+
date: string;
|
|
7
|
+
excerpt: string;
|
|
8
|
+
tags: string[];
|
|
9
|
+
content?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Sample blog posts - in production, these would be loaded from MDX files
|
|
13
|
+
export const blogPosts: BlogPost[] = [
|
|
14
|
+
{
|
|
15
|
+
slug: 'welcome',
|
|
16
|
+
title: 'Welcome to My Blog',
|
|
17
|
+
date: new Date().toISOString().split('T')[0],
|
|
18
|
+
excerpt: 'This is my first blog post using MDX. Learn how to customize and add more posts.',
|
|
19
|
+
tags: ['welcome', 'intro', 'mdx']
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
slug: 'getting-started-react',
|
|
23
|
+
title: 'Getting Started with React',
|
|
24
|
+
date: new Date(Date.now() - 86400000).toISOString().split('T')[0],
|
|
25
|
+
excerpt: 'Learn the fundamentals of React and build your first component.',
|
|
26
|
+
tags: ['react', 'javascript', 'tutorial']
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
slug: 'typescript-tips',
|
|
30
|
+
title: 'TypeScript Tips and Tricks',
|
|
31
|
+
date: new Date(Date.now() - 172800000).toISOString().split('T')[0],
|
|
32
|
+
excerpt: 'Improve your TypeScript skills with these practical tips.',
|
|
33
|
+
tags: ['typescript', 'tips', 'best-practices']
|
|
34
|
+
}
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export function useBlogPosts() {
|
|
38
|
+
const [posts, setPosts] = useState<BlogPost[]>([]);
|
|
39
|
+
const [loading, setLoading] = useState(true);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
// Simulate loading blog posts
|
|
43
|
+
setPosts(blogPosts);
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
return { posts, loading };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function formatDate(dateString: string): string {
|
|
51
|
+
return new Date(dateString).toLocaleDateString('en-US', {
|
|
52
|
+
year: 'numeric',
|
|
53
|
+
month: 'long',
|
|
54
|
+
day: 'numeric'
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getReadingTime(content: string): string {
|
|
59
|
+
const wordsPerMinute = 200;
|
|
60
|
+
const words = content.split(/\s+/).length;
|
|
61
|
+
const minutes = Math.ceil(words / wordsPerMinute);
|
|
62
|
+
return `${minutes} min read`;
|
|
63
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom/client';
|
|
3
|
+
import { HelmetProvider } from 'react-helmet-async';
|
|
4
|
+
import App from './App';
|
|
5
|
+
import './styles/globals.css';
|
|
6
|
+
|
|
7
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
8
|
+
<React.StrictMode>
|
|
9
|
+
<HelmetProvider>
|
|
10
|
+
<App />
|
|
11
|
+
</HelmetProvider>
|
|
12
|
+
</React.StrictMode>,
|
|
13
|
+
);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
* {
|
|
7
|
+
@apply border-border;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
@apply bg-background text-foreground;
|
|
12
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
13
|
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
|
14
|
+
-webkit-font-smoothing: antialiased;
|
|
15
|
+
-moz-osx-font-smoothing: grayscale;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Default (dark mode) */
|
|
19
|
+
:root {
|
|
20
|
+
--background: 224 71% 4%;
|
|
21
|
+
--foreground: 213 31% 91%;
|
|
22
|
+
--muted: 223 47% 11%;
|
|
23
|
+
--muted-foreground: 215.4 16.3% 56.9%;
|
|
24
|
+
--primary: 220 90% 56%;
|
|
25
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
26
|
+
--secondary: 280 80% 60%;
|
|
27
|
+
--secondary-foreground: 210 40% 98%;
|
|
28
|
+
--accent: 340 82% 52%;
|
|
29
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
30
|
+
--border: 217.2 32.6% 17.5%;
|
|
31
|
+
--input: 217.2 32.6% 17.5%;
|
|
32
|
+
--ring: 224.3 76.3% 48%;
|
|
33
|
+
--radius: 0.5rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Dark mode */
|
|
37
|
+
.dark {
|
|
38
|
+
--background: 224 71% 4%;
|
|
39
|
+
--foreground: 213 31% 91%;
|
|
40
|
+
--muted: 223 47% 11%;
|
|
41
|
+
--muted-foreground: 215.4 16.3% 56.9%;
|
|
42
|
+
--border: 217.2 32.6% 17.5%;
|
|
43
|
+
--input: 217.2 32.6% 17.5%;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Light mode */
|
|
47
|
+
.light {
|
|
48
|
+
--background: 0 0% 100%;
|
|
49
|
+
--foreground: 224 71% 4%;
|
|
50
|
+
--muted: 220 14% 96%;
|
|
51
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
52
|
+
--border: 220 13% 91%;
|
|
53
|
+
--input: 220 13% 91%;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@layer utilities {
|
|
58
|
+
.glass {
|
|
59
|
+
background: rgba(255, 255, 255, 0.05);
|
|
60
|
+
-webkit-backdrop-filter: blur(10px);
|
|
61
|
+
backdrop-filter: blur(10px);
|
|
62
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.light .glass {
|
|
66
|
+
background: rgba(0, 0, 0, 0.03);
|
|
67
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.gradient-text {
|
|
71
|
+
@apply bg-clip-text text-transparent bg-gradient-to-r from-primary via-secondary to-accent;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.glow-effect {
|
|
75
|
+
box-shadow: 0 0 20px rgba(var(--primary), 0.3);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.animate-float {
|
|
79
|
+
animation: float 6s ease-in-out infinite;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.animate-glow {
|
|
83
|
+
animation: glow 2s ease-in-out infinite alternate;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@keyframes float {
|
|
88
|
+
0%, 100% { transform: translateY(0); }
|
|
89
|
+
50% { transform: translateY(-20px); }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@keyframes glow {
|
|
93
|
+
from { filter: brightness(1); }
|
|
94
|
+
to { filter: brightness(1.2); }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Smooth scrolling */
|
|
98
|
+
html {
|
|
99
|
+
scroll-behavior: smooth;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Custom scrollbar */
|
|
103
|
+
::-webkit-scrollbar {
|
|
104
|
+
width: 10px;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
::-webkit-scrollbar-track {
|
|
108
|
+
background: hsl(var(--muted));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
::-webkit-scrollbar-thumb {
|
|
112
|
+
background: hsl(var(--primary));
|
|
113
|
+
border-radius: 5px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
::-webkit-scrollbar-thumb:hover {
|
|
117
|
+
background: hsl(var(--secondary));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Theme transition */
|
|
121
|
+
body {
|
|
122
|
+
transition: background-color 0.3s ease, color 0.3s ease;
|
|
123
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
darkMode: ['class'],
|
|
4
|
+
content: [
|
|
5
|
+
'./index.html',
|
|
6
|
+
'./src/**/*.{js,ts,jsx,tsx}',
|
|
7
|
+
],
|
|
8
|
+
theme: {
|
|
9
|
+
extend: {
|
|
10
|
+
colors: {
|
|
11
|
+
border: 'hsl(var(--border))',
|
|
12
|
+
input: 'hsl(var(--input))',
|
|
13
|
+
ring: 'hsl(var(--ring))',
|
|
14
|
+
background: 'hsl(var(--background))',
|
|
15
|
+
foreground: 'hsl(var(--foreground))',
|
|
16
|
+
primary: {
|
|
17
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
18
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
19
|
+
},
|
|
20
|
+
secondary: {
|
|
21
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
22
|
+
foreground: 'hsl(var(--secondary-foreground))',
|
|
23
|
+
},
|
|
24
|
+
accent: {
|
|
25
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
26
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
27
|
+
},
|
|
28
|
+
muted: {
|
|
29
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
30
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
borderRadius: {
|
|
34
|
+
lg: 'var(--radius)',
|
|
35
|
+
md: 'calc(var(--radius) - 2px)',
|
|
36
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
37
|
+
},
|
|
38
|
+
keyframes: {
|
|
39
|
+
'fade-in': {
|
|
40
|
+
'0%': { opacity: '0', transform: 'translateY(10px)' },
|
|
41
|
+
'100%': { opacity: '1', transform: 'translateY(0)' },
|
|
42
|
+
},
|
|
43
|
+
'slide-in': {
|
|
44
|
+
'0%': { transform: 'translateX(-100%)' },
|
|
45
|
+
'100%': { transform: 'translateX(0)' },
|
|
46
|
+
},
|
|
47
|
+
'glow': {
|
|
48
|
+
'0%, 100%': { boxShadow: '0 0 20px rgba(var(--primary), 0.5)' },
|
|
49
|
+
'50%': { boxShadow: '0 0 40px rgba(var(--primary), 0.8)' },
|
|
50
|
+
},
|
|
51
|
+
'float': {
|
|
52
|
+
'0%, 100%': { transform: 'translateY(0px)' },
|
|
53
|
+
'50%': { transform: 'translateY(-20px)' },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
animation: {
|
|
57
|
+
'fade-in': 'fade-in 0.5s ease-out',
|
|
58
|
+
'slide-in': 'slide-in 0.5s ease-out',
|
|
59
|
+
'glow': 'glow 2s ease-in-out infinite',
|
|
60
|
+
'float': 'float 3s ease-in-out infinite',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
plugins: [],
|
|
65
|
+
}
|