feat: initialize reactjs project using vite
This commit is contained in:
144
src/pages/About.module.css
Normal file
144
src/pages/About.module.css
Normal file
@@ -0,0 +1,144 @@
|
||||
.about {
|
||||
padding-top: 80px;
|
||||
}
|
||||
|
||||
/* Hero */
|
||||
.hero {
|
||||
padding: var(--space-3xl) 0;
|
||||
text-align: center;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--md-sys-color-surface-container-lowest) 0%,
|
||||
var(--md-sys-color-background) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: clamp(2.5rem, 5vw, 4rem);
|
||||
font-weight: 700;
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.25rem;
|
||||
color: var(--md-sys-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Intro */
|
||||
.intro {
|
||||
padding: var(--space-3xl) 0;
|
||||
}
|
||||
|
||||
.introContent {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.introText {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.8;
|
||||
margin-bottom: var(--space-lg);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Section Header */
|
||||
.sectionHeader {
|
||||
text-align: center;
|
||||
margin-bottom: var(--space-2xl);
|
||||
}
|
||||
|
||||
.sectionHeader h2 {
|
||||
font-size: clamp(1.75rem, 3vw, 2.5rem);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.sectionHeader p {
|
||||
font-size: 1rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
/* Skills */
|
||||
.skills {
|
||||
padding: var(--space-3xl) 0;
|
||||
background-color: var(--md-sys-color-surface-container-lowest);
|
||||
}
|
||||
|
||||
.techGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: var(--space-md);
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.techCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-lg);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
border-radius: var(--radius-lg);
|
||||
transition: border-color var(--transition-fast), background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.techCard:hover {
|
||||
border-color: var(--md-sys-color-primary);
|
||||
background-color: var(--md-sys-color-surface-container-high);
|
||||
}
|
||||
|
||||
.techIcon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.techName {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
/* Values */
|
||||
.values {
|
||||
padding: var(--space-3xl) 0;
|
||||
}
|
||||
|
||||
.valuesGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: var(--space-xl);
|
||||
}
|
||||
|
||||
.valueCard {
|
||||
padding: var(--space-xl);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
border-radius: var(--radius-lg);
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.valueCard:hover {
|
||||
border-color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.valueNumber {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--md-sys-color-primary);
|
||||
opacity: 0.5;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.valueTitle {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.valueDescription {
|
||||
font-size: 0.95rem;
|
||||
opacity: 0.75;
|
||||
line-height: 1.6;
|
||||
}
|
||||
140
src/pages/About.tsx
Normal file
140
src/pages/About.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { motion } from 'motion/react';
|
||||
import { useTranslation } from '../i18n';
|
||||
import styles from './About.module.css';
|
||||
|
||||
const techStack = [
|
||||
{ name: 'React', icon: '⚛️' },
|
||||
{ name: 'TypeScript', icon: '📘' },
|
||||
{ name: 'Node.js', icon: '🟢' },
|
||||
{ name: 'Python', icon: '🐍' },
|
||||
{ name: 'Firebase', icon: '🔥' },
|
||||
{ name: 'PostgreSQL', icon: '🐘' },
|
||||
{ name: 'Docker', icon: '🐳' },
|
||||
{ name: 'Git', icon: '📦' },
|
||||
];
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { staggerChildren: 0.1 },
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.4 },
|
||||
},
|
||||
};
|
||||
|
||||
export function About() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<main className={styles.about}>
|
||||
{/* Hero Section */}
|
||||
<section className={styles.hero}>
|
||||
<div className="container">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h1 className={styles.title}>{t.about.title}</h1>
|
||||
<p className={styles.subtitle}>{t.about.subtitle}</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Intro Section */}
|
||||
<section className={styles.intro}>
|
||||
<div className="container">
|
||||
<motion.div
|
||||
className={styles.introContent}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<p className={styles.introText}>{t.about.intro}</p>
|
||||
<p className={styles.introText}>{t.about.experience}</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Skills Section */}
|
||||
<section className={styles.skills}>
|
||||
<div className="container">
|
||||
<motion.div
|
||||
className={styles.sectionHeader}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h2>{t.about.skills.title}</h2>
|
||||
<p>{t.about.skills.subtitle}</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className={styles.techGrid}
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
{techStack.map((tech) => (
|
||||
<motion.div
|
||||
key={tech.name}
|
||||
className={styles.techCard}
|
||||
variants={itemVariants}
|
||||
whileHover={{ scale: 1.05, y: -4 }}
|
||||
>
|
||||
<span className={styles.techIcon}>{tech.icon}</span>
|
||||
<span className={styles.techName}>{tech.name}</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Values Section */}
|
||||
<section className={styles.values}>
|
||||
<div className="container">
|
||||
<motion.div
|
||||
className={styles.sectionHeader}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h2>{t.about.values.title}</h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className={styles.valuesGrid}
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
{t.about.values.items.map((value, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className={styles.valueCard}
|
||||
variants={itemVariants}
|
||||
>
|
||||
<div className={styles.valueNumber}>0{index + 1}</div>
|
||||
<h3 className={styles.valueTitle}>{value.title}</h3>
|
||||
<p className={styles.valueDescription}>{value.description}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
141
src/pages/Contact.module.css
Normal file
141
src/pages/Contact.module.css
Normal file
@@ -0,0 +1,141 @@
|
||||
.contact {
|
||||
padding-top: 80px;
|
||||
}
|
||||
|
||||
/* Hero */
|
||||
.hero {
|
||||
padding: var(--space-3xl) 0;
|
||||
text-align: center;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--md-sys-color-surface-container-lowest) 0%,
|
||||
var(--md-sys-color-background) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: clamp(2.5rem, 5vw, 4rem);
|
||||
font-weight: 700;
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.25rem;
|
||||
color: var(--md-sys-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.content {
|
||||
padding: var(--space-3xl) 0;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-3xl);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.grid {
|
||||
grid-template-columns: 1.5fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Form Section */
|
||||
.formSection {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.7;
|
||||
margin-bottom: var(--space-2xl);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.success {
|
||||
padding: var(--space-md);
|
||||
background-color: rgba(127, 217, 152, 0.1);
|
||||
border: 1px solid var(--md-sys-color-primary);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--md-sys-color-primary);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: var(--space-md);
|
||||
background-color: rgba(255, 180, 171, 0.1);
|
||||
border: 1px solid var(--md-sys-color-error);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--md-sys-color-error);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* Info Section */
|
||||
.infoSection {
|
||||
padding: var(--space-xl);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
border-radius: var(--radius-lg);
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.infoTitle {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.infoList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xl);
|
||||
}
|
||||
|
||||
.infoItem {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.infoIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.infoIcon svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.infoItem h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
opacity: 0.7;
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.infoItem a {
|
||||
font-size: 1rem;
|
||||
color: var(--md-sys-color-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.infoItem a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
244
src/pages/Contact.tsx
Normal file
244
src/pages/Contact.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
import { useState, type FormEvent } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import emailjs from '@emailjs/browser';
|
||||
import { useTranslation } from '../i18n';
|
||||
import { Button, Input, Textarea } from '../components/ui';
|
||||
import styles from './Contact.module.css';
|
||||
|
||||
// EmailJS configuration - replace these with your actual IDs
|
||||
const EMAILJS_SERVICE_ID = 'YOUR_SERVICE_ID';
|
||||
const EMAILJS_TEMPLATE_ID = 'YOUR_TEMPLATE_ID';
|
||||
const EMAILJS_PUBLIC_KEY = 'YOUR_PUBLIC_KEY';
|
||||
|
||||
interface FormData {
|
||||
name: string;
|
||||
email: string;
|
||||
subject: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface FormErrors {
|
||||
name?: string;
|
||||
email?: string;
|
||||
subject?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export function Contact() {
|
||||
const { t } = useTranslation();
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
name: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
message: '',
|
||||
});
|
||||
const [errors, setErrors] = useState<FormErrors>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: FormErrors = {};
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = 'Required';
|
||||
}
|
||||
|
||||
if (!formData.email.trim()) {
|
||||
newErrors.email = 'Required';
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||
newErrors.email = 'Invalid email';
|
||||
}
|
||||
|
||||
if (!formData.subject.trim()) {
|
||||
newErrors.subject = 'Required';
|
||||
}
|
||||
|
||||
if (!formData.message.trim()) {
|
||||
newErrors.message = 'Required';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
setSubmitStatus('idle');
|
||||
|
||||
try {
|
||||
await emailjs.send(
|
||||
EMAILJS_SERVICE_ID,
|
||||
EMAILJS_TEMPLATE_ID,
|
||||
{
|
||||
from_name: formData.name,
|
||||
from_email: formData.email,
|
||||
subject: formData.subject,
|
||||
message: formData.message,
|
||||
},
|
||||
EMAILJS_PUBLIC_KEY
|
||||
);
|
||||
|
||||
setSubmitStatus('success');
|
||||
setFormData({ name: '', email: '', subject: '', message: '' });
|
||||
} catch {
|
||||
setSubmitStatus('error');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (field: keyof FormData, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
if (errors[field]) {
|
||||
setErrors((prev) => ({ ...prev, [field]: undefined }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className={styles.contact}>
|
||||
{/* Hero */}
|
||||
<section className={styles.hero}>
|
||||
<div className="container">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h1 className={styles.title}>{t.contact.title}</h1>
|
||||
<p className={styles.subtitle}>{t.contact.subtitle}</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Content */}
|
||||
<section className={styles.content}>
|
||||
<div className="container">
|
||||
<div className={styles.grid}>
|
||||
{/* Form */}
|
||||
<motion.div
|
||||
className={styles.formSection}
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
>
|
||||
<p className={styles.intro}>{t.contact.intro}</p>
|
||||
|
||||
<form onSubmit={handleSubmit} className={styles.form}>
|
||||
<Input
|
||||
label={t.contact.form.name}
|
||||
placeholder={t.contact.form.namePlaceholder}
|
||||
value={formData.name}
|
||||
onChange={(e) => handleChange('name', e.target.value)}
|
||||
error={errors.name}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label={t.contact.form.email}
|
||||
type="email"
|
||||
placeholder={t.contact.form.emailPlaceholder}
|
||||
value={formData.email}
|
||||
onChange={(e) => handleChange('email', e.target.value)}
|
||||
error={errors.email}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label={t.contact.form.subject}
|
||||
placeholder={t.contact.form.subjectPlaceholder}
|
||||
value={formData.subject}
|
||||
onChange={(e) => handleChange('subject', e.target.value)}
|
||||
error={errors.subject}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label={t.contact.form.message}
|
||||
placeholder={t.contact.form.messagePlaceholder}
|
||||
value={formData.message}
|
||||
onChange={(e) => handleChange('message', e.target.value)}
|
||||
error={errors.message}
|
||||
rows={6}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
isLoading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? t.contact.form.sending : t.contact.form.submit}
|
||||
</Button>
|
||||
|
||||
{submitStatus === 'success' && (
|
||||
<motion.p
|
||||
className={styles.success}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
{t.contact.form.success}
|
||||
</motion.p>
|
||||
)}
|
||||
|
||||
{submitStatus === 'error' && (
|
||||
<motion.p
|
||||
className={styles.error}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
{t.contact.form.error}
|
||||
</motion.p>
|
||||
)}
|
||||
</form>
|
||||
</motion.div>
|
||||
|
||||
{/* Info */}
|
||||
<motion.div
|
||||
className={styles.infoSection}
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
>
|
||||
<h2 className={styles.infoTitle}>{t.contact.info.title}</h2>
|
||||
|
||||
<div className={styles.infoList}>
|
||||
<div className={styles.infoItem}>
|
||||
<div className={styles.infoIcon}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
|
||||
<polyline points="22,6 12,13 2,6" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{t.contact.info.email}</h3>
|
||||
<a href="mailto:info@ragusa-it.dev">info@ragusa-it.dev</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.infoItem}>
|
||||
<div className={styles.infoIcon}>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{t.contact.info.github}</h3>
|
||||
<a
|
||||
href="https://github.com/ragusa-it"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
github.com/ragusa-it
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
10
src/pages/Home.tsx
Normal file
10
src/pages/Home.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Hero, Services } from '../components/sections';
|
||||
|
||||
export function Home() {
|
||||
return (
|
||||
<main>
|
||||
<Hero />
|
||||
<Services />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
3
src/pages/index.ts
Normal file
3
src/pages/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { Home } from './Home';
|
||||
export { About } from './About';
|
||||
export { Contact } from './Contact';
|
||||
Reference in New Issue
Block a user