Compare commits

...

51 Commits

Author SHA1 Message Date
seccodesmith 2cfdd61cc2 Update README.md 2025-05-27 15:18:53 +00:00
seccodesmith d9bdf12328 Merge pull request 'final-touches' (#10) from final-touches into main
Reviewed-on: #10
2025-05-27 14:38:29 +00:00
seccodesmith d00712c287 Merge branch 'main' into final-touches 2025-05-27 14:38:24 +00:00
seccodesmith 59dd998a5d feat: update project image paths and enhance project details in ProjectProps.json 2025-05-27 16:37:23 +02:00
seccodesmith 8d440c1993 feat: Add JSON data files for About, Blog Posts, Contact, Projects, Skills, and Social Links
- Created AboutProps.json with core values, technical arsenal, professional journey, and testimonials.
- Added BlogPosts.json for blog post data including title, content, and metadata.
- Introduced Contact.json for contact information and FAQs.
- Created ProjectProps.json as a template for project details.
- Added SkillCardProps.json for skill categories and skills.
- Introduced SocialLink.json for social media links.
- Updated components to fetch data from newly created JSON files instead of hardcoded values.
- Adjusted TypeScript configuration to support JSON module imports.
- Enhanced Vite configuration for public directory and build targets.
2025-05-27 16:14:15 +02:00
seccodesmith b855cc9127 feat: refactor data structures and interfaces for better organization 2025-05-27 14:10:27 +02:00
seccodesmith fe463c4fcb Merge pull request 'fix-contact' (#9) from fix-contact into main
Reviewed-on: #9
2025-05-27 11:53:44 +00:00
seccodesmith 29218b0e38 feat: add Accordion styling and integrate into Contact page 2025-05-27 13:52:14 +02:00
seccodesmith cd684dbaf7 feat: implement styling for Contact page and form components 2025-05-27 13:41:26 +02:00
seccodesmith 6b9522153f feat: update Contact page to use dynamic email and social links 2025-05-27 12:30:22 +02:00
seccodesmith 805bd07eb4 feat: add contact data and styling for the Contact page 2025-05-26 23:45:39 +02:00
seccodesmith e08c2d4013 Fix list in project modal 2025-05-26 22:42:05 +02:00
seccodesmith 56e8c2c252 Merge pull request 'Fix-projects' (#8) from Fix-projects into main
Reviewed-on: #8
2025-05-26 20:40:50 +00:00
seccodesmith b1816ec604 feat: update ProjectModal and projectsData for enhanced project details and technology display 2025-05-26 22:34:44 +02:00
seccodesmith e8a3611dfb feat: refactor BlogCard, ProjectCard, and ProjectModal components to use new type definitions and improve structure 2025-05-26 18:09:09 +02:00
seccodesmith 8e2030b0ab feat: enhance ProjectModal and Projects components by removing unused onClose prop and improving modal styling 2025-05-26 00:06:30 +02:00
seccodesmith c539be3c87 feat: update BlogCard and Project components with improved styling and structure 2025-05-24 22:31:34 +02:00
seccodesmith bce2276dfa feat: refactor ProjectCard and Projects components for improved category handling and styling 2025-05-24 19:31:43 +02:00
seccodesmith 7b2566d985 feat: enhance ProjectCard and Projects components with new styles and improved structure
style: update opacity in Home styles for better visibility
refactor: remove unnecessary comments in Projects component for cleaner code
2025-05-23 22:19:04 +02:00
seccodesmith e526fac14a Merge pull request 'Fixing-about' (#7) from Fixing-about into main
Reviewed-on: #7
2025-05-23 18:18:48 +00:00
seccodesmith 5bf0ffbad2 feat: update About page with new professional journey entry and testimonials section enhancements 2025-05-23 20:18:08 +02:00
seccodesmith 9ae26fbd9f style: enhance About page styles with responsive design and improved section titles 2025-05-23 19:29:51 +02:00
seccodesmith 6d32ddbfd0 refactor: standardize class naming conventions in About module styles to camelCase 2025-05-23 18:51:46 +02:00
seccodesmith 6ea40432f7 feat: add About section with core values, technical skills, and testimonials 2025-05-23 18:36:37 +02:00
seccodesmith 35c0a17f8a Merge pull request 'Fixing-blog' (#6) from Fixing-blog into main
Reviewed-on: #6
2025-05-23 10:01:37 +00:00
seccodesmith 9b2c295dd8 Merge branch 'main' into Fixing-blog 2025-05-23 10:01:31 +00:00
seccodesmith 45ebea5dab feat: add pagination functionality to Blog component and add function to automatic hide / show next previoce button 2025-05-23 12:00:51 +02:00
seccodesmith 7cab6ddea5 feat: implement dynamic Table of Contents in BlogPost component 2025-05-23 11:23:13 +02:00
seccodesmith 574e87bf99 feat: enhance BlogPost component with Table of Contents functionality and content formatting improvements 2025-05-23 10:42:36 +02:00
seccodesmith 4cc133fffa feat: enhance BlogPost component with dedent function, update styles, and improve content formatting 2025-05-23 09:49:37 +02:00
seccodesmith 142772a08a feat: add BlogPost styles using CSS modules, refactor BlogPost component for improved styling and structure 2025-05-22 23:01:16 +02:00
seccodesmith 3da8c4bddf Merge pull request 'feat: enhance responsive styles for title, subtitle, and buttons in Home component' (#5) from Fixing-home-modal into main
Reviewed-on: #5
2025-05-22 13:55:18 +00:00
seccodesmith 7d5a678008 feat: enhance responsive styles for title, subtitle, and buttons in Home component 2025-05-22 15:54:04 +02:00
seccodesmith b6894fac9b feat: refactor PageHeader component to use CSS modules and remove unused styles from Blog module 2025-05-22 15:44:26 +02:00
seccodesmith 77ba446d4d feat: add Blog and BlogCard styles, refactor components to use CSS modules, and remove unused CSS files 2025-05-22 15:05:24 +02:00
seccodesmith 762fc64806 Merge pull request 'Fixing-main' (#4) from Fixing-main into main
Reviewed-on: #4
2025-05-21 21:50:35 +00:00
seccodesmith 3b86302a6e feat: implement dynamic social links in Footer and add random code line data for Home component 2025-05-21 23:49:34 +02:00
seccodesmith 29b9dd93e2 feat: remove unused SVG files, add skill card data, and refactor Home component imports 2025-05-21 12:55:06 +02:00
seccodesmith d3f7368ba8 feat: update favicon and background images, refactor styles, and enhance skill card data 2025-05-21 12:13:48 +02:00
seccodesmith 7e91215af1 Merge pull request 'Trying-scss' (#3) from Trying-scss into main
Reviewed-on: #3
2025-05-20 19:07:28 +00:00
seccodesmith 03a9f61a2a feat: refactor Home and SkillCard components, add skillCard data for dynamic rendering 2025-05-20 20:51:24 +02:00
seccodesmith fdb79c70ad feat: update Font Awesome and DevIcon versions, refactor styles in Home and SkillCard components 2025-05-20 20:01:33 +02:00
seccodesmith fb84bf87de feat: refactor styles and update imports for Home and Blog components 2025-05-20 14:43:49 +02:00
seccodesmith 848318a23e feat: update dependencies and add new styles
- Added node and sass to dependencies in package.json
- Added @types/node to devDependencies
- Removed unused CSS files and replaced them with SCSS files
- Updated import paths in App.tsx and Blog.tsx to use new aliasing
- Configured TypeScript paths for easier imports
- Updated Vite config to include path aliases
- Created new SCSS files for About, Blog, Error, and Main styles
- Added a variables file for color and effect variables
- Updated index.scss for global styles and responsive adjustments
2025-05-18 14:29:54 +02:00
seccodesmith bb3f67a83f Remove unused CSS module import from BlogCard component 2025-05-17 22:26:29 +02:00
seccodesmith f612ee1520 Merge branch 'main' of https://git.seccodesmith.pl/seccodesmith/seccodesmith-frontend 2025-05-17 22:20:59 +02:00
seccodesmith 35b34ab5aa Some change 2025-05-17 22:17:31 +02:00
seccodesmith 2e8777b5c3 Merge pull request 'Add CSS module import to BlogCard component and restructure Home page skills section' (#2) from master into main
Reviewed-on: #2
2025-05-16 13:04:08 +00:00
seccodesmith 1656b07225 Add CSS module import to BlogCard component and restructure Home page skills section 2025-05-16 14:57:32 +02:00
seccodesmith bdc073f5d7 Merge pull request 'First commit' (#1) from master into main
Reviewed-on: #1
2025-05-16 12:04:02 +00:00
seccodesmith c741dc73bb First commit 2025-05-16 14:02:21 +02:00
74 changed files with 11258 additions and 1 deletions
+150 -1
View File
@@ -1,2 +1,151 @@
# seccodesmith-frontend # SecCodeSmith Frontend Portfolio
![Image](./public/images/SecCodeSmithFrontProj/Main.png)
A modern portfolio website with a unique fantasy blacksmith and dragon theme, showcasing my development skills and projects.
## 🐉 About
This portfolio represents my journey as a developer, designed with the aesthetic of a fantasy blacksmith working alongside dragons. The site combines modern web technologies with creative storytelling to create an engaging user experience.
**Live Demo**: [https://seccodesmith.pl](https://seccodesmith.pl)
## 🛠️ Built With
- **[Vite](https://vitejs.dev/)** - Next Generation Frontend Tooling
- **[React](https://reactjs.org/)** - JavaScript library for building user interfaces
- **[TypeScript](https://www.typescriptlang.org/)** - JavaScript with syntax for types
- **[SCSS](https://sass-lang.com/)** - CSS preprocessor for better styling
## 📋 Prerequisites
Before you begin, ensure you have the following installed:
- **Node.js** (v16.0.0 or higher)
- **npm** (v8.0.0 or higher) or **yarn** (v1.22.0 or higher)
You can check your versions by running:
```bash
node --version
npm --version
```
## 🚀 Getting Started
### Installation
1. Clone the repository:
```bash
git clone https://git.seccodesmith.pl/seccodesmith/seccodesmith-frontend.git
cd seccodesmith-frontend
```
2. Install dependencies:
```bash
npm install
# or
yarn install
```
### Running the Project
#### Development Mode
To run the project in development mode with hot-reload:
```bash
npm run dev
# or
yarn dev
```
The application will be available at `http://localhost:5173` (default Vite port).
#### Production Build
To build the project for production:
```bash
npm run build
# or
yarn build
```
#### Preview Production Build
To preview the production build locally:
```bash
npm run preview
# or
yarn preview
```
## 📁 Project Structure
```
seccodesmith-frontend/
├── src/
│ ├── assets/ # Images, fonts, and other static assets
│ ├── components/ # React components
│ ├── styles/ # SCSS files
│ ├── pages/ # Page components
│ ├── utils/ # Utility functions
│ ├── App.tsx # Main App component
│ └── main.tsx # Application entry point
├── public/ # Public static files
├── index.html # HTML template
├── vite.config.ts # Vite configuration
├── tsconfig.json # TypeScript configuration
├── package.json # Project dependencies and scripts
└── README.md # Project documentation
```
## ✨ Features
- Responsive design that works on all devices
- Interactive animations and transitions
- Dark/Light theme toggle
- Project showcase with detailed descriptions
- Contact form integration
- Performance optimized with Vite
## 🧪 Available Scripts
- `npm run dev` - Start development server
- `npm run build` - Build for production
- `npm run preview` - Preview production build
- `npm run lint` - Run ESLint
- `npm run type-check` - Run TypeScript compiler check
## 🎨 Customization
To customize the portfolio for your own use:
1. Update personal information in `src/data/personal.ts`
2. Replace projects in `src/data/projects.ts`
3. Modify color schemes in `src/styles/variables.scss`
4. Update images in `src/assets/`
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 🤝 Contributing
While this is a personal portfolio project, suggestions and feedback are welcome! Feel free to:
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
## 📬 Contact
**SecCodeSmith** - [contact@seccodesmith.pl](mailto:contact@seccodesmith.pl)
Project Link: [https://git.seccodesmith.pl/seccodesmith/seccodesmith-frontend](https://git.seccodesmith.pl/seccodesmith/seccodesmith-frontend)
## 🙏 Acknowledgments
- Inspiration from fantasy blacksmith and dragon themes
- [React Icons](https://react-icons.github.io/react-icons/) for icon library
- [Vite Documentation](https://vitejs.dev/guide/) for build tool guidance
---
⚔️ Forged with passion by SecCodeSmith 🐲
+28
View File
@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)
+28
View File
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- DevIcon -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/devicons/devicon@v2.16.0/devicon.min.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=Fira+Code:wght@400;600&display=swap" rel="stylesheet">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- highlight CSS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js" integrity="sha512-EBLzUL8XLl+va/zAsmXwS7Z2B1F9HUHkZwyS/VKwh3S7T/U0nF4BaU29EP/ZSf6zgiIxYAnKLu6bJ8dqpmX5uw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<title>SecCodeSmith - Code Forged in Digital Fire</title>
<link rel="favicon" href="public/favicon.ico" type="image/x-icon">
<meta name="description" content="SecCodeSmith - Master craftsman of code specializing in embedded systems, machine learning, and web development. Forging digital solutions with security and precision.">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
+4387
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
{
"name": "seccodesmith-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@testing-library/react": "^16.3.0",
"node": "^20.19.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.0",
"sass": "^1.89.0",
"vitest": "^3.1.3"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@testing-library/jest-dom": "^6.6.3",
"@types/node": "^22.15.18",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"path": "^0.12.7",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5"
}
}
+87
View File
@@ -0,0 +1,87 @@
{
"coreValues": [
{
"title": "Security by Design",
"icon": "fas fa-shield-alt",
"description": "Security is not an afterthought but the very foundation upon which robust systems are built. Every line of code is crafted with defensive principles in mind."
},
{
"icon": "fas fa-tachometer-alt",
"title": "Performance Optimization",
"description": "Like a finely-honed blade, code should be both effective and efficient. I meticulously optimize every component for maximum speed and minimal resource consumption."
},
{
"icon": "fas fa-gem",
"title": "Elegant Simplicity",
"description": "True mastery is revealed not in complexity but in elegant simplicity. I strive to create solutions that are accessible, maintainable, and beautifully crafted."
},
{
"icon": "fas fa-sync-alt",
"title": "Continuous Evolution",
"description": "The digital landscape changes with the seasons. I embrace continuous learning and adaptation, integrating new techniques and technologies to forge ever-better solutions."
}
],
"technicalArsenal": [
{
"icon": "fas fa-microchip",
"title": "Embedded Systems",
"skills": [
"STM32 microcontroller programming (I2C, SPI, UART)",
"ESP32 wireless solutions",
"Firmware development with C/C++",
"Raspberry Pi and Pico-SDK integration",
"RTOS implementation"
]
},
{
"icon": "fas fa-brain",
"title": "AI & Data Analysis",
"skills": [
"Machine learning with PyTorch",
"Data manipulation with Pandas",
"Predictive modeling via Scikit-learn",
"Computer vision with Scikit-image",
"Edge AI deployment"
]
},
{
"icon": "fas fa-window-maximize",
"title": "Web Development",
"skills": [
"Web Development",
"Backend development with ASP.NET",
"Database management with Entity Framework",
"Frontend frameworks: Blazor, React",
"Responsive design with Bootstrap",
"Python-based web solutions with Django"
]
},
{
"icon": "fas fa-server",
"title": "DevOps & Infrastructure",
"skills": [
"Containerization with Docker",
"Multi-container orchestration via Docker-compose",
"Linux server administration",
"CI/CD pipeline implementation",
"Build system configuration with CMake"
]
}
],
"professionalJourney": [
{
"title": "Student at Rzeszów University of Technology",
"description": "Specialization: Computer Science",
"duration": "2021.10 - 2025.6"
},
{
"title": "intern software engineer at Opeteam S.A.",
"description": "- Performing programming tasks in R&D projects within the product development department,\n - Database administration, \n - Defining unit tests and manual testing of solutions, \n - Selecting technologies for basic problems, - Documenting the source code (technical documentation)., \n",
"duration": "2022.8 - 2023.4"
}
],
"testimonials": [
]
}
+19
View File
@@ -0,0 +1,19 @@
[
{
"id": "1",
"slug": "StartOFJurney",
"title": "Something ends, something begins.",
"excerpt": "",
"image": "images/Begining.jpg",
"category": "Story",
"date": "27-05-2025",
"author": "SecCodeSmith (Jakub)",
"commentCount": 0,
"readTime": "10m",
"featured": true,
"tags": [
"journey"
],
"content": "# I created this blog as the start of building my professional image, \n a place to document my projects and share my experiences. It's also a personal challenge for me. \n I am about to finish my studies and step into a new chapter of my professional life. \n On this blog, I will describe my projects and the lessons I've learned along the way. \n My first project will be creating my own website, featuring a blog, information about me, and descriptions of my work. \n See you soon!"
}
]
+18
View File
@@ -0,0 +1,18 @@
{
"email": "forge@seccodesmith.pl",
"businessEmail": "business@seccodesmith.pl",
"socialLinks": [
{ "platform": "LinkedIn", "url": "https://www.linkedin.com/in/jakub-berechowski", "icon": "fab fa-linkedin" },
{ "platform": "GitHub", "url": "https://github.com/seccodesmith", "icon": "fab fa-github" },
{ "platform": "Facebook", "url": "https://www.facebook.com/people/Seccodesmith/61575058536717/", "icon": "fab fa-facebook" }
],
"questionsAndAnswers": [
{ "question": "What is your typical project timeline?", "answer": "Project timelines vary based on complexity and scope. Small projects typically take 2-4 weeks, medium-sized projects 1-2 months, and larger enterprise solutions 3-6 months. I provide detailed timeline estimates during the initial consultation, and maintain transparent communication about progress throughout the development cycle." },
{ "question": "How does your collaboration process work?", "answer": "My collaboration process consists of five phases: initial consultation, requirements gathering, design and planning, implementation, and testing/deployment. I use agile methodologies with regular check-ins to ensure your vision is being realized. Communication is maintained through your preferred channels (email, Slack, etc.), and you\"ll have access to development milestones through shared repositories and project management tools." },
{ "question": "What are your payment terms and methods?", "answer": "I typically work with a milestone-based payment structure: 30% upfront to begin work, 30% at the midpoint after key deliverables are approved, and 40% upon project completion. For longer projects, we can establish additional milestones. I accept bank transfers, PayPal, and cryptocurrency payments. All payment terms are clearly outlined in the project contract before work begins."},
{ "question": "Who owns the code and deliverables?", "answer": "Upon final payment, you receive full ownership rights to all custom code and deliverables created specifically for your project. I maintain no proprietary claim to custom solutions developed for clients. For projects that incorporate open-source components, those elements remain under their original licenses, and I provide clear documentation of all third-party code used in your project." },
{ "question": "Do you offer support and maintenance?", "answer": "Yes, I offer a 30-day warranty period for all projects, during which bug fixes and minor adjustments are covered at no additional cost. For ongoing support, I provide maintenance packages that include regular updates, security patches, and priority response times. These can be tailored to your specific needs and budget, whether you require occasional support or dedicated monthly maintenance."}
],
"MapIframeUrl": "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d82077.2903513271!2d21.9433946!3d50.0411861!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x473cfae3cc14d449%3A0xd2240d31b33eb2ed!2zUnplc3rDs3c!5e0!3m2!1sen!2spl!4v1651813309336!5m2!1sen!2spl",
"phone": ""
}
+43
View File
@@ -0,0 +1,43 @@
[
{
"id": "SecCodeSmithFrontend",
"title": "SecCodeSmith Portfolio Page - Frontend",
"description": "A modern, responsive portfolio page showcasing my projects, skills, and blog posts. Built with React and styled-components for a sleek user interface.",
"image": "/images/SecCodeSmithFrontProj/Main.png",
"category": ["Web"],
"featured": true,
"technologies": [
{ "name": "React", "icon": "devicon-react-original tech-icon" },
{ "name": "TypeScript", "icon": "devicon-typescript-plain tech-icon" },
{ "name": "SCSS", "icon": "devicon-sass-original tech-icon" },
{ "name": "Vite", "icon": "devicon-vitejs-plain tech-icon" }
],
"links": {
"github": "https://github.com/SecCodeSmith/",
"demo": "https://seccodesmith.pl"
},
"projectDetails": {
"descriptions": [
"This portfolio page is designed to highlight my skills and projects in a visually appealing way. It features a clean layout, smooth animations, and responsive design for optimal viewing on all devices.",
"The project utilizes React for building the user interface, TypeScript for type safety, and SCSS for styling. The use of Vite ensures fast development and build times."
],
"startDate": "12-05-2025",
"endDate": "",
"dateFormatted": "DD MMMM YYYY",
"role": "Frontend Developer",
"status": "In Progress",
"keyFeatures": [
"Responsive design with mobile-first approach",
"Dynamic project showcase with filtering options",
"Interactive blog section with latest posts"
],
"gallery": ["/images/SecCodeSmithFrontProj/1.png", "/images/SecCodeSmithFrontProj/2.png", "/images/SecCodeSmithFrontProj/3.png", "/images/SecCodeSmithFrontProj/4.png" ],
"fullTechStack": [
{ "name": "React", "icons": "devicon-react-original" },
{ "name": "TypeScript", "icons": "devicon-typescript-plain" },
{ "name": "SCSS", "icons": "devicon-sass-original" },
{ "name": "Vite", "icons": "devicon-vitejs-plain" }
]
}
}
]
+58
View File
@@ -0,0 +1,58 @@
[
{
"categoryIcon": "fa-code",
"categoryTitle": "Languages",
"skills": [
{ "name": "C/C++", "icon": "devicon-cplusplus-plain" },
{ "name": "Python", "icon": "devicon-python-plain" },
{ "name": ".NET", "icon": "devicon-dotnetcore-plain" },
{ "name": "C#", "icon": "devicon-csharp-plain" }
]
},
{
"categoryIcon": "fa-microchip",
"categoryTitle": "Embedded Systems",
"skills": [
{ "name": "STM32 (I2C, SPI, UART)", "icon": "fas fa-microchip" },
{ "name": "ESP32", "icon": "fas fa-wifi" },
{ "name": "Pico-SDK", "icon": "fas fa-memory" }
]
},
{
"categoryIcon": "fa-brain",
"categoryTitle": "Data & ML",
"skills": [
{ "name": "Pandas", "icon": "devicon-pandas-plain" },
{ "name": "Scikit-learn", "icon": "devicon-scikitlearn-plain" },
{ "name": "Scikit-image", "icon": "fas fa-image" },
{ "name": "PyTorch", "icon": "devicon-pytorch-plain" },
{ "name": "OpenCV", "icon": "devicon-opencv-plain" }
]
},
{
"categoryIcon": "fas fa-window-maximize",
"categoryTitle": "Web Development",
"skills": [
{ "name": "ASP.NET Core", "icon": "devicon-dot-net-plain" },
{ "name": "Entity Framework", "icon": "fas fa-database" },
{ "name": "Blazor", "icon": "devicon-dotnetcore-plain" },
{ "name": "React", "icon": "devicon-react-original" },
{ "name": "Django", "icon": "devicon-django-plain" },
{ "name": "Bootstrap", "icon": "devicon-bootstrap-plain" }
]
},
{
"categoryIcon": "fas fa-cloud",
"categoryTitle": "DevOps & Systems",
"skills": [
{ "name": "Docker", "icon": "devicon-docker-plain" },
{ "name": "Docker-compose", "icon": "fas fa-layer-group" },
{ "name": "Linux CLI", "icon": "devicon-bash-plain" },
{ "name": "Git", "icon": "devicon-git-plain" },
{ "name": "CI/CD", "icon": "fas fa-cogs" },
{ "name": "Cmake", "icon": "devicon-cmake-plain" },
{ "name": "Linux", "icon": "devicon-linux-plain" },
{ "name": "Jenkins", "icon": "devicon-jenkins-plain" }
]
}
]
+22
View File
@@ -0,0 +1,22 @@
[
{
"icon": "fab fa-github",
"url": ""
},
{
"icon": "fab fa-linkedin",
"url": ""
},
{
"icon": "fab fa-twitter",
"url": ""
},
{
"icon": "fab fa-discord",
"url": ""
},
{
"icon": "fas fa-envelope",
"url": ""
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

+30
View File
@@ -0,0 +1,30 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import { Layout } from './components/Layout'
import { Home } from './pages/Home'
import { About } from './pages/About'
import { Blog } from './pages/Blog'
import { BlogPost } from './pages/BlogPost'
import { Projects } from './pages/Projects'
import { Contact } from './pages/Contact'
import { NotFound } from './pages/NotFound'
import '@components/styles.css'
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/blog" element={<Blog />} />
<Route path="/blog/:slug" element={<BlogPost />} />
<Route path="/projects" element={<Projects />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</Router>
)
}
export default App
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+618
View File
@@ -0,0 +1,618 @@
@use './variables' as *;
/* Section styling */
.sectionTitle {
font-size: 2.25rem;
margin-bottom: 1.5rem;
position: relative;
padding-bottom: 1rem;
display: inline-block;
}
.sectionTitle::after {
content: "";
position: absolute;
width: 50px;
height: 3px;
background: $accent-fire;
bottom: 0;
left: 0;
box-shadow: $glow-effect;
}
.profileImage {
width: 100%;
aspect-ratio: 1/1;
border-radius: 8px;
overflow: hidden;
border: 3px solid rgba(255, 100, 0, 0.3);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
position: relative;
}
.profileImage::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(45deg, rgba(255, 100, 0, 0.2), transparent);
z-index: 1;
}
.profileImage img {
width: 100%;
height: 100%;
object-fit: cover;
filter: contrast(1.1) brightness(0.9);
}
.introText {
font-size: 1.1rem;
line-height: 1.8;
color: $text-ash;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
}
.introText p {
margin-bottom: 1.25rem;
}
.introText strong {
color: $text-light;
}
.introText .accent {
color: $accent-fire;
}
.connectLink {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 20px;
background-color: rgba(255, 255, 255, 0.05);
border-radius: 4px;
text-decoration: none;
color: $text-light;
transition: all 0.3s ease;
border: 1px solid $card-border;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
}
.connectLink:hover {
background-color: rgba(255, 100, 0, 0.1);
transform: translateY(-3px);
border-color: $accent-fire;
color: $text-light;
}
.connectIcon {
color: $accent-fire;
}
/* Value cards */
.valueCard {
background-color: $card-bg;
border-radius: 8px;
padding: 1.875rem;
border: 1px solid $card-border;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
text-align: center;
height: 100%;
}
.valueCard:hover {
transform: translateY(-5px);
border-color: rgba(255, 100, 0, 0.3);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
}
.valueIcon {
font-size: 2.5rem;
color: $accent-fire;
margin-bottom: 1.25rem;
}
.valueTitle {
font-size: 1.3rem;
margin-bottom: 1rem;
position: relative;
display: inline-block;
color: $text-light;
}
.valueTitle::after {
content: "";
position: absolute;
width: 40px;
height: 2px;
background: $accent-fire;
bottom: -5px;
left: 50%;
transform: translateX(-50%);
}
.valueText {
color: $text-ash;
line-height: 1.6;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* Skills card */
.skillCard {
background-color: $card-bg;
border-radius: 8px;
padding: 1.875rem;
border: 1px solid $card-border;
transition: all 0.3s ease;
position: relative;
height: 100%;
}
.skillCard:hover {
transform: translateY(-10px);
border-color: rgba(255, 100, 0, 0.3);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
}
.skillCard::before {
content: "";
position: absolute;
width: 100%;
height: 5px;
top: 0;
left: 0;
background: linear-gradient(to right, $accent-dragon, $accent-fire);
border-radius: 8px 8px 0 0;
}
.skillHeader {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 1.25rem;
padding-bottom: 0.625rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.skillIcon {
font-size: 2rem;
color: $accent-fire;
}
.skillTitle {
font-size: 1.3rem;
margin-bottom: 0;
color: $text-light;
}
.skillCheck {
color: $accent-fire;
margin-right: 0.625rem;
}
/* Timeline */
.timeline {
position: relative;
padding: 0;
margin-top: 3rem;
list-style: none;
}
.timeline:before {
content: "";
position: absolute;
width: 3px;
height: 100%;
background: linear-gradient(to bottom, $accent-fire, $accent-dragon, rgba(255, 100, 0, 0.2));
left: 50%;
transform: translateX(-50%);
z-index: 1;
}
.timelineItem {
position: relative;
margin-bottom: 3rem;
width: 100%;
}
.timelineItem:nth-child(odd) .timelineContent {
margin-left: auto;
padding-left: 3rem;
border-radius: 8px;
}
.timelineItem:nth-child(even) .timelineContent {
margin-right: auto;
padding-right: 3rem;
border-radius: 8px;
}
.timelineDot {
width: 16px;
height: 16px;
background-color: $accent-fire;
border-radius: 50%;
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
box-shadow: $glow-effect;
z-index: 2;
}
.timelineDate {
display: inline-block;
font-size: 0.9rem;
background-color: rgba(255, 100, 0, 0.1);
color: $accent-fire;
padding: 5px 10px;
border-radius: 4px;
margin-bottom: 1rem;
font-weight: 600;
letter-spacing: 1px;
}
.timelineTitle {
font-size: 1.3rem;
margin-bottom: 0.625rem;
color: $text-light;
}
.timelineText {
color: $text-ash;
line-height: 1.6;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.timelineContent {
background-color: $card-bg;
border-radius: 8px;
padding: 1.5rem;
border: 1px solid $card-border;
position: relative;
transition: all 0.3s ease;
width: 45%;
}
.timelineContent:hover {
transform: translateY(-5px);
border-color: rgba(255, 100, 0, 0.3);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
}
/* Testimonials */
.testimonial {
background-color: $card-bg;
border-radius: 8px;
padding: 1.875rem;
margin-bottom: 1.875rem;
border: 1px solid $card-border;
transition: all 0.3s ease;
position: relative;
}
.testimonial:hover {
transform: translateY(-5px);
border-color: rgba(255, 100, 0, 0.3);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
}
.testimonialContent {
padding-left: 1.25rem;
border-left: 3px solid $accent-fire;
font-style: italic;
color: $text-ash;
margin-bottom: 1.25rem;
line-height: 1.7;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.authorAvatar {
width: 60px;
height: 60px;
border-radius: 50%;
overflow: hidden;
}
.authorAvatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.authorName {
font-weight: 600;
font-size: 1.1rem;
color: $text-light;
margin-bottom: 0;
}
.authorTitle {
color: $text-ash;
font-size: 0.9rem;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* Blog post specific styles */
.postContainer {
max-width: 1200px;
margin: 0 auto;
padding: 120px 20px 60px;
display: grid;
grid-template-columns: 1fr 300px;
gap: 40px;
}
.postMain {
background-color: $card-bg;
border-radius: 8px;
overflow: hidden;
border: 1px solid $card-border;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.postHeader {
position: relative;
}
.postFeaturedImage {
height: 400px;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
position: relative;
}
.postFeaturedImage::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100px;
background: linear-gradient(to top, $card-bg, transparent);
}
.postInfo {
padding: 30px 40px;
}
.postContent {
padding: 0 40px 40px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.8;
color: #e0e0e0;
}
.shareSection {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid $card-border;
}
.shareTitle {
font-size: 1.2rem;
margin-bottom: 15px;
}
.shareButtons {
display: flex;
gap: 15px;
}
.shareButton {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.05);
display: flex;
align-items: center;
justify-content: center;
color: $text-ash;
transition: all 0.3s ease;
}
.shareButton:hover {
background-color: $accent-fire;
color: $text-light;
transform: translateY(-3px);
}
.authorSection {
margin-top: 40px;
padding: 30px;
background-color: rgba(255, 255, 255, 0.02);
border-radius: 8px;
display: flex;
gap: 20px;
align-items: center;
}
.tocList {
list-style: none;
}
.tocItem {
margin-bottom: 12px;
padding-left: 15px;
position: relative;
}
.tocItem::before {
content: "";
position: absolute;
left: 0;
top: 10px;
width: 5px;
height: 5px;
border-radius: 50%;
background-color: $accent-fire;
}
.tocLink {
color: $text-ash;
text-decoration: none;
transition: all 0.3s ease;
font-size: 0.9rem;
line-height: 1.5;
display: block;
}
.tocLink:hover {
color: $accent-fire;
transform: translateX(5px);
}
.tocSubitem {
margin-top: 8px;
padding-left: 15px;
}
.relatedPostsList {
list-style: none;
}
.relatedPostItem {
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.relatedPostItem:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.relatedPostLink {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: $text-ash;
transition: color 0.3s ease;
}
.relatedPostLink:hover {
color: $accent-fire;
}
.relatedPostThumb {
width: 60px;
height: 60px;
border-radius: 4px;
background-size: cover;
background-position: center;
flex-shrink: 0;
}
.relatedPostInfo {
flex-grow: 1;
}
.relatedPostTitle {
font-size: 0.9rem;
line-height: 1.4;
margin-bottom: 5px;
}
.relatedPostDate {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.5);
}
/* Social grid for contact page */
.socialGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 15px;
margin-top: 1.5rem;
}
.socialItem {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 10px;
padding: 15px;
border-radius: 8px;
background-color: rgba(255, 255, 255, 0.05);
transition: all 0.3s ease;
text-decoration: none;
}
.socialItem:hover {
background-color: rgba(255, 100, 0, 0.1);
transform: translateY(-5px);
}
.socialIconLg {
font-size: 1.8rem;
color: $accent-fire;
}
.socialName {
color: $text-ash;
font-size: 0.9rem;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
@media (max-width: 992px) {
.pageTitle {
font-size: 2.5rem;
}
.timeline:before {
left: 2rem;
}
.timelineItem:nth-child(odd) .timeline-content,
.timelineItem:nth-child(even) .timeline-content {
width: calc(100% - 3rem);
margin-left: 3rem;
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.timelineDot {
left: 2rem;
}
}
@media (max-width: 768px) {
.pageTitle {
font-size: 2rem;
}
.introText {
margin-top: 2rem;
}
}
@media (max-width: 576px) {
.pageTitle {
font-size: 1.8rem;
}
.sectionTitle {
font-size: 1.8rem;
}
.testimonialAuthor {
flex-direction: column;
align-items: center;
text-align: center;
}
.authorInfo {
margin-top: 1rem;
text-align: center;
}
}
+41
View File
@@ -0,0 +1,41 @@
@use './variables.scss' as *;
/* Accordion styling */
.accordion-button {
background-color: $card-bg !important;
color: $text-light !important;
border-color: $card-border !important;
box-shadow: none !important;
padding: 1rem 1.25rem;
font-family: 'Cinzel', serif;
}
.accordion-button:not(.collapsed) {
color: $accent-fire !important;
}
.accordion-button:focus {
border-color: $accent-fire !important;
box-shadow: 0 0 0 0.25rem rgba(255, 100, 0, 0.25) !important;
}
.accordion-button::after {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23ff6400'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e") !important;
}
.accordion-item {
background-color: transparent;
border: none;
margin-bottom: 1rem;
}
.accordion-body {
background-color: $card-bg;
border: 1px solid $card-border;
border-top: none;
color: $text-ash;
padding: 1.25rem;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
border-bottom-left-radius: 0.375rem;
border-bottom-right-radius: 0.375rem;
}
+223
View File
@@ -0,0 +1,223 @@
@use "./variables.scss" as *;
/* Meta items */
@mixin metaItem {
.metaItem {
display: inline-flex;
align-items: center;
gap: 6px;
margin-right: 1rem;
color: $text-ash;
font-size: 0.9rem;
}
}
@mixin metaIcon {
.metaIcon {
font-size: 1.2rem;
color: $text-ash;
transition: all 0.3s ease;
}
.metaIcon:hover {
color: $accent-fire;
}
}
/* Read more link */
@mixin readMore {
.readMore {
display: inline-block;
color: $accent-fire;
text-decoration: none;
font-weight: 600;
transition: all 0.3s ease;
position: relative;
}
.readMore::after {
content: "";
margin-left: 5px;
transition: transform 0.3s ease;
}
.readMore:hover::after {
transform: translateX(5px);
}
}
/* Tags */
@mixin postTag {
.postTag {
display: inline-block;
background-color: rgba(255, 100, 0, 0.1);
color: $accent-fire;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
transition: all 0.3s ease;
}
.postTag:hover {
background-color: rgba(255, 100, 0, 0.2);
transform: translateY(-2px);
}
}
@include metaItem;
@include metaIcon;
@include readMore;
@include postTag;
/* Sidebar */
.sidebarWidget {
background-color: $card-bg;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid $card-border;
}
.widgetTitle {
font-size: 1.2rem;
margin-bottom: 1.25rem;
padding-bottom: 0.625rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
}
.widgetTitle::after {
content: "";
position: absolute;
width: 40px;
height: 2px;
background: $accent-fire;
bottom: -1px;
left: 0;
}
.searchForm .formControl {
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid $card-border;
color: $text-light;
}
.searchForm .formControl:focus {
box-shadow: 0 0 0 0.25rem rgba(255, 100, 0, 0.25);
border-color: $accent-fire;
}
.searchForm .btn {
background-color: $accent-fire;
border-color: $accent-fire;
}
.searchForm .btn:hover {
background-color: $accent-dragon;
border-color: $accent-dragon;
}
/* Lists in sidebar */
.listGroupItem {
background-color: transparent;
border-color: rgba(255, 255, 255, 0.05);
color: $text-ash;
transition: all 0.3s ease;
}
.listGroupItem:hover {
background-color: rgba(255, 100, 0, 0.1);
}
.listGroupItem a {
color: $text-ash;
text-decoration: none;
display: flex;
justify-content: space-between;
align-items: center;
}
.listGroup-item a:hover {
color: $accent-fire;
}
.categoryCount {
background-color: rgba(255, 100, 0, 0.1);
color: $accent-fire;
padding: 2px 8px;
border-radius: 10px;
font-size: 0.8rem;
}
/* Recent posts in sidebar */
.recentPostItem {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.recentPostItem:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.recentPostThumb {
width: 60px;
height: 60px;
border-radius: 4px;
object-fit: cover;
}
.recentPostInfo {
flex-grow: 1;
}
.recentPostTitle {
font-size: 0.9rem;
line-height: 1.4;
margin-bottom: 5px;
}
.recentPostTitle a {
color: $text-ash;
text-decoration: none;
transition: color 0.3s;
}
.recentPostTitle a:hover {
color: $accent-fire;
}
.recentPostDate {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.5);
}
/* Pagination */
.pageItem .pageLink {
background-color: $card-bg;
border-color: $card-border;
color: $text-ash;
}
.pageItem .pageLink:hover {
background-color: rgba(255, 100, 0, 0.1);
color: $accent-fire;
border-color: $accent-fire;
}
.pageItem.active .pageLink {
background-color: $accent-fire;
border-color: $accent-fire;
color: $text-light;
}
+95
View File
@@ -0,0 +1,95 @@
@use "./variables.scss" as *;
@use "./Blog.module.scss" as Blog;
@include Blog.metaItem;
@include Blog.metaIcon;
@include Blog.readMore;
@include Blog.postTag;
/* Card styling */
.card {
background-color: $card-bg;
border: 1px solid $card-border;
border-radius: 8px;
transition: all 0.3s ease;
margin-bottom: 1.5rem;
overflow: hidden;
}
.card:hover {
transform: translateY(-5px);
border-color: rgba(255, 100, 0, 0.3);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
}
.cardImgTop {
height: 200px;
object-fit: cover;
}
.featuredImg {
height: 300px;
}
.cardTitle {
font-size: 1.5rem;
margin-bottom: 0.75rem;
}
.cardTitle a {
color: $text-light;
text-decoration: none;
transition: color 0.3s;
}
.cardTitle a:hover {
color: $accent-fire;
}
.cardText {
color: $text-ash;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
}
.featuredCard .cardTitle {
font-size: 1.8rem;
}
.cardFooter {
background-color: rgba(0, 0, 0, 0.2);
border-top: 1px solid $card-border;
}
/* Category badges */
.categoryBadge {
position: absolute;
top: 1rem;
right: 1rem;
background-color: rgba(139, 0, 0, 0.9);
color: $text-light;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
}
.featuredBadge {
position: absolute;
top: 1rem;
left: 1rem;
background: linear-gradient(45deg, $accent-dragon, $accent-fire);
color: $text-light;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
display: flex;
align-items: center;
gap: 5px;
}
+814
View File
@@ -0,0 +1,814 @@
@use './variables' as *;
@use './Blog.module.scss' as Blog;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Cinzel', serif;
}
.postContainer {
max-width: 1200px;
margin: 0 auto;
padding: 120px 20px 60px;
display: grid;
grid-template-columns: 1fr 300px;
gap: 40px;
}
.postMain {
background-color: $card-bg;
border-radius: 8px;
overflow: hidden;
border: 1px solid $card-border;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.postHeader {
position: relative;
}
.postFeaturedImage {
height: 400px;
background: url('/api/placeholder/900/400') center/cover no-repeat;
position: relative;
}
.postFeaturedImage::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100px;
background: linear-gradient(to top, $card-bg, transparent);
}
.postCategory {
position: absolute;
top: 20px;
left: 20px;
background: rgba(139, 0, 0, 0.9);
color: $text-light;
padding: 5px 15px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
backdrop-filter: blur(5px);
z-index: 1;
}
.postInfo {
padding: 30px 40px;
}
.postTitle {
font-size: 2.5rem;
margin-bottom: 20px;
line-height: 1.3;
}
.postMeta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 20px;
font-size: 0.9rem;
color: $text-ash;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid $card-border;
}
.metaItem {
display: flex;
align-items: center;
gap: 6px;
}
.metaIcon {
color: $accent-fire;
}
.postContent {
padding: 0 40px 40px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.8;
color: #e0e0e0;
}
.postContent p {
margin-bottom: 20px;
}
.postContent h2 {
font-size: 1.8rem;
margin: 40px 0 20px;
padding-bottom: 10px;
border-bottom: 1px solid $card-border;
font-family: 'Cinzel', serif;
color: $text-light;
position: relative;
}
.postContent h2::before {
content: "//";
margin-right: 10px;
color: $accent-fire;
font-weight: normal;
font-family: 'Fira Code', monospace;
}
.postContent h3 {
font-size: 1.4rem;
margin: 30px 0 15px;
font-family: 'Cinzel', serif;
color: $text-light;
}
.postContent ul,
.postContent ol {
margin: 0 0 20px 20px;
padding-left: 20px;
}
.postContent li {
margin-bottom: 10px;
}
.postContent a {
color: $accent-fire;
text-decoration: none;
transition: all 0.3s ease;
}
.postContent a:hover {
border-bottom-style: solid;
}
.postContent blockquote {
margin: 30px 0;
padding: 20px 30px;
border-left: 4px solid $accent-fire;
background-color: rgba(255, 100, 0, 0.05);
font-style: italic;
color: $text-ash;
position: relative;
}
.postContent blockquote::before {
content: "\"";
position: absolute;
top: 10px;
left: 10px;
font-size: 3rem;
color: rgba(255, 100, 0, 0.2);
font-family: Georgia, serif;
}
.postContent img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 20px 0;
display: block;
}
.postContent pre {
margin: 20px 0;
border-radius: 8px;
overflow-x: auto;
background-color: $code-bg;
font-family: 'Fira Code', monospace;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
border: 1px solid $card-border;
}
.postContent code {
font-family: 'Fira Code', monospace;
font-size: 0.9rem;
padding: 0 4px;
background-color: rgba(255, 255, 255, 0.05);
border-radius: 3px;
color: $text-light;
}
.postContent pre code {
padding: 0;
background-color: transparent;
border-radius: 0;
}
.postContent .highlight {
margin: 30px 0;
position: relative;
}
.postContent .highlight::before {
content: attr(data-language);
position: absolute;
top: 0;
right: 0;
background-color: $accent-fire;
color: $text-light;
padding: 2px 10px;
font-size: 0.7rem;
border-radius: 0 8px 0 8px;
font-family: 'Fira Code', monospace;
text-transform: uppercase;
z-index: 1;
}
.codeTitle {
background-color: rgba(255, 100, 0, 0.1);
padding: 10px 15px;
border-radius: 8px 8px 0 0;
font-family: 'Fira Code', monospace;
font-size: 0.9rem;
color: $accent-fire;
border-bottom: 1px solid rgba(255, 100, 0, 0.2);
display: flex;
align-items: center;
gap: 10px;
}
.postTags {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid $card-border;
}
.postTag {
background-color: rgba(255, 100, 0, 0.1);
color: $accent-fire;
padding: 5px 12px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
transition: all 0.3s ease;
}
.postTag:hover {
background-color: rgba(255, 100, 0, 0.2);
transform: translateY(-2px);
}
.shareSection {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid $card-border;
}
.shareTitle {
font-size: 1.2rem;
margin-bottom: 15px;
}
.shareButtons {
display: flex;
gap: 15px;
}
.shareButton {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.05);
display: flex;
align-items: center;
justify-content: center;
color: $text-ash;
transition: all 0.3s ease;
}
.shareButton:hover {
background-color: $accent-fire;
color: $text-light;
transform: translateY(-3px);
}
.authorSection {
margin-top: 40px;
padding: 30px;
background-color: rgba(255, 255, 255, 0.02);
border-radius: 8px;
display: flex;
gap: 20px;
align-items: center;
}
.authorAvatar {
width: 80px;
height: 80px;
border-radius: 50%;
overflow: hidden;
border: 3px solid rgba(255, 100, 0, 0.3);
flex-shrink: 0;
}
.authorAvatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.authorInfo {
flex-grow: 1;
}
.authorName {
font-size: 1.2rem;
margin-bottom: 10px;
}
.authorBio {
color: $text-ash;
font-size: 0.9rem;
line-height: 1.6;
}
.authorSocial {
display: flex;
gap: 15px;
margin-top: 15px;
}
.authorSocial a {
color: $text-ash;
transition: all 0.3s ease;
}
.authorSocial a:hover {
color: $accent-fire;
}
.commentsSection {
margin-top: 60px;
}
.commentsTitle {
font-size: 1.5rem;
margin-bottom: 30px;
position: relative;
display: inline-block;
}
.commentsTitle::after {
content: "";
position: absolute;
width: 40px;
height: 2px;
background: $accent-fire;
bottom: -10px;
left: 0;
}
.comment {
padding: 20px 0;
border-bottom: 1px solid $card-border;
display: flex;
gap: 20px;
}
.commentAvatar {
width: 50px;
height: 50px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
}
.commentAvatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.commentContent {
flex-grow: 1;
}
.commentAuthor {
font-weight: 600;
margin-bottom: 5px;
}
.commentDate {
font-size: 0.8rem;
color: $text-ash;
margin-bottom: 10px;
}
.commentText {
color: #e0e0e0;
line-height: 1.6;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.commentReply {
margin-top: 10px;
color: $accent-fire;
text-decoration: none;
font-size: 0.9rem;
display: inline-block;
}
.commentForm {
margin-top: 40px;
}
.formTitle {
font-size: 1.3rem;
margin-bottom: 20px;
}
.formGroup {
margin-bottom: 20px;
}
.formControl {
width: 100%;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid $card-border;
border-radius: 4px;
padding: 12px 15px;
color: $text-light !important;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
resize: vertical;
transition: all 0.3s ease;
}
.formControl:focus {
outline: none;
border-color: $accent-fire;
box-shadow: 0 0 0 2px rgba(255, 100, 0, 0.2);
}
.formRow {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.submitButton {
background-color: $accent-fire;
color: $text-light;
border: none;
border-radius: 4px;
padding: 12px 25px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
font-family: 'Cinzel', serif;
letter-spacing: 1px;
}
.submitButton:hover {
background-color: $accent-dragon;
transform: translateY(-2px);
}
.postSidebar {
position: sticky;
top: 120px;
align-self: start;
}
.sidebarWidget {
background-color: $card-bg;
border-radius: 8px;
padding: 25px;
margin-bottom: 30px;
border: 1px solid $card-border;
}
.widgetTitle {
font-size: 1.2rem;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
}
.widgetTitle::after {
content: "";
position: absolute;
width: 40px;
height: 2px;
background: $accent-fire;
bottom: -1px;
left: 0;
}
.tocList {
list-style: none;
}
.tocItem {
margin-bottom: 12px;
padding-left: 15px;
position: relative;
}
.tocItem::before {
content: "";
position: absolute;
left: 0;
top: 10px;
width: 5px;
height: 5px;
border-radius: 50%;
background-color: $accent-fire;
}
.tocLink {
color: $text-ash;
text-decoration: none;
transition: all 0.3s ease;
font-size: 0.9rem;
line-height: 1.5;
display: block;
}
.tocLink:hover {
color: $accent-fire;
transform: translateX(5px);
}
.tocSubitem {
margin-top: 8px;
padding-left: 15px;
}
.relatedPostsList {
list-style: none;
}
.relatedPostItem {
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.relatedPostItem:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.relatedPostLink {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: $text-ash;
transition: color 0.3s ease;
}
.relatedPostLink:hover {
color: $accent-fire;
}
.relatedPostThumb {
width: 60px;
height: 60px;
border-radius: 4px;
background-size: cover;
background-position: center;
flex-shrink: 0;
}
.relatedPostInfo {
flex-grow: 1;
}
.relatedPostTitle {
font-size: 0.9rem;
line-height: 1.4;
margin-bottom: 5px;
}
.relatedPostDate {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.5);
}
.footer {
background-color: $primary-dark;
padding: 50px 5% 30px;
text-align: center;
position: relative;
border-top: 1px solid rgba(255, 100, 0, 0.2);
margin-top: 80px;
}
.footerContent {
max-width: 800px;
margin: 0 auto;
}
.socialLinks {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 30px;
}
.socialIcon {
font-size: 1.5rem;
color: $text-ash;
transition: all 0.3s ease;
}
.socialIcon:hover {
color: $accent-fire;
transform: translateY(-5px);
}
.copyright {
color: $text-ash;
font-size: 0.9rem;
margin-top: 20px;
}
.binaryLine {
position: absolute;
font-family: 'Fira Code', monospace;
color: $accent-fire;
font-size: 0.8rem;
opacity: 0.15;
pointer-events: none;
}
@media (max-width: 992px) {
.postContainer {
grid-template-columns: 1fr;
}
.postSidebar {
position: static;
order: -1;
}
.postFeaturedImage {
height: 300px;
}
.postTitle {
font-size: 2rem;
}
.postInfo,
.postContent {
padding: 20px;
}
}
@media (max-width: 768px) {
.navbar {
flex-direction: column;
gap: 15px;
}
.navLinks {
width: 100%;
justify-content: space-around;
font-size: 0.9rem;
}
.postFeaturedImage {
height: 250px;
}
.postTitle {
font-size: 1.8rem;
}
.formRow {
grid-template-columns: 1fr;
gap: 15px;
}
.authorSection {
flex-direction: column;
text-align: center;
align-items: center;
}
.authorSocial {
justify-content: center;
}
}
@media (max-width: 480px) {
.navLinks {
flex-wrap: wrap;
gap: 10px;
}
.postFeaturedImage {
height: 200px;
}
.postTitle {
font-size: 1.5rem;
}
.postMeta {
gap: 10px;
}
.comment {
flex-direction: column;
align-items: flex-start;
}
.commentAvatar {
margin-bottom: 10px;
}
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: $primary-dark;
}
::-webkit-scrollbar-thumb {
background: $card-border;
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: $accent-fire;
}
/* Code line numbers */
.hljsLnNumbers {
user-select: none;
text-align: center;
color: rgba(255, 255, 255, 0.3);
border-right: 1px solid rgba(255, 255, 255, 0.1);
vertical-align: top;
padding-right: 10px !important;
}
.hljsLnCode {
padding-left: 10px !important;
}
/* Tooltips */
.tooltip {
position: relative;
display: inline-block;
color: $accent-fire;
text-decoration: underline;
text-decoration-style: dotted;
cursor: help;
}
.tooltip .tooltipText {
visibility: hidden;
width: 200px;
background-color: $card-bg;
color: $text-light;
text-align: center;
border-radius: 6px;
padding: 10px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.3s;
border: 1px solid $card-border;
font-size: 0.8rem;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.tooltip .tooltipText::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: $card-border transparent transparent transparent;
}
.tooltip:hover .tooltipText {
visibility: visible;
opacity: 1;
}
+230
View File
@@ -0,0 +1,230 @@
@use './variables' as *;
@use './card.scss' as card;
@include card.card;
/* Card styling */
.contactCard {
position: relative;
border-left: 3px solid $accent-fire;
padding-left: 1.5rem;
}
.contactIcon {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: rgba(255, 100, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
color: $accent-fire;
font-size: 1.5rem;
margin-bottom: 1rem;
}
.contactTitle {
font-size: 1.3rem;
color: $text-light;
margin-bottom: 1rem;
}
.contactContent {
color: $text-ash;
line-height: 1.6;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.contactLink {
color: $accent-fire;
text-decoration: none;
transition: all 0.3s ease;
}
.contactLink:hover {
text-decoration: underline;
color: $accent-ember;
}
/* Button styling */
.btnPrimary {
background: linear-gradient(45deg, $accent-dragon, $accent-fire);
border: none;
padding: 0.75rem 2rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
box-shadow: 0 4px 15px rgba(139, 0, 0, 0.3);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.btnPrimary:hover {
transform: translateY(-3px);
box-shadow: 0 7px 25px rgba(139, 0, 0, 0.5);
background-color: $accent-fire;
background-image: none;
}
.btnPrimary:focus {
box-shadow: 0 0 0 0.25rem rgba(255, 100, 0, 0.4);
}
.btnPrimary::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: 0.6s;
}
.btnPrimary:hover::before {
left: 100%;
}
.btnPrimary:disabled {
background: linear-gradient(45deg, rgba(139, 0, 0, 0.5), rgba(255, 100, 0, 0.5));
transform: none;
}
/* Social links styling */
.socialGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 15px;
margin-top: 1.5rem;
}
.socialItem {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 10px;
padding: 15px;
border-radius: 8px;
background-color: rgba(255, 255, 255, 0.05);
transition: all 0.3s ease;
text-decoration: none;
}
.socialItem:hover {
background-color: rgba(255, 100, 0, 0.1);
transform: translateY(-5px);
}
.socialIconLg {
font-size: 1.8rem;
color: $accent-fire;
}
.socialName {
color: $text-ash;
font-size: 0.9rem;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* Map styling */
.mapContainer {
position: relative;
overflow: hidden;
border-radius: 8px;
border: 1px solid $card-border;
margin-bottom: 2rem;
}
.mapContainer::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 5px;
background: linear-gradient(to right, $accent-dragon, $accent-fire);
z-index: 1;
}
.mapFrame {
width: 100%;
height: 400px;
border: none;
background-color: $card-bg;
filter: grayscale(90%) invert(92%) hue-rotate(180deg);
}
/* Binary background effect */
.binaryLine {
position: absolute;
font-family: 'Fira Code', monospace;
color: $accent-fire;
font-size: 0.8rem;
opacity: 0.15;
pointer-events: none;
}
/* Ember effect */
.emberParticle {
position: absolute;
width: 3px;
height: 3px;
background-color: $accent-fire;
border-radius: 50%;
opacity: 0.6;
pointer-events: none;
z-index: 2;
box-shadow: 0 0 5px $accent-fire;
animation: float 3s linear infinite;
}
@keyframes float {
0% {
transform: translateY(0) rotate(0deg);
opacity: 0.6;
}
100% {
transform: translateY(-100px) rotate(360deg);
opacity: 0;
}
}
/* Table styling */
.table {
color: $text-ash;
margin-bottom: 0;
}
.table td {
padding: 0.5rem 0.25rem;
border-color: rgba(255, 255, 255, 0.05);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
@media (max-width: 768px) {
.schedule-container {
overflow-x: auto;
}
.social-grid {
grid-template-columns: repeat(3, 1fr);
}
.map-frame {
height: 300px;
}
}
@media (max-width: 576px) {
.social-grid {
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.form-floating>label {
padding: 0.75rem 0.75rem;
}
}
+227
View File
@@ -0,0 +1,227 @@
.error-container {
position: relative;
z-index: 2;
padding-top: 150px;
padding-bottom: 100px;
}
.error-code {
font-size: 8rem;
font-weight: 700;
background: linear-gradient(to right, var(--accent-fire), var(--text-light));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
margin-bottom: 1rem;
text-shadow: var(--glow-effect);
position: relative;
}
.error-title {
font-size: 2rem;
margin-bottom: 1.5rem;
color: var(--text-light);
}
.error-text {
font-size: 1.1rem;
color: var(--text-ash);
margin-bottom: 2rem;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.broken-sword {
font-size: 5rem;
color: var(--accent-fire);
margin-bottom: 2rem;
position: relative;
z-index: 1;
}
.broken-sword::after {
content: "";
position: absolute;
width: 40px;
height: 4px;
background: var(--accent-fire);
transform: rotate(-45deg);
top: 60%;
left: 45%;
box-shadow: var(--glow-effect);
}
.home-button {
background: linear-gradient(45deg, var(--accent-dragon), var(--accent-fire));
border: none;
padding: 0.75rem 2rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
box-shadow: 0 4px 15px rgba(139, 0, 0, 0.3);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
color: var(--text-light);
text-decoration: none;
border-radius: 4px;
display: inline-flex;
align-items: center;
gap: 10px;
}
.home-button:hover {
transform: translateY(-3px);
box-shadow: 0 7px 25px rgba(139, 0, 0, 0.5);
color: var(--text-light);
}
.home-button::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: 0.6s;
}
.home-button:hover::before {
left: 100%;
}
.error-code-container {
position: relative;
display: inline-block;
}
.error-code-container::before,
.error-code-container::after {
content: "<";
position: absolute;
color: var(--accent-fire);
opacity: 0.6;
font-size: 6rem;
font-family: 'Fira Code', monospace;
}
.error-code-container::before {
left: -4rem;
}
.error-code-container::after {
content: "/>";
right: -6rem;
}
/* Media Queries */
@media (max-width: 992px) {
.timeline:before {
left: 2rem;
}
.timeline-item:nth-child(odd) .timeline-content,
.timeline-item:nth-child(even) .timeline-content {
width: calc(100% - 3rem);
margin-left: 3rem;
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.timeline-dot {
left: 2rem;
}
.post-container {
grid-template-columns: 1fr;
}
.post-sidebar {
position: static;
order: -1;
}
.post-featured-image {
height: 300px;
}
.post-title {
font-size: 2rem;
}
.post-info, .post-content {
padding: 20px;
}
}
@media (max-width: 768px) {
.intro-text {
margin-top: 2rem;
}
.testimonial-author {
flex-direction: column;
align-items: center;
text-align: center;
}
.author-info {
margin-top: 1rem;
text-align: center;
}
.error-code {
font-size: 6rem;
}
.error-title {
font-size: 1.8rem;
}
.broken-sword {
font-size: 4rem;
}
.error-code-container::before,
.error-code-container::after {
font-size: 4rem;
}
.error-code-container::before {
left: -3rem;
}
.error-code-container::after {
right: -4.5rem;
}
}
@media (max-width: 576px) {
.error-code {
font-size: 4rem;
}
.error-title {
font-size: 1.5rem;
}
.broken-sword {
font-size: 3rem;
}
.error-code-container::before,
.error-code-container::after {
font-size: 3rem;
}
.error-code-container::before {
left: -2rem;
}
.error-code-container::after {
right: -3rem;
}
}
+95
View File
@@ -0,0 +1,95 @@
@use './variables.scss' as *;
/* Form styling */
.form-control {
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid var(--card-border);
color: var(--text-light);
padding: 0.75rem 0.9375rem;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.form-control:focus {
background-color: rgba(255, 255, 255, 0.08);
border-color: var(--accent-fire);
box-shadow: 0 0 0 0.25rem rgba(255, 100, 0, 0.25);
color: var(--text-light);
}
.form-control::placeholder {
color: rgba(255, 255, 255, 0.3);
}
textarea.form-control {
min-height: 120px;
}
.form-label {
color: var(--text-ash);
font-weight: 600;
margin-bottom: 0.5rem;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.form-text {
color: rgba(255, 255, 255, 0.5);
}
.form-select {
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid var(--card-border);
color: var(--text-light);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ff6400' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.form-select:focus {
background-color: rgba(255, 255, 255, 0.08);
border-color: var(--accent-fire);
box-shadow: 0 0 0 0.25rem rgba(255, 100, 0, 0.25);
color: var(--text-light);
}
.form-select option {
background-color: var(--card-bg);
color: var(--text-light);
}
/* Alert styling */
.alert-success {
background-color: rgba(25, 135, 84, 0.1);
border-color: rgba(25, 135, 84, 0.3);
color: #75b798;
}
.alert-danger {
background-color: rgba(220, 53, 69, 0.1);
border-color: rgba(220, 53, 69, 0.3);
color: #ea868f;
}
/* Form validation styling */
.was-validated .form-control:invalid,
.form-control.is-invalid {
border-color: #dc3545;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.was-validated .form-control:valid,
.form-control.is-valid {
border-color: #198754;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.invalid-feedback {
color: #dc3545;
font-size: 0.875em;
margin-top: 0.25rem;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
+230
View File
@@ -0,0 +1,230 @@
@use './variables' as *;
/* Skills Section */
.skillsSection {
padding: 6rem 0;
background-color: $secondary-dark;
position: relative;
}
/* Hero Section */
.hero {
height: 100vh;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
padding-top: 76px;
}
.hero::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: url('/api/placeholder/1200/800') center/cover no-repeat;
opacity: 0.15;
filter: brightness(0.5) contrast(1.2);
z-index: -1;
}
.titleMain {
font-size: 4rem;
font-weight: 700;
position: relative;
display: inline-block;
}
.titleMain::before {
content: "< ";
color: $accent-fire;
opacity: 0.8;
}
.titleMain::after {
content: " />";
color: $accent-fire;
opacity: 0.8;
}
.subtitle {
font-size: 1.6rem;
color: $text-ash;
margin-bottom: 2rem;
}
/* Binary Background */
.binaryBg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
opacity: 0.07;
overflow: hidden;
}
.binaryLine {
position: absolute;
font-family: 'Fira Code', monospace;
color: $accent-fire;
font-size: 1rem;
animation: fall linear infinite;
opacity: 0.9;
}
@keyframes fall {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(100vh);
}
}
/* Button Styling */
.btnPrimary {
background: linear-gradient(45deg, $accent-dragon, $accent-fire);
border: none;
padding: 0.75rem 2rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
box-shadow: 0 4px 15px rgba(139, 0, 0, 0.3);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.btnPrimary:hover {
transform: translateY(-3px);
box-shadow: 0 7px 25px rgba(139, 0, 0, 0.5);
background-color: $accent-fire;
background-image: none;
}
.btnPrimary::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: all 0.6s ease;
}
.btnPrimary:hover::before {
left: 100%;
}
/* Blinking cursor effect */
.blinkingCursor {
display: inline-block;
width: 10px;
height: 24px;
background-color: $accent-fire;
margin-left: 5px;
animation: blink 1s step-end infinite;
box-shadow: $glow-effect;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
/* Particles effect */
.particles {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
}
.particle {
position: absolute;
width: 3px;
height: 3px;
background-color: $accent-fire;
border-radius: 50%;
opacity: 0;
animation: rise linear forwards;
}
@keyframes rise {
0% {
transform: translateY(0) translateX(0);
opacity: 0;
}
10% {
opacity: 0.5;
}
90% {
opacity: 0.5;
}
100% {
transform: translateY(-100px) translateX(var(--x));
opacity: 0;
}
}
// Style for phone
@media (max-width: 992px) {
.title-main {
font-size: 3rem;
}
}
@media (max-width: 768px) {
.hero {
height: 100vh;
padding-top: 50px;
}
.titleMain {
font-size: 2rem;
}
.subtitle {
font-size: 1.4rem;
}
.btnPrimary {
padding: 0.5rem 1.5rem;
font-size: 1.2rem;
}
}
@media (max-width: 576px) {
.title-main {
font-size: 2rem;
}
.subtitle {
font-size: 1rem;
}
.btn-primary {
padding: 0.6rem 1.5rem;
font-size: 0.9rem;
}
}
+58
View File
@@ -0,0 +1,58 @@
@use "./variables.scss" as *;
/* Binary background effect */
.binaryLine {
position: absolute;
font-family: 'Fira Code', monospace;
color: $accent-fire;
font-size: 0.8rem;
opacity: 0.15;
pointer-events: none;
}
/* Page Header */
.pageHeader {
padding-top: 7rem;
padding-bottom: 3rem;
background-color: $secondary-dark;
position: relative;
overflow: hidden;
}
.pageHeader::before {
content: "";
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-image: url('/api/placeholder/1200/400');
opacity: 0.15;
filter: brightness(0.3);
z-index: 0;
}
.pageTitle {
font-size: 3rem;
margin-bottom: 1rem;
position: relative;
display: inline-block;
}
.pageTitle::after {
content: "";
position: absolute;
width: 60px;
height: 3px;
background: $accent-fire;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
box-shadow: $glow-effect;
}
.pageSubtitle {
font-size: 1.2rem;
color: $text-ash;
}
+378
View File
@@ -0,0 +1,378 @@
@use './variables' as *;
.filterContainer {
margin-bottom: 2rem;
}
.filterButton {
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid $card-border;
color: $text-ash;
padding: 0.5rem 1rem;
margin: 0 0.25rem 0.5rem;
border-radius: 4px;
transition: all 0.3s ease;
}
.filterButton:hover,
.filterButton.active {
background-color: rgba(255, 100, 0, 0.1);
border-color: rgba(255, 100, 0, 0.3);
color: $accent-fire;
}
.filterButton.active {
border-color: $accent-fire;
}
/* Project Cards */
.card {
background-color: $card-bg;
border: 1px solid $card-border;
border-radius: 8px;
transition: all 0.3s ease;
margin-bottom: 1.5rem;
overflow: hidden;
height: 100%;
}
.card:hover {
transform: translateY(-10px);
border-color: rgba(255, 100, 0, 0.3);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
}
.cardImgTop {
height: 200px;
object-fit: cover;
}
.cardTitle {
font-size: 1.5rem;
margin-bottom: 0.75rem;
}
.cardText {
color: $text-ash;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
margin-bottom: 1.5rem;
}
.featuredCard {
position: relative;
}
.featuredCard .cardImgTop {
height: 300px;
}
/* Category badges */
.categoryBadgeContainer {
position: absolute;
top: 1rem;
right: 1rem;
display: flex;
flex-direction: row-reverse;
gap: 0.5rem;
z-index: 1;
}
.categoryBadge {
background-color: rgba(139, 0, 0, 0.9);
color: $text-light;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
}
.featuredBadge {
position: absolute;
top: 1rem;
left: 1rem;
background: linear-gradient(45deg, $accent-dragon, $accent-fire);
color: $text-light;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
display: flex;
align-items: center;
gap: 5px;
z-index: 1;
}
/* Tech stack */
.techTitle {
font-size: 0.9rem;
margin-bottom: 0.625rem;
color: $text-ash;
font-family: 'Fira Code', monospace;
}
.techStack {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 1.5rem;
}
.techItem {
display: inline-flex;
align-items: center;
gap: 6px;
background-color: rgba(255, 255, 255, 0.05);
padding: 5px 10px;
border-radius: 4px;
font-size: 0.8rem;
font-family: 'Fira Code', monospace;
color: $text-ash;
transition: all 0.3s ease;
}
.techItem:hover {
background-color: rgba(255, 100, 0, 0.1);
color: $accent-fire;
transform: translateY(-2px);
}
.techIcon {
font-size: 1rem;
color: $accent-fire;
}
/* Project links */
.btnOutlineSecondary {
background-color: rgba(255, 255, 255, 0.05);
border-color: $card-border;
color: $text-light;
}
.btnOutlineSecondary:hover {
background-color: rgba(255, 100, 0, 0.1);
border-color: $accent-fire;
color: $text-light;
}
.btnIcon {
margin-right: 0.5rem;
color: $accent-fire;
}
/* View All Button */
.btnPrimary {
background: linear-gradient(45deg, $accent-dragon, $accent-fire);
border: none;
padding: 0.75rem 2rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
box-shadow: 0 4px 15px rgba(139, 0, 0, 0.3);
transition: all 0.3s ease;
}
.btnPrimary:hover {
transform: translateY(-3px);
box-shadow: 0 7px 25px rgba(139, 0, 0, 0.5);
background-color: $accent-fire;
background-image: none;
}
/* Modal Styling */
.modalContent {
background-color: $card-bg;
border: 1px solid $card-border;
color: $text-light;
}
.modalHeader {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.modalTitle {
color: $text-light;
}
.modalFooter {
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.btnClose {
filter: invert(1) grayscale(100%) brightness(200%);
}
.featuresList {
padding-left: 1.5rem;
color: $text-ash;
}
.featuresList li {
margin-bottom: 0.5rem;
position: relative;
}
.featuresList li::before {
content: "";
color: $accent-fire;
position: absolute;
left: -1rem;
top: 0;
font-size: 1.2rem;
}
.techChip {
display: inline-flex;
align-items: center;
gap: 6px;
background-color: rgba(255, 255, 255, 0.05);
padding: 8px 15px;
border-radius: 20px;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
transition: all 0.3s ease;
}
.techChip:hover {
background-color: rgba(255, 100, 0, 0.1);
transform: translateY(-3px);
}
.techChip-icon {
color: $accent-fire;
font-size: 1.1rem;
}
.techChip-name {
color: $text-ash;
font-size: 0.9rem;
font-family: 'Fira Code', monospace;
}
.galleryItem {
height: 150px;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
cursor: pointer;
margin-bottom: 15px;
}
.galleryItem:hover {
transform: scale(1.05);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.galleryImage {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Binary background effect */
.binaryLine {
position: absolute;
font-family: 'Fira Code', monospace;
color: $accent-fire;
font-size: 0.8rem;
opacity: 0.15;
pointer-events: none;
}
/* Ember particle effect */
.emberParticle {
position: absolute;
width: 3px;
height: 3px;
background-color: $accent-fire;
border-radius: 50%;
opacity: 0.6;
pointer-events: none;
z-index: 2;
box-shadow: 0 0 5px $accent-fire;
animation: float 3s linear infinite;
}
@keyframes float {
0% {
transform: translateY(0) rotate(0deg);
opacity: 0.6;
}
100% {
transform: translateY(-100px) rotate(360deg);
opacity: 0;
}
}
/* Footer */
.footer {
background-color: $primary-dark;
padding: 3rem 0;
border-top: 1px solid rgba(255, 100, 0, 0.2);
margin-top: 3rem;
}
.socialLinks {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 1.5rem;
}
.socialIcon {
font-size: 1.5rem;
color: $text-ash;
transition: all 0.3s ease;
}
.socialIcon:hover {
color: $accent-fire;
transform: translateY(-5px);
}
.copyright {
color: $text-ash;
font-size: 0.9rem;
text-align: center;
}
/* Responsive adjustments */
@media (max-width: 992px) {
.pageTitle {
font-size: 2.5rem;
}
.featuredCard .row {
flex-direction: column;
}
}
@media (max-width: 768px) {
.pageTitle {
font-size: 2rem;
}
.techStack {
justify-content: center;
}
}
@media (max-width: 576px) {
.pageTitle {
font-size: 1.8rem;
}
.projectLinks {
flex-direction: column;
}
.projectLinks .btn {
margin-bottom: 0.5rem;
}
}
+99
View File
@@ -0,0 +1,99 @@
@use './variables' as *;
/* Skills Section */
.skillsSection {
padding: 6rem 0;
background-color: $secondary-dark;
position: relative;
}
.sectionTitle {
text-align: center;
font-size: 2.5rem;
margin-bottom: 3rem;
position: relative;
display: inline-block;
}
.sectionTitle::after {
content: "";
position: absolute;
width: 80px;
height: 3px;
background: $accent-fire;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
box-shadow: $glow-effect;
}
.card {
background-color: $card-bg;
border: 1px solid $card-border;
border-radius: 8px;
transition: all 0.3s ease;
height: 100%;
position: relative;
overflow: hidden;
}
.card:hover {
transform: translateY(-10px);
border-color: rgba(255, 100, 0, 0.3);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
.card::before {
content: "";
position: absolute;
width: 100%;
height: 5px;
top: 0;
left: 0;
background: linear-gradient(to right, $accent-dragon, $accent-fire);
}
.cardHeader {
background-color: transparent;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
gap: 10px;
}
.categoryIcon {
color: $accent-fire;
font-size: 1.5rem;
}
.categoryTitle {
font-size: 1.4rem;
margin-bottom: 0;
}
.skillItem {
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 12px;
}
.skillIcon {
color: $accent-fire;
font-size: 1.5rem;
min-width: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.skillIcon i {
display: inline-flex;
align-items: center;
justify-content: center;
}
.skillName {
color: $text-ash;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
+28
View File
@@ -0,0 +1,28 @@
// Colors
$primary-dark: #0a0a0a;
$secondary-dark: #1a1a1a;
$accent-fire: #ff6400;
$accent-ember: #ff4500;
$accent-dragon: #8B0000;
$text-light: #f5f5f5;
$text-ash: #aaaaaa;
$card-bg: #141414;
$card-border: #2a2a2a;
$code-bg: #1e1e1e;
$font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
// Effects
$glow-effect: 0 0 10px $accent-fire, 0 0 20px rgba(255, 100, 0, 0.4);
// Maps for easy access
$colors: (
"primary-dark": $primary-dark,
"secondary-dark": $secondary-dark,
"accent-fire": $accent-fire,
"accent-ember": $accent-ember,
"accent-dragon": $accent-dragon,
"text-light": $text-light,
"text-ash": $text-ash,
"card-bg": $card-bg,
"card-border": $card-border
);
+35
View File
@@ -0,0 +1,35 @@
@use './variables.scss' as *;
/* Card styling */
@mixin card {
.card {
background-color: $card-bg;
border: 1px solid $card-border;
border-radius: 8px;
transition: all 0.3s ease;
overflow: hidden;
}
.card:hover {
transform: translateY(-7px);
border-color: rgba(255, 100, 0, 0.3);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
}
.card::before {
content: "";
position: absolute;
width: 100%;
height: 5px;
top: 0;
left: 0;
background: linear-gradient(to right, $accent-dragon, $accent-fire);
z-index: 1;
}
@media (max-width: 576px) {
.card-body {
padding: 1.25rem;
}
}
}
+167
View File
@@ -0,0 +1,167 @@
@use './variables' as *;
body {
background-color: $primary-dark;
color: $text-light;
background-image: url('../images/background.png');
background-repeat: round;
background-size: 300px 300px;
background-position: 0 0;
position: relative;
overflow-x: hidden;
font-family: 'Cinzel', serif;
}
body::before {
content: "";
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at 50% 50%, rgba(139, 0, 0, 0.08), rgba(10, 10, 10, 0.4));
pointer-events: none;
z-index: -1;
}
/* Bootstrap Overrides */
.navbar {
background-color: rgba(10, 10, 10, 0.95) !important;
border-bottom: 1px solid rgba(255, 100, 0, 0.3);
}
.navbar-brand {
display: flex;
align-items: center;
gap: 12px;
}
.navbar-toggler {
border: 1px solid rgba(255, 100, 0, 0.5);
color: $accent-fire;
}
.navbar-toggler-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 100, 0, 0.8%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
}
.nav-link {
color: $text-light !important;
position: relative;
padding: 0.5rem 1rem;
transition: color 0.3s;
}
.nav-link:hover,
.nav-link.active {
color: $accent-fire !important;
}
.nav-link::after {
content: "";
position: absolute;
width: 0;
height: 2px;
bottom: 0;
left: 0;
background: linear-gradient(to right, $accent-fire, transparent);
transition: width 0.3s ease;
}
.nav-link:hover::after,
.nav-link.active::after {
width: 100%;
}
/* Logo styling */
.logo-image{
height: 2rem;
}
.logo-icon {
font-size: 2rem;
color: $accent-fire;
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
}
.logo-text {
font-weight: 700;
font-size: 1.5rem;
background: linear-gradient(to right, $accent-fire, $text-light);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
letter-spacing: 1px;
}
/* Footer */
.footer {
background-color: $primary-dark;
padding: 3rem 0;
border-top: 1px solid rgba(255, 100, 0, 0.2);
}
.social-links {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 1.5rem;
}
.social-icon {
font-size: 1.5rem;
color: $text-ash;
transition: all 0.3s ease;
}
.social-icon:hover {
color: $accent-fire;
transform: translateY(-5px);
}
.copyright {
color: $text-ash;
font-size: 0.9rem;
margin-top: 1rem;
text-align: center;
}
/* Responsive adjustments */
@media (max-width: 992px) {
.title-main {
font-size: 3rem;
}
}
@media (max-width: 768px) {
.title-main {
font-size: 2.5rem;
}
.subtitle {
font-size: 1.2rem;
}
.binary-line {
font-size: 0.8rem;
}
}
@media (max-width: 576px) {
.title-main {
font-size: 2rem;
}
.subtitle {
font-size: 1rem;
}
.btn-primary {
padding: 0.6rem 1.5rem;
font-size: 0.9rem;
}
}
+54
View File
@@ -0,0 +1,54 @@
import { Link } from 'react-router-dom';
import styles from '@styles/BlogCard.module.scss';
import type {BlogPostProps} from '../untils/BlogPostProps';
interface BlogCardProps {
post: BlogPostProps;
}
export const BlogCard: React.FC<BlogCardProps> = ({ post }) => {
return (
<div className={`card ${styles.card} ${post.featured ? styles.featuredCard : ''}`}>
<div className="position-relative">
<img src={`${import.meta.env.BASE_URL}${post.image}`} className={`card-img-top ${post.featured ? styles.featuredImg : ''}`} alt={post.title} />
<div className={styles.categoryBadge}>{post.category}</div>
{post.featured && (
<div className={`featured-badge ${styles.featuredBadge}`}>
<i className="fas fa-star"></i>
<span>Featured</span>
</div>
)}
</div>
<div className={`card-body ${styles.cardBody}`}>
<h2 className={`card-title ${styles.cardTitle}`}>
<Link to={`/blog/${post.slug}`}>{post.title}</Link>
</h2>
<div className={`d-flex meta-items mb-3`}>
<div className={`meta-item ${styles.metaItem}`}>
<i className="fas fa-calendar-alt meta-icon"></i>
<span>{post.date}</span>
</div>
<div className={`meta-item ${styles.metaItem}`}>
<i className={`fas fa-user meta-icon ${styles.metaIcon}`}></i>
<span>{post.author}</span>
</div>
<div className={`meta-item ${styles.metaItem}`}>
<i className={`fas fa-comments meta-icon ${styles.metaIcon}`}></i>
<span>{post.commentCount} Comments</span>
</div>
</div>
<p className={`card-text ${styles.cardText}`}>
{post.excerpt}
</p>
<Link to={`/blog/${post.slug}`} className={`read-more ${styles.readMore}`}>Continue Reading</Link>
<div className="mt-3">
{post.tags.map((tag, index) => (
<span key={index} className={`post-tag ${styles.postTag}`}>{tag}</span>
))}
</div>
</div>
</div>
);
}
+201
View File
@@ -0,0 +1,201 @@
import { useState, type FormEvent } from 'react';
import '@styles/FormControl.scss'
import styles from '@styles/Contact.module.scss'
export const ContactForm = () => {
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
projectType: '',
message: '',
budget: ''
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);
const [showError, ] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
// Simulate form submission
setTimeout(() => {
setIsSubmitting(false);
setShowSuccess(true);
setFormData({
name: '',
email: '',
subject: '',
projectType: '',
message: '',
budget: ''
});
// Hide success message after 5 seconds
setTimeout(() => {
setShowSuccess(false);
}, 5000);
}, 1500);
};
return (
<div className={`card h-100 ${styles.card}`}>
<div className={`card-header bg-transparent ${styles.cardHeader}`}>
<h2 className="mb-0 py-2">Send a Message to the Forge</h2>
</div>
<div className={`card-body p-4 ${styles.cardBody}`}>
{/* Success message */}
{showSuccess && (
<div className="alert alert-success" role="alert">
<i className="fas fa-check-circle me-2"></i> Your message has been successfully sent. I'll get back to you soon!
</div>
)}
{/* Error message */}
{showError && (
<div className="alert alert-danger" role="alert">
<i className="fas fa-exclamation-circle me-2"></i> There was an error sending your message. Please try again later.
</div>
)}
{/* Contact Form */}
<form id="contact-form" className="needs-validation" noValidate onSubmit={handleSubmit}>
<div className="row mb-3">
<div className="col-md-6 mb-3 mb-md-0">
<label htmlFor="name" className="form-label">Your Name</label>
<input
type="text"
className="form-control"
id="name"
name="name"
placeholder="Enter your name"
required
value={formData.name}
onChange={handleChange}
/>
<div className="invalid-feedback">
Please provide your name.
</div>
</div>
<div className="col-md-6">
<label htmlFor="email" className="form-label">Your Email</label>
<input
type="email"
className="form-control"
id="email"
name="email"
placeholder="Enter your email"
required
value={formData.email}
onChange={handleChange}
/>
<div className="invalid-feedback">
Please provide a valid email address.
</div>
</div>
</div>
<div className="mb-3">
<label htmlFor="subject" className="form-label">Subject</label>
<input
type="text"
className="form-control"
id="subject"
name="subject"
placeholder="What's this regarding?"
required
value={formData.subject}
onChange={handleChange}
/>
<div className="invalid-feedback">
Please provide a subject for your message.
</div>
</div>
<div className="mb-3">
<label htmlFor="projectType" className="form-label">Project Type</label>
<select
className="form-select"
id="projectType"
name="projectType"
required
value={formData.projectType}
onChange={handleChange}
>
<option value="" selected disabled>Select a project type</option>
<option value="embedded">Embedded Systems</option>
<option value="web">Web Development</option>
<option value="ml">Machine Learning</option>
<option value="iot">IoT Solution</option>
<option value="consulting">Technical Consulting</option>
<option value="other">Other</option>
</select>
<div className="invalid-feedback">
Please select a project type.
</div>
</div>
<div className="mb-3">
<label htmlFor="message" className="form-label">Your Message</label>
<textarea
className="form-control"
id="message"
name="message"
rows={5}
placeholder="Tell me about your project, requirements, or questions..."
required
value={formData.message}
onChange={handleChange}
></textarea>
<div className="invalid-feedback">
Please provide a message.
</div>
</div>
<div className="mb-4">
<label htmlFor="budget" className="form-label">Budget Range (Optional)</label>
<select
className="form-select"
id="budget"
name="budget"
value={formData.budget}
onChange={handleChange}
>
<option value="" selected disabled>Select your budget range</option>
<option value="small">$1,000 - $5,000</option>
<option value="medium">$5,000 - $15,000</option>
<option value="large">$15,000 - $30,000</option>
<option value="enterprise">$30,000+</option>
<option value="undecided">Not sure yet</option>
</select>
</div>
<div className="text-center">
<button
type="submit"
className={`btn btn-primary position-relative ${styles.btnPrimary}`}
id="submit-button"
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
Sending...
</>
) : (
<>
<i className="fas fa-paper-plane me-2"></i> Send Message
</>
)}
</button>
</div>
</form>
</div>
</div>
);
}
+23
View File
@@ -0,0 +1,23 @@
import {socialLinkData} from '../data/socialLinkData'
import type {SocialLink} from '../untils/SocialLink'
export const Footer = () => {
const socialLinks: SocialLink[] = socialLinkData;
const currentYear = new Date().getFullYear()
return (
<footer className="footer">
<div className="container">
<div className="social-links">
{socialLinks.map((link, index) => (
<a key={index} href={link.url} className="social-icon">
<i className={link.icon}></i>
</a>
))}
</div>
<p className="copyright">&copy; {currentYear} SecCodeSmith. All rights forged in digital fire.</p>
</div>
</footer>
)
}
+82
View File
@@ -0,0 +1,82 @@
import { useState, useEffect } from 'react'
import { NavLink, useLocation } from 'react-router-dom'
import logo from '../assets/images/favicon.png'
export const Header = () => {
const location = useLocation()
const [isNavCollapsed, setIsNavCollapsed] = useState(true)
// Close the mobile menu when changing routes
useEffect(() => {
setIsNavCollapsed(true)
}, [location])
return (
<nav className="navbar navbar-expand-lg fixed-top">
<div className="container">
<NavLink className="navbar-brand" to="/">
<div className="logo-icon">
<img src={logo} alt="Logo" className='logo-image' height="100%" />
</div>
<div className="logo-text">SecCodeSmith</div>
</NavLink>
<button
className="navbar-toggler"
type="button"
onClick={() => setIsNavCollapsed(!isNavCollapsed)}
aria-controls="navbarNav"
aria-expanded={!isNavCollapsed}
aria-label="Toggle navigation"
>
<span className="navbar-toggler-icon"></span>
</button>
<div className={`${isNavCollapsed ? 'collapse' : ''} navbar-collapse justify-content-end`} id="navbarNav">
<ul className="navbar-nav">
<li className="nav-item">
<NavLink
className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`}
to="/"
>
Home
</NavLink>
</li>
<li className="nav-item">
<NavLink
className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`}
to="/blog"
>
Blog
</NavLink>
</li>
<li className="nav-item">
<NavLink
className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`}
to="/about"
>
About
</NavLink>
</li>
<li className="nav-item">
<NavLink
className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`}
to="/projects"
>
Projects
</NavLink>
</li>
<li className="nav-item">
<NavLink
className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`}
to="/contact"
>
Contact
</NavLink>
</li>
</ul>
</div>
</div>
</nav>
)
}
+15
View File
@@ -0,0 +1,15 @@
import { Outlet } from 'react-router-dom'
import { Header } from './Header'
import { Footer } from './Footer'
export const Layout = () => {
return (
<>
<Header />
<main>
<Outlet />
</main>
<Footer />
</>
)
}
+25
View File
@@ -0,0 +1,25 @@
import style from '@styles/PageHeader.module.scss'
interface PageHeaderProps {
title: string;
subtitle?: string;
}
export const PageHeader: React.FC<PageHeaderProps> = ({ title, subtitle }) => {
return (
<section className={`text-center ${style.pageHeader}`}>
<div className={`container position-relative`}>
<h1 className={style.pageTitle}>{title}</h1>
{subtitle && <p className={`mt-4 ${style.pageSubtitle}`}>{subtitle}</p>}
{/* Binary background effect */}
<div className="binary-bg">
<div className={style.binaryLine} style={{ top: '20%', left: '10%' }}>01001100 01101111 01110010 01100101 01101101</div>
<div className={style.binaryLine} style={{ bottom: '30%', right: '15%' }}>01001001 01110000 01110011 01110101 01101101</div>
<div className={style.binaryLine} style={{ top: '50%', left: '80%' }}>01000100 01101111 01101100 01101111 01110010</div>
<div className={style.binaryLine} style={{ bottom: '10%', left: '40%' }}>01010011 01101001 01110100 00100000 01000001 01101101 01100101 01110100</div>
</div>
</div>
</section>
)
}
+141
View File
@@ -0,0 +1,141 @@
import React, { useEffect, useState } from 'react'
import style from '@styles/Project.module.scss'
import type {ProjectTech, Category, ProjectProps } from '../untils/ProjectProps'
interface ProjectCardProps {
project: ProjectProps;
onOpenDetails: (id: string) => void;
}
const ProjectCardTech: React.FC<{ technology: ProjectTech[] }> = ({ technology }) => (
<div>
<h4 className={style.techTitle}>// TECHNOLOGIES_USED</h4>
<div className={style.techStack}>
{technology.map((tech, index) => (
<span className={`${style.techItem}`} key={index}>
<i className={`${tech.icon} ${style.techIcon}`}></i>
<span>{tech.name}</span>
</span>
))}
</div>
</div>
)
const CategoryBadge: React.FC<{ category: Category[] }> = ({ category }) => (
< div className={style.categoryBadgeContainer} >
{
category.map((cat, index) => (
<span key={index} className={`${style.categoryBadge}`}>
{cat.shortName}
</span>
))}
</div>
)
const ProjectLinks: React.FC<{ links: { github?: string; demo?: string }, id: string, onOpenDetails: (id: string) => void }> = ({ links, id, onOpenDetails }) => (
<div className={`mt-auto d-flex gap-2 flex-wrap project-links ${style.projectLinks}`}>
<button
className={`btn btn-outline-secondary ${style.btnOutlineSecondary}`}
onClick={() => onOpenDetails(id)}
data-bs-toggle="modal" data-bs-target="#shadowguardianModal">
<i className={`fas fa-info-circle ${style.btnIcon}`} ></i>
Project Details
</button>
{links.github && (
<a href={links.github} className={`btn btn-outline-secondary ${style.btnOutlineSecondary} `}>
<i className={`fab fa-github ${style.btnIcon}`} ></i>
View Code
</a>
)}
{links.demo && (
<a href={links.demo} className={`btn btn-outline-secondary ${style.btnOutlineSecondary}`}>
<i className={`fas fa-external-link-alt ${style.btnIcon}`}></i>
Live Demo
</a>
)}
</div>
)
export const ProjectCard: React.FC<ProjectCardProps> = ({ project, onOpenDetails }) => {
const [, setHovered] = useState(false);
useEffect(() => {
const createEmbers = () => {
const embersContainer = document.querySelectorAll(`.${style.cardImgTop}`);
embersContainer.forEach((container) => {
for (let i = 0; i < 5; i++) {
const ember = document.createElement('div');
ember.className = style.emberParticle;
ember.style.left = `${Math.random() * 100}%`;
ember.style.bottom = '0';
ember.style.animationDuration = `${Math.random() * 2 + 3}s`;
ember.style.animationDelay = `${Math.random() * 3}s`;
container.appendChild(ember);
}
});
};
createEmbers();
}, []);
if (project.featured) {
return (
<div className={`"col-12 mb-4`}>
<div
className={`card ${style.card} ${style.featuredCard}`}
data-category={project.category.join(',')}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<div className={`row g-0`}>
<div className={`col-md-6 position-relative`}>
<img src={project.image} className={`img-fluid rounded-start h-100 w-100 object-fit-cover`} alt={project.title} />
<div className={style.featuredBadge}>
<i className="fas fa-star"></i>
<span>Featured</span>
</div>
<CategoryBadge category={project.category} />
</div>
<div className={`col-md-6`}>
<div className={`card-body d-flex flex-column h-100 ${style.cardBody}`}>
<h3 className={`${style.cardTitle} card-title`}>{project.title}</h3>
<p className={`${style.cardText} card-text`}>
{project.description}
</p>
<ProjectCardTech technology={project.technologies} />
<ProjectLinks links={project.links} id={project.id} onOpenDetails={onOpenDetails} />
</div>
</div>
</div>
</div>
</div>
)
}
return (
<div className={`col-md-6 col-lg-4 mb-4`}>
<div className={`card ${style.card} h-100`}
data-category={project.category.join(',')}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}>
<div className={`position-relative`}>
<img src={`${import.meta.env.BASE_URL}${project.image}`} className={style.cardImgTop} alt={project.title} />
<CategoryBadge category={project.category} />
</div>
<div className={`card-body d-flex flex-column ${style.cardBody}`}>
<h3 className={`${style.cardTitle} card-title`}>{project.title}</h3>
<p className={`${style.cardText} card-text`}>
{project.description}
</p>
<ProjectCardTech technology={project.technologies} />
<ProjectLinks links={project.links} id={project.id} onOpenDetails={onOpenDetails} />
</div>
</div>
</div>
)
};
+159
View File
@@ -0,0 +1,159 @@
import type { ProjectProps } from '../untils/ProjectProps';
import style from '@styles/Project.module.scss'
interface ProjectModalProps {
project: ProjectProps | null;
}
export const ProjectModal: React.FC<ProjectModalProps> = ({ project }) => {
if (!project) return null;
return (
<div className="modal fade" id="shadowguardianModal" tabIndex={-1} aria-labelledby="shadowguardianModalLabel" aria-hidden="true">
<div className="modal-dialog modal-xl modal-dialog-scrollable">
<div className={`modal-content ${style.modalContent}`}>
<div className={`modal-header ${style.modalHeader}`}>
<h5 className={style.modalHeader} id="shadowguardianModalLabel">
{project.title}
</h5>
<button type="button" className={`btn-close ${style.btnClose}`} data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div className={`modal-body ${style.modalBody}`}>
<div className="mb-4">
<img src={project.image} className="img-fluid rounded mb-3" alt={project.title} />
<div className={`row ${style.row}`}>
<div className="col-md-8">
{
project.projectDetails ? project.projectDetails.descriptions?.map((desc, index) => (
<p key={index} className="mb-3">{desc}</p>
)) :
<p>{project.description}</p>
}
</div>
<div className="col-md-4">
<div className={`card h-100 ${style.card}`}>
<div className={`card-header ${style.cardHeader}`}>
<h5 className="mb-0">Project Overview</h5>
</div>
<div className="card-body">
{project.projectDetails && project.projectDetails.startDate && project.projectDetails.endDate ? (
<p><strong>Duration:</strong> {new Date(project.projectDetails.startDate).toLocaleDateString()} - {new Date(project.projectDetails.endDate).toLocaleDateString()}</p>
) : (
<p><strong>Duration:</strong> Ongoing</p>
)
}
{
project.projectDetails && project.projectDetails.role ? (
<p><strong>Role:</strong> {project.projectDetails.role}</p>
) : (
<p><strong>Role:</strong> Developer</p>
)
}
{
project.projectDetails && project.projectDetails.client ? (
<p><strong>Client:</strong> {project.projectDetails.client}</p>
) : (
<p><strong>Client:</strong> Internal Project</p>
)
}
</div>
</div>
</div>
</div>
{
project.projectDetails?.keyFeatures && project.projectDetails.keyFeatures.length > 0 && (
<>
<h4 className="mt-4 mb-3">Key Features</h4>
<ul className={style.featuresList}>
{
project.projectDetails?.keyFeatures.map((feature, index) => (
<li key={index}>
{feature}
</li>
))
}
</ul>
</>
)
}
<h4 className="mt-4 mb-3">Technologies Used</h4>
<div className="mb-4"></div>
{
project.projectDetails?.fullTechStack && project.projectDetails?.fullTechStack?.length > 0 ? (
<>
{project.projectDetails.fullTechStack.map((tech, index) => (
<span key={index} className={`tech-chip ${style.techChip}`}>
<i className={`${tech.icons} tech-chip-icon`}></i>
<span className="tech-chip-name">{tech.name}</span>
</span>
))}
</>
) : (
<>
{project.technologies && project.technologies.length > 0 ? (
project.technologies.map((tech, index) => (
<span key={index} className={`tech-chip ${style.techChip}`}>
<i className={`${tech.icon} tech-chip-icon`}></i>
<span className="tech-chip-name">{tech.name}</span>
</span>
))
) : (
<p className="text-muted">No technologies listed.</p>
)
}
</>
)
}
</div>
{
project.projectDetails?.gallery && project.projectDetails?.gallery.length > 0 && (
<>
<h4 className="mt-4 mb-3">Gallery</h4>
<div className="row">
{
project.projectDetails.gallery.map((image, index) => (
<div className="col-md-4 col-6">
<div key={index} className={`gallery-item ${style.galleryItem}`}>
<img src={image} alt={`Gallery Image ${index + 1}`} className={`gallery-image ${style.galleryImage}`}></img>
</div>
</div>
))
}
</div>
</>
)
}
</div>
<div className={`modal-footer ${style.modalFooter}`}>
{project.links.github && (
<a href={project.links.github} className={`btn btn-outline-secondary ${style.btnOutlineSecondary}`} target="_blank" rel="noopener noreferrer">
<i className="fab fa-github btn-icon"></i>Source Code
</a>
)}
{project.links.demo && (
<a href={project.links.demo} className={`btn btn-outline-secondary ${style.btnOutlineSecondary}`} target="_blank" rel="noopener noreferrer">
<i className="fas fa-external-link-alt btn-icon"></i>Live Demo
</a>
)}
{project.links.documentation && (
<a href={project.links.documentation} className={`btn btn-outline-secondary ${style.btnOutlineSecondary}`} target="_blank" rel="noopener noreferrer">
<i className="fas fa-book btn-icon"></i>Documentation
</a>
)}
<button type="button" className={`btn btn-secondary`} data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
);
};
+33
View File
@@ -0,0 +1,33 @@
import styles from '@styles/SkillCard.module.scss';
import React from 'react';
import type {SkillCardProps} from '../untils/SkillCardProps';
const SkillCard: React.FC<SkillCardProps> = ({ categoryTitle, categoryIcon, skills }) => {
return (
<div className="col-md-6 col-lg-4">
<div className={`card h-100 ${styles.card}`}>
<div className={`card-header ${styles.cardHeader}`}>
<i className={`fas ${categoryIcon} category-icon ${styles.categoryIcon}`}></i>
<h3 className={`${styles.categoryTitle}`}>{categoryTitle}</h3>
</div>
<div className="card-body">
{skills.length > 0 ? (
<ul className="list-unstyled mb-0">
{skills.map((skill, index) => (
<li key={index} className={styles.skillItem}>
<span className={styles.skillIcon}><i className={`${skill.icon ?? ""}`}></i></span>
<span className={styles.skillName}>{skill.name}</span>
</li>
))}
</ul>
) : (
<p>No skills listed in this category.</p>
)}
</div>
</div>
</div>
);
};
export default SkillCard;
+638
View File
@@ -0,0 +1,638 @@
/* Skills card */
.skill-card {
background-color: var(--card-bg);
border-radius: 8px;
padding: 1.875rem;
border: 1px solid var(--card-border);
transition: all 0.3s ease;
position: relative;
height: 100%;
}
.skill-card:hover {
transform: translateY(-10px);
border-color: rgba(255, 100, 0, 0.3);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
}
.skill-card::before {
content: "";
position: absolute;
width: 100%;
height: 5px;
top: 0;
left: 0;
background: linear-gradient(to right, var(--accent-dragon), var(--accent-fire));
border-radius: 8px 8px 0 0;
}
.skill-header {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 1.25rem;
padding-bottom: 0.625rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.skill-icon {
font-size: 2rem;
color: var(--accent-fire);
}
.skill-title {
font-size: 1.3rem;
margin-bottom: 0;
color: var(--text-light);
}
.skill-check {
color: var(--accent-fire);
margin-right: 0.625rem;
}
/* Timeline */
.timeline {
position: relative;
padding: 0;
margin-top: 3rem;
list-style: none;
}
.timeline:before {
content: "";
position: absolute;
width: 3px;
height: 100%;
background: linear-gradient(to bottom, var(--accent-fire), var(--accent-dragon), rgba(255, 100, 0, 0.2));
left: 50%;
transform: translateX(-50%);
z-index: 1;
}
.timeline-item {
position: relative;
margin-bottom: 3rem;
width: 100%;
}
.timeline-item:nth-child(odd) .timeline-content {
margin-left: auto;
padding-left: 3rem;
border-radius: 8px;
}
.timeline-item:nth-child(even) .timeline-content {
margin-right: auto;
padding-right: 3rem;
border-radius: 8px;
}
.timeline-dot {
width: 16px;
height: 16px;
background-color: var(--accent-fire);
border-radius: 50%;
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
box-shadow: var(--glow-effect);
z-index: 2;
}
.timeline-date {
display: inline-block;
font-size: 0.9rem;
background-color: rgba(255, 100, 0, 0.1);
color: var(--accent-fire);
padding: 5px 10px;
border-radius: 4px;
margin-bottom: 1rem;
font-weight: 600;
letter-spacing: 1px;
}
.timeline-title {
font-size: 1.3rem;
margin-bottom: 0.625rem;
color: var(--text-light);
}
.timeline-text {
color: var(--text-ash);
line-height: 1.6;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.timeline-content {
background-color: var(--card-bg);
border-radius: 8px;
padding: 1.5rem;
border: 1px solid var(--card-border);
position: relative;
transition: all 0.3s ease;
width: 45%;
}
.timeline-content:hover {
transform: translateY(-5px);
border-color: rgba(255, 100, 0, 0.3);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
}
/* Testimonials */
.testimonial {
background-color: var(--card-bg);
border-radius: 8px;
padding: 1.875rem;
margin-bottom: 1.875rem;
border: 1px solid var(--card-border);
transition: all 0.3s ease;
position: relative;
}
.testimonial:hover {
transform: translateY(-5px);
border-color: rgba(255, 100, 0, 0.3);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
}
.testimonial-content {
padding-left: 1.25rem;
border-left: 3px solid var(--accent-fire);
font-style: italic;
color: var(--text-ash);
margin-bottom: 1.25rem;
line-height: 1.7;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.author-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
overflow: hidden;
}
.author-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.author-name {
font-weight: 600;
font-size: 1.1rem;
color: var(--text-light);
margin-bottom: 0;
}
.author-title {
color: var(--text-ash);
font-size: 0.9rem;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* Blog post specific styles */
.post-container {
max-width: 1200px;
margin: 0 auto;
padding: 120px 20px 60px;
display: grid;
grid-template-columns: 1fr 300px;
gap: 40px;
}
.post-main {
background-color: var(--card-bg);
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--card-border);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.post-header {
position: relative;
}
.post-featured-image {
height: 400px;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
position: relative;
}
.post-featured-image::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100px;
background: linear-gradient(to top, var(--card-bg), transparent);
}
.post-info {
padding: 30px 40px;
}
.post-content {
padding: 0 40px 40px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.8;
color: #e0e0e0;
}
.share-section {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid var(--card-border);
}
.share-title {
font-size: 1.2rem;
margin-bottom: 15px;
}
.share-buttons {
display: flex;
gap: 15px;
}
.share-button {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.05);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-ash);
transition: all 0.3s ease;
}
.share-button:hover {
background-color: var(--accent-fire);
color: var(--text-light);
transform: translateY(-3px);
}
.author-section {
margin-top: 40px;
padding: 30px;
background-color: rgba(255, 255, 255, 0.02);
border-radius: 8px;
display: flex;
gap: 20px;
align-items: center;
}
.toc-list {
list-style: none;
}
.toc-item {
margin-bottom: 12px;
padding-left: 15px;
position: relative;
}
.toc-item::before {
content: "";
position: absolute;
left: 0;
top: 10px;
width: 5px;
height: 5px;
border-radius: 50%;
background-color: var(--accent-fire);
}
.toc-link {
color: var(--text-ash);
text-decoration: none;
transition: all 0.3s ease;
font-size: 0.9rem;
line-height: 1.5;
display: block;
}
.toc-link:hover {
color: var(--accent-fire);
transform: translateX(5px);
}
.toc-subitem {
margin-top: 8px;
padding-left: 15px;
}
.related-posts-list {
list-style: none;
}
.related-post-item {
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.related-post-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.related-post-link {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: var(--text-ash);
transition: color 0.3s ease;
}
.related-post-link:hover {
color: var(--accent-fire);
}
.related-post-thumb {
width: 60px;
height: 60px;
border-radius: 4px;
background-size: cover;
background-position: center;
flex-shrink: 0;
}
.related-post-info {
flex-grow: 1;
}
.related-post-title {
font-size: 0.9rem;
line-height: 1.4;
margin-bottom: 5px;
}
.related-post-date {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.5);
}
/* Social grid for contact page */
.social-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 15px;
margin-top: 1.5rem;
}
.social-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 10px;
padding: 15px;
border-radius: 8px;
background-color: rgba(255, 255, 255, 0.05);
transition: all 0.3s ease;
text-decoration: none;
}
.social-item:hover {
background-color: rgba(255, 100, 0, 0.1);
transform: translateY(-5px);
}
/* 404 page styles */
.error-container {
position: relative;
z-index: 2;
padding-top: 150px;
padding-bottom: 100px;
}
.error-code {
font-size: 8rem;
font-weight: 700;
background: linear-gradient(to right, var(--accent-fire), var(--text-light));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
margin-bottom: 1rem;
text-shadow: var(--glow-effect);
position: relative;
}
.error-title {
font-size: 2rem;
margin-bottom: 1.5rem;
color: var(--text-light);
}
.error-text {
font-size: 1.1rem;
color: var(--text-ash);
margin-bottom: 2rem;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.broken-sword {
font-size: 5rem;
color: var(--accent-fire);
margin-bottom: 2rem;
position: relative;
z-index: 1;
}
.broken-sword::after {
content: "";
position: absolute;
width: 40px;
height: 4px;
background: var(--accent-fire);
transform: rotate(-45deg);
top: 60%;
left: 45%;
box-shadow: var(--glow-effect);
}
.home-button {
background: linear-gradient(45deg, var(--accent-dragon), var(--accent-fire));
border: none;
padding: 0.75rem 2rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
box-shadow: 0 4px 15px rgba(139, 0, 0, 0.3);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
color: var(--text-light);
text-decoration: none;
border-radius: 4px;
display: inline-flex;
align-items: center;
gap: 10px;
}
.home-button:hover {
transform: translateY(-3px);
box-shadow: 0 7px 25px rgba(139, 0, 0, 0.5);
color: var(--text-light);
}
.home-button::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: 0.6s;
}
.home-button:hover::before {
left: 100%;
}
.error-code-container {
position: relative;
display: inline-block;
}
.error-code-container::before,
.error-code-container::after {
content: "<";
position: absolute;
color: var(--accent-fire);
opacity: 0.6;
font-size: 6rem;
font-family: 'Fira Code', monospace;
}
.error-code-container::before {
left: -4rem;
}
.error-code-container::after {
content: "/>";
right: -6rem;
}
/* Media Queries */
@media (max-width: 992px) {
.timeline:before {
left: 2rem;
}
.timeline-item:nth-child(odd) .timeline-content,
.timeline-item:nth-child(even) .timeline-content {
width: calc(100% - 3rem);
margin-left: 3rem;
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.timeline-dot {
left: 2rem;
}
.post-container {
grid-template-columns: 1fr;
}
.post-sidebar {
position: static;
order: -1;
}
.post-featured-image {
height: 300px;
}
.post-title {
font-size: 2rem;
}
.post-info, .post-content {
padding: 20px;
}
}
@media (max-width: 768px) {
.intro-text {
margin-top: 2rem;
}
.testimonial-author {
flex-direction: column;
align-items: center;
text-align: center;
}
.author-info {
margin-top: 1rem;
text-align: center;
}
.error-code {
font-size: 6rem;
}
.error-title {
font-size: 1.8rem;
}
.broken-sword {
font-size: 4rem;
}
.error-code-container::before,
.error-code-container::after {
font-size: 4rem;
}
.error-code-container::before {
left: -3rem;
}
.error-code-container::after {
right: -4.5rem;
}
}
@media (max-width: 576px) {
.error-code {
font-size: 4rem;
}
.error-title {
font-size: 1.5rem;
}
.broken-sword {
font-size: 3rem;
}
.error-code-container::before,
.error-code-container::after {
font-size: 3rem;
}
.error-code-container::before {
left: -2rem;
}
.error-code-container::after {
right: -3rem;
}
}
+7
View File
@@ -0,0 +1,7 @@
import type {AboutProps} from '../untils/AboutProps'
const url = `${import.meta.env.BASE_URL}data/AboutProps.json`;
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to fetch about props: ${res.status}`);
export const aboutProps: AboutProps = await res.json();
+7
View File
@@ -0,0 +1,7 @@
import type {BlogPostProps} from '../untils/BlogPostProps'
const url = `${import.meta.env.BASE_URL}data/BlogPosts.json`;
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to fetch blog posts: ${res.status}`);
export const blogPostsData: BlogPostProps[] = await res.json();
+7
View File
@@ -0,0 +1,7 @@
import type {ContactProps} from '../untils/ContactProps'
const url = `${import.meta.env.BASE_URL}data/Contact.json`;
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to fetch blog posts: ${res.status}`);
export const contactData: ContactProps = await res.json();
+48
View File
@@ -0,0 +1,48 @@
import type { ProjectProps, Category } from "../untils/ProjectProps";
const url = `${import.meta.env.BASE_URL}data/ProjectProps.json`;
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to fetch projects: ${res.status}`);
type CategoryMap = Record<string, Category>
export const Categories: CategoryMap = {
Embedded: {
fullName: "Embedded Systems",
shortName: "Embedded",
icon: "fas fa-microchip",
},
Web: {
fullName: "Web Development",
shortName: "Web",
icon: "fas fa-code",
},
ML: {
fullName: "Machine Learning",
shortName: "ML",
icon: "fas fa-robot",
},
IoT: {
fullName: "Internet of Things",
shortName: "IoT",
icon: "fas fa-network-wired",
},
Sec: {
fullName: "Cybersecurity",
shortName: "Security",
icon: "fas fa-shield-alt",
},
}
type RawProject = Omit<ProjectProps, 'category'> & { category: string[] };
const rawProjects: RawProject[] = await res.json();
// map the category-keys back to your CategoryMap objects
export const projectsData: ProjectProps[] = rawProjects.map(p => ({
...p,
category: p.category.map(key => {
const cat = Categories[key];
if (!cat) throw new Error(`Unknown project category key: ${key}`);
return cat;
})
}));
+102
View File
@@ -0,0 +1,102 @@
export const randomCodeLineData :string[] = [
'print("Hello, world!")',
'int main() { return 0; }',
'std::vector<int> v{1, 2, 3, 4};',
'Console.WriteLine(\"Enter your name:\");',
'for i in range(10):',
'char* buffer = malloc(256);',
'using (var fs = new FileStream(path, FileMode.Open))',
'if (x > y) x = y;',
'def factorial(n):',
'double result = pow(x, y);',
'string greeting = \"Hi there\";',
'auto it = map.find(key);',
'List<string> names = new List<string>();',
'while (ptr != nullptr) {',
'with open(\"file.txt\", \"r\") as f:',
'int *arr = new int[10];',
'var dict = new Dictionary<string, int>();',
'std::cout << \"Value: \" << value << std::endl;',
'await Task.Delay(1000);',
'lambda x: x * x',
'float pi = 3.14159f;',
'bool success = TryParse(input, out result);',
'try { /* ... */ } catch (Exception ex) { }',
'def hello(name=\"User\"):',
'constexpr int MAX = 100;',
'public class Person { }',
'if __name__ == \"__main__\":',
'switch(choice) { case 1: break; }',
'var json = JsonConvert.DeserializeObject(data);',
'std::sort(v.begin(), v.end());',
'Console.ReadKey();',
'for (int i = 0; i < n; i++)',
'x = x[::-1]',
'char c = getchar();',
'[Obsolete(\"Use NewMethod instead\")]',
'str_list = [s.upper() for s in strings]',
'uint64_t id = generate_id();',
'public interface IService { }',
'import os',
'static void Helper() { }',
'client.SendAsync(request);',
'matrix[i][j] = i * j;',
'def merge(a, b): return a + b',
'std::unique_ptr<Foo> p = std::make_unique<Foo>();',
'Task.Run(() => DoWork());',
'with lock(mutex):',
'vector.push_back(elem);',
'string.Join(\", \", list)',
'if (obj is MyClass mc) { }',
'os.remove(\"temp.txt\")',
'char16_t ch = u\'A\';',
'public static readonly string ID = Guid.NewGuid().ToString();',
'def fib(n): return fib(n-1) + fib(n-2)',
'memcpy(dest, src, size);',
'foreach (var item in collection) { }',
'assert(x != null);',
'std::thread t([](){ doWork(); });',
'[Serializable]',
'lst.append(item)',
'GC.Collect();',
'auto lambda = [](int x){ return x+1; };',
'public override string ToString() => Name;',
'if (!File.Exists(path)) File.Create(path);',
'double average = accumulate(v.begin(), v.end(), 0.0) / v.size();',
'using System.Text;',
'def decorator(func):',
'for (auto &p : pairs) { }',
'var query = from x in db.Items select x;',
'bytes_data = b\"\\x00\\xFF\"',
'cout << std::hex << n;',
'string.Format(\"{0}, {1}\", a, b);',
'import sys, json',
'bool isEmpty = list.Count == 0;',
'std::map<string,int> mp;',
'Console.ForegroundColor = ConsoleColor.Green;',
'def prime(n):',
'int score = Random().Next(0, 100);',
'var path = Path.Combine(dir, file);',
'auto now = std::chrono::system_clock::now();',
'if (val?.Length > 0) { }',
'with session.begin():',
'list<int> primes;',
'public event EventHandler Clicked;',
'x, y = y, x',
'char16_t* wstr = L\"wide\";',
'dynamic obj = new ExpandoObject();',
'assert(sizeof(int) == 4);',
'Console.Write($\"Count: {count}\");',
'std::tie(a, b) = getPair();',
'byte[] buffer = new byte[1024];',
'def async_func():',
'virtual void Render() override;',
'os.getcwd()',
'for k, v in dict.items():',
'std::shared_ptr<Foo> sp = ptr;',
'public bool Equals(Person other) => Id==other.Id;',
'return x is not None',
'size_t len = strlen(str);',
'var enumerable = Enumerable.Range(1,10);',
'float angle = atan2(y, x);',
]
+7
View File
@@ -0,0 +1,7 @@
import {type SkillCardProps} from '../untils/SkillCardProps';
const url = `${import.meta.env.BASE_URL}data/SkillCardProps.json`;
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to fetch skill card data: ${res.status}`);
export const skillCardData: SkillCardProps[] = await res.json();
+7
View File
@@ -0,0 +1,7 @@
import {type SocialLink} from "../untils/SocialLink";
const url = `${import.meta.env.BASE_URL}data/SocialLink.json`;
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to fetch skill card data: ${res.status}`);
export const socialLinkData: SocialLink[] = await res.json();
+10
View File
@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import '@styles/index.scss'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
+178
View File
@@ -0,0 +1,178 @@
import { PageHeader } from '../components/PageHeader';
import { aboutProps } from '../data/AboutProps';
import style from '@styles/About.module.scss';
export const About = () => {
const data = aboutProps;
return (
<>
<PageHeader
title="The Forgemaster"
subtitle="Crafting digital solutions from the raw materials of innovation"
/>
{/* Introduction Section */}
<section className="py-5">
<div className="container">
<div className="row">
<div className="col-lg-4">
<h2 className={`${style.sectionTitle}`}>The Master Behind the Mask</h2>
<div className={style.profileImage}>
<img src="/images/profile.jpg" alt="SecCodeSmith" />
</div>
</div>
<div className="col-lg-8">
<div className={style.introText}>
<p className={style.introText}>Greetings, seeker of digital arcana. I am <strong>SecCodeSmith</strong>, a master craftsman of code and conductor of silicon. For over a decade, I have been forging robust solutions across the realms of embedded systems, machine learning, and web development.</p>
<p className={style.introText}>My journey began in the depths of <span className={style.accent}>low-level programming</span>, where I learned to bend hardware to my will through carefully crafted instructions. From the arcane energies of microcontrollers to the vast landscapes of cloud architecture, I have honed my skills to create solutions that stand the test of time.</p>
<p className={style.introText}>What distinguishes my work is the <strong>meticulous attention to security</strong> and performance. Like a blacksmith who understands that a sword's true value lies not just in its edge but in the integrity of its steel, I craft code that is not merely functional but resilient against the chaotic forces of the digital wilderness.</p>
<p className={style.introText}>Beyond the technical realms, I am a devoted practitioner of open-source sorcery, contributing to projects that empower others to create and innovate. When not at the forge, I can be found exploring the ancient texts of computer science, delving into new programming languages, or mentoring apprentices on their own journey.</p>
<div className="mt-4">
<a href="#" className={style.connectLink}>
<i className={`fab fa-github ${style.connectIcon}`}></i>
<span>GitHub</span>
</a>
<a href="#" className={style.connectLink}>
<i className={`fab fa-linkedin ${style.connectIcon}`}></i>
<span>LinkedIn</span>
</a>
<a href="#" className={style.connectLink}>
<i className={`fab fa-twitter ${style.connectIcon}`}></i>
<span>Twitter</span>
</a>
<a href="#" className={style.connectLink}>
<i className={`fas fa-envelope ${style.connectIcon}`}></i>
<span>Email</span>
</a>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Core Values Section */}
<section className="py-5 bg-dark">
<div className="container">
<div className="row mb-4">
<div className="col-12">
<h2 className={` ${style.sectionTitle}`}>Forging Principles</h2>
</div>
</div>
<div className={`row g-4 ${style.row}`}>
{data.coreValues.map((value, index) => (
<div className="col-md-6 col-lg-3" key={index}>
<div className={` h-100 ${style.valueCard}`}>
<div className={style.valueIcon}>
<i className={value.icon}></i>
</div>
<h3 className={`${style.valueTitle}`}>{value.title}</h3>
<p className={`${style.valueText}`}>{value.description}</p>
</div>
</div>
))}
</div>
</div>
</section>
{/* Technical Expertise Section */}
<section className="py-5">
<div className="container">
<div className="row mb-4">
<div className="col-12">
<h2 className={`${style.sectionTitle}`}>Arsenal of Expertise</h2>
</div>
</div>
<div className="row g-4">
{data.technicalArsenal.map((value, index) => (
<div className="col-md-6 col-lg-3" key={index}>
<div className={`h-100 ${style.skillCard}`} key={index}>
<div className={`${style.skillHeader}`}>
<div className={`${style.skillIcon}`}>
<i className={value.icon}></i>
</div>
<h3 className={`skill-title ${style.skillTitle}`}>{value.title}</h3>
</div>
<ul className="list-unstyled">
{value.skills.map((skillItem, skillIndex) => (
<li className="mb-2" key={skillIndex}>
<i className={`fas fa-check skill-check ${style.skillCheck}`}></i>
<span>{skillItem}</span>
</li>
))}
</ul>
</div>
</div>
))}
</div>
</div>
</section >
{/* Professional Journey Section */}
< section className="py-5 bg-dark" >
<div className="container">
<div className="row mb-5">
<div className="col-12 text-center">
<h2 className={`section-title ${style.sectionTitle}`}>The Smith's Journey</h2>
</div>
</div>
<div className="row">
<div className="col-12">
<ul className={`${style.timeline}`}>
{data.professionalJourney.map((journey, index) => (
<li className={`${style.timelineItem} clearfix`} key={index}>
<div className={`${style.timelineDot}`}></div>
<div className={`${style.timelineContent}`}>
<div className={`${style.timelineDate}`}>{journey.duration}</div>
<h3 className={`${style.timelineTitle}`}>{journey.title}</h3>
<p className={`${style.timelineText}`}>{journey.description}</p>
</div>
</li>
))}
</ul>
</div>
</div>
</div>
</section >
{/* Testimonials Section */}
{data.testimonials.length > 0 && (
< section className="py-5" >
<div className="container">
<div className="row mb-4">
<div className="col-12">
<h2 className={`section-title ${style.sectionTitle}`}>Tales from the Guild</h2>
</div>
</div>
<div className="row">
<div className="col-lg-8 mx-auto">
{data.testimonials.map((testimonial, index) => (
<div className="testimonial" key={index}>
<div className="testimonial-content">
{testimonial.content}
</div>
<div className="d-flex align-items-center gap-3 testimonial-author">
<div className="author-avatar">
<img src="/images/testimonials/james.jpg" alt={testimonial.author} />
</div>
<div className="author-info">
<h4 className="author-name">{testimonial.author}</h4>
<div className="author-title">{testimonial.position}</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</section >)}
</>
);
};
+170
View File
@@ -0,0 +1,170 @@
import { useState } from 'react';
import { PageHeader } from '../components/PageHeader';
import { BlogCard } from '../components/BlogCard';
import { blogPostsData } from '../data/blogPostsData';
import style from '@styles/Blog.module.scss';
export const Blog = () => {
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, /*setCurrentPage*/] = useState(1);
const filteredPosts = searchQuery
? blogPostsData.filter(post =>
post.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
post.excerpt.toLowerCase().includes(searchQuery.toLowerCase()) ||
post.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()))
)
: blogPostsData;
const filteredPostPageCount = Math.ceil(filteredPosts.length / 6);
const featuredPost = blogPostsData.find(post => post.featured);
const regularPosts = filteredPosts.filter(post => !post.featured);
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value);
};
const categories = Array.from(new Set(blogPostsData.map(post => post.category)));
const allTags = blogPostsData.flatMap(post => post.tags);
const uniqueTags = Array.from(new Set(allTags));
return (
<>
<PageHeader
title="The Arcane Chronicles"
subtitle="Insights, discoveries, and technical runes from the digital forge"
/>
<div className="container mt-5">
<div className="row">
<div className="col-lg-8">
{featuredPost && (!searchQuery ||
featuredPost.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
featuredPost.excerpt.toLowerCase().includes(searchQuery.toLowerCase()) ||
featuredPost.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()))
) && (
<BlogCard post={featuredPost} />
)}
<div className="row">
{regularPosts.map(post => (
<div className="col-md-6" key={post.id}>
<BlogCard post={post} />
</div>
))}
</div>
{filteredPosts.length === 0 && (
<div className="text-center mt-5">
<h3>No posts found matching your search.</h3>
<button
className="btn btn-primary mt-3"
onClick={() => setSearchQuery('')}
>
Clear Search
</button>
</div>
)}
{filteredPostPageCount > 1 && (
<nav className="mt-5" aria-label="Blog pagination">
<ul className="pagination justify-content-center">
{(currentPage > 1) && ( <li className={`page-item ${style.pageItem}`}>
<a className={`page-link ${style.pageLink}`} href="#" aria-label="Previous">
<i className="fas fa-angle-left"></i>
</a>
</li>)}
{Array.from({ length: filteredPostPageCount }, (_, index) => (
<li className={`page-item ${style.pageItem}`} key={index}>
<a className={`page-link ${style.pageLink}`} href="#">{index + 1}</a>
</li>
))}
{(currentPage < filteredPostPageCount) && ( <li className={`page-item ${style.pageItem}`}>
<a className={`page-link ${style.pageLink}`} href="#" aria-label="Next">
<i className="fas fa-angle-right"></i>
</a>
</li>)}
</ul>
</nav>
)}
</div>
<div className={`col-lg-4 sidebar`}>
<div className={`sidebar-widget ${style.sidebarWidget}`}>
<h3 className={`widget-title ${style.widgetTitle}`}>Search</h3>
<form className={`search-form ${style.searchForm}`}>
<div className="input-group">
<input
type="text"
className="form-control"
placeholder="Search articles..."
value={searchQuery}
onChange={handleSearchChange}
/>
<button className={`btn btn-primary ${style.btn}`} type="submit">
<i className="fas fa-search"></i>
</button>
</div>
</form>
</div>
<div className={`sidebar-widget ${style.sidebarWidget}`}>
<h3 className={`widget-title ${style.widgetTitle}`}>Categories</h3>
<div className="list-group">
{categories.map((category, index) => {
const count = blogPostsData.filter(post => post.category === category).length;
return (
<div className={`list-group-item ${style.listGroupItem}`} key={index}>
<a href="#" onClick={() => setSearchQuery(category)}>
<span>{category}</span>
<span className={`category-count ${style.categoryCount}`}>{count}</span>
</a>
</div>
);
})}
</div>
</div>
<div className={`sidebar-widget ${style.sidebarWidget}`}>
<h3 className={`widget-title ${style.widgetTitle}`}>Recent Posts</h3>
<div className="recent-posts">
{blogPostsData.slice(0, 3).map(post => (
<div className={`recent-post-item ${style.recentPostItem}`} key={post.id}>
<img src={post.image} className={`recent-post-thumb ${style.recentPostThumb}`} alt={post.title} />
<div className={style.recentPostInfo}>
<h4 className={style.recentPostTitle}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</h4>
<span className={style.recentPostDate}>{post.date}</span>
</div>
</div>
))}
</div>
</div>
<div className={`sidebar-widget ${style.sidebarWidget}`}>
<h3 className={`widget-title ${style.widgetTitle}`}>Popular Tags</h3>
<div>
{uniqueTags.map((tag, index) => (
<span
key={index}
className={style.postTag}
onClick={() => setSearchQuery(tag)}
style={{ cursor: 'pointer' }}
>
{tag}
</span>
))}
</div>
</div>
</div>
</div>
</div>
</>
);
};
+299
View File
@@ -0,0 +1,299 @@
import { useEffect, useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import { blogPostsData } from '../data/blogPostsData';
import { NotFound } from './NotFound';
import style from '@styles/BlogPost.module.scss';
interface TableOfContentsItem {
title: string;
id: number;
children?: TableOfContentsItem[];
}
function dedent(str: string): string {
const lines = str.split('\n');
while (lines.length > 0 && lines[0].trim() === '') {
lines.shift();
}
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
lines.pop();
}
const indents = lines
.filter(line => line.trim().length > 0)
.map(line => line.match(/^ */)![0].length);
const minIndent = indents.length ? Math.min(...indents) : 0;
return lines.map(line => line.slice(minIndent)).join('\n');
}
const TableOfContents = ({ toc }: { toc: TableOfContentsItem[] }) => {
return (
<ul className={style.tocList}>
{toc.map(item => (
<li key={item.id} className={style.tocItem}>
<a href={`#${item.id}`} className={style.tocLink}>{item.title}</a>
{item.children && item.children.length > 0 && (
<TableOfContents toc={item.children} />
)}
</li>
))}
</ul>
);
}
export const BlogPost = () => {
const { slug } = useParams<{ slug: string }>();
const [post, setPost] = useState(blogPostsData.find(post => post.slug === slug));
const [relatedPosts, setRelatedPosts] = useState(blogPostsData.slice(0, 3));
const [toc, setToc] = useState<TableOfContentsItem[]>([]);
useEffect(() => {
const currentPost = blogPostsData.find(post => post.slug === slug);
let id = 0;
const toc: TableOfContentsItem[] = [];
if (currentPost) {
setPost(currentPost);
const related = blogPostsData
.filter(p => p.id !== currentPost.id)
.filter(p =>
p.category === currentPost.category ||
p.tags.some(tag => currentPost.tags.includes(tag))
)
.slice(0, 3);
setRelatedPosts(related);
let newContent = '';
if (currentPost.content) {
const content = dedent(currentPost.content);
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
if (/^#\s/.test(lines[i])) {
newContent += `<h2 id="${id}">${lines[i].replace(/^#+\s*/, '')}</h2> \n`;
toc.push({ title: lines[i].replace(/^#+\s*/, ''), id });
id++;
}
else if (/^##\s/.test(lines[i])) {
newContent += `<h3 id="${id}">${lines[i].replace(/^#+\s*/, '')}</h3> \n`;
if (toc.length > 0) {
const parent = toc[toc.length - 1];
if (!parent.children) {
parent.children = [];
}
parent.children.push({ title: lines[i].replace(/^#+\s*/, ''), id });
}
id++;
}
else if (/^###\s/.test(lines[i])) {
newContent += `<h4 id="${id}">${lines[i].replace(/^#+\s*/, '')}</h4> \n`;
if (toc.length > 0) {
const h2Parent = toc[toc.length - 1];
if (h2Parent.children && h2Parent.children.length > 0) {
const h3Parent = h2Parent.children[h2Parent.children.length - 1];
if (!h3Parent.children) {
h3Parent.children = [];
}
h3Parent.children.push({ title: lines[i].replace(/^#+\s*/, ''), id });
}
}
id++;
}
else if (/^\*\s/.test(lines[i])) {
newContent += '<ul>\n';
for (; i < lines.length; i++) {
if (/^\*\s/.test(lines[i])) {
newContent += `<li>${lines[i].replace(/^\*\s/, '')}</li>\n`;
}
else {
break;
}
}
newContent += '</ul>\n';
}
else if (/^\-\s/.test(lines[i])) {
newContent += '<ol>\n';
for (; i < lines.length; i++) {
if (/^\-\s/.test(lines[i])) {
newContent += `<li>${lines[i].replace(/^\-\s/, '')}</li>\n`;
}
else {
break;
}
}
newContent += '</ol>\n';
}
else {
newContent += `<p>${lines[i]}</p>\n`;
}
}
}
newContent = newContent.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
newContent = newContent.replace(/__(.*?)__/g, '<strong>$1</strong>');
newContent = newContent.replace(/\*(.*?)\*/g, '<em>$1</em>');
newContent = newContent.replace(/_(.*?)_/g, '<em>$1</em>');
newContent = newContent.replace(/`(.*?)`/g, '<code>$1</code>');
newContent = newContent.replace(/!\[(.*?)\]\((.*?)\)/g, '<img alt="$1" src="$2" />');
newContent = newContent.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2">$1</a>');
const contentElement = document.getElementById('postContent');
contentElement!.innerHTML = newContent;
setToc(toc);
window.scrollTo(0, 0);
}
}, [slug]);
if (!post) {
return <NotFound />;
}
return (
<div className={style.postContainer}>
<main className={style.postMain}>
<article>
{/* Post Header */}
<div className={style.postHeader}>
<div className={style.postFeaturedImage} style={{ backgroundImage: `url(${import.meta.env.BASE_URL}${post.image})` }}></div>
<div className={style.postCategory}>{post.category}</div>
</div>
{/* Post Info */}
<div className={style.postInfo}>
<h1 className={style.postTitle}>{post.title}</h1>
<div className={style.postMeta}>
<div className={style.metaItem}>
<i className={`fas fa-calendar-alt ${style.metaIcon}`}></i>
<span>{post.date}</span>
</div>
<div className={style.metaItem}>
<i className={`fas fa-user ${style.metaIcon}`}></i>
<span>{post.author}</span>
</div>
<div className={style.metaItem}>
<i className={`fas fa-clock ${style.metaIcon}`}></i>
<span>{post.readTime}</span>
</div>
<div className={style.metaItem}>
<i className={`fas fa-comments ${style.metaIcon}`}></i>
<span>{post.commentCount} Comments</span>
</div>
</div>
</div>
<div className={style.postContent} id="postContent">
</div>
<div className={style.postContent}>
{/* Post Tags */}
<div className={style.postTags}>
{post.tags.map((tag, index) => (
<span key={index} className={style.postTag}>{tag}</span>
))}
</div>
{/* Share Section */}
<div className={style.shareSection}>
<h3 className={style.shareTitle}>Share this arcane knowledge</h3>
<div className={style.shareButtons}>
<a href="#" className={style.shareButton}><i className="fab fa-twitter"></i></a>
<a href="#" className={style.shareButton}><i className="fab fa-facebook-f"></i></a>
<a href="#" className={style.shareButton}><i className="fab fa-linkedin-in"></i></a>
<a href="#" className={style.shareButton}><i className="fab fa-reddit-alien"></i></a>
<a href="#" className={style.shareButton}><i className="far fa-envelope"></i></a>
</div>
</div>
{/* Author Info */}
<div className={style.authorSection}>
<div className={style.authorAvatar}>
<img src="/images/author.jpg" alt="SecCodeSmith" />
</div>
<div className={style.authorInfo}>
<h3 className={style.authorName}>SecCodeSmith</h3>
<p className={style.authorBio}>Digital blacksmith forging embedded systems and IoT solutions in the fires of innovation. Specializes in low-level firmware, communication protocols, and hardware-software integration for resource-constrained devices.</p>
<div className={style.authorSocial}>
<a href="#"><i className="fab fa-github"></i></a>
<a href="#"><i className="fab fa-twitter"></i></a>
<a href="#"><i className="fab fa-linkedin-in"></i></a>
<a href="#"><i className="fab fa-stack-overflow"></i></a>
</div>
</div>
</div>
{/*Temporarily disable comments section */}
{ 0 && (
<div className={style.commentsSection}>
<h3 className={style.commentsTitle}>Discussions ({post.commentCount})</h3>
{/* Sample Comments */}
<div className={style.comment}>
<div className={style.commentAvatar}>
<img src="/images/user-1.jpg" alt="User" />
</div>
<div className={style.commentContent}>
<h4 className={style.commentAuthor}>Michael Chen</h4>
<div className={style.commentDate}>May 5, 2025 at 15:42</div>
<p className={style.commentText}>Excellent article! I've been struggling with I2C bus lockups in my weather station project. The recovery function you provided solved the issue. Have you considered adding a timeout mechanism to the SPI transfers for error detection?</p>
<a href="#" className={style.commentReply}>Reply</a>
</div>
</div>
{/* Comment Form */}
<div className={`${style.commentForm} comment-form`}>
<h3 className={style.formTitle}>Leave your mark</h3>
<form>
<div className={`form-row ${style.formRow}`}>
<div className={`form-group ${style.formGroup}`}>
<input type="text" className={`${style.formControl}`} placeholder="Your Name" />
</div>
<div className={`form-group ${style.formGroup}`}>
<input type="email" className={`${style.formControl}`} placeholder="Your Email" />
</div>
</div>
<div className={`form-group ${style.formGroup}`}>
<textarea className={`${style.formControl}`} rows={5} placeholder="Your Comment"></textarea>
</div>
<button type="submit" className={`submit-button ${style.submitButton}`}>Post Comment</button>
</form>
</div>
</div>
)}
</div>
</article>
</main>
<aside className={style.postSidebar}>
{/* Table of Contents */}
<div className={style.sidebarWidget}>
<h3 className={style.widgetTitle}>Table of Contents</h3>
<TableOfContents toc={toc} />
</div>
{/* Related Posts */}
<div className={style.sidebarWidget}>
<h3 className={style.widgetTitle}>Related Scrolls</h3>
<ul className={style.relatedPostsList}>
{relatedPosts.map(relatedPost => (
<li className={style.relatedPostItem} key={relatedPost.id}>
<Link to={`/blog/${relatedPost.slug}`} className={style.relatedPostLink}>
<div className={style.relatedPostThumb} style={{ backgroundImage: `url(${relatedPost.image})` }}></div>
<div className={style.relatedPostInfo}>
<h4 className={style.relatedPostTitle}>{relatedPost.title}</h4>
<span className={style.relatedPostDate}>{relatedPost.date}</span>
</div>
</Link>
</li>
))}
</ul>
</div>
</aside>
</div>
);
};
+160
View File
@@ -0,0 +1,160 @@
import { PageHeader } from '../components/PageHeader';
import { ContactForm } from '../components/ContactForm';
import style from '@styles/Contact.module.scss';
import '@styles/Accordion.scss';
import { contactData } from '../data/contactData'
import type { ContactProps } from '../untils/ContactProps';
export const Contact = () => {
const data: ContactProps = contactData;
return (
<>
<PageHeader
title="Forge Connection"
subtitle="Have a project in mind? Send a message to the forge and let's craft something extraordinary together"
/>
<section className="py-5">
<div className="container">
<div className="row g-4">
{/* Contact Form Column */}
<div className="col-lg-7">
<ContactForm />
</div>
{/* Contact Info Column */}
<div className="col-lg-5">
{/* Email Info */}
<div className={`card mb-4 ${style.card}`}>
<div className={`card-body p-4 ${style.cardBody}`}>
<div className={`${style.contactCard}`}>
<div className={`${style.contactIcon}`}>
<i className="fas fa-envelope"></i>
</div>
<h3 className={style.contactTitle}>Email</h3>
<div className="contact-content">
<p>For project inquiries or questions:</p>
<p>
<a href={`mailto:${data.email}`} className={style.contactLink}>
{data.email}
</a>
</p>
<p>For business collaborations:</p>
<p>
<a href={`mailto:${data.businessEmail}`} className={style.contactLink}>
{data.businessEmail}
</a>
</p>
</div>
</div>
</div>
</div>
{
data.socialLinks.length > 0 && (
<div className={`card ${style.card}`}>
<div className={`card-body p-4 ${style.cardBody}`}>
<div className={style.contactCard}>
<div className={style.contactIcon}>
<i className="fas fa-link"></i>
</div>
<h3 className={style.contactTitle}>Connect Through the Ether</h3>
<div className={style.contactContent}>
<p>You can also reach me through these digital pathways:</p>
<div className={style.socialGrid}>
{data.socialLinks.map((link, index) => (
<a key={index} href={link.url} className={`social-item ${style.socialItem}`}>
<i className={`${link.icon} social-icon-lg ${style.socialIconLg}`}></i>
<span className={`social-name ${style.socialName}`}>{link.platform}</span>
</a>
))}
</div>
</div>
</div>
</div>
</div>
)
}
</div>
</div>
</div>
</section >
{/* FAQ Section */}
< section className="py-5 bg-dark" >
<div className="container">
<div className="row mb-4">
<div className="col-12 text-center">
<h2 className="mb-3">Frequently Asked Questions</h2>
<p className="text-muted">Common inquiries about forging digital solutions</p>
</div>
</div>
<div className="row">
<div className="col-lg-8 mx-auto">
<div className="accordion" id="faqAccordion">
{
data.questionsAndAnswers.map((item, index) => (
<div className={`accordion-item`} key={index}>
<h2 className={`accordion-header`} id={`heading${index}`}>
<button
className={`accordion-button collapsed`}
type="button"
data-bs-toggle="collapse"
data-bs-target={`#collapse${index}`}
aria-expanded="false"
aria-controls={`collapse${index}`}
>
{item.question}
</button>
</h2>
<div
id={`collapse${index}`}
className="accordion-collapse collapse"
aria-labelledby={`heading${index}`}
data-bs-parent="#faqAccordion"
>
<div className={`accordion-body`}>
{item.answer}
</div>
</div>
</div>
))
}
</div>
</div>
</div>
</div>
</section >
{/* Map Section */}
< section className="py-5" >
<div className="container">
<div className="row mb-4">
<div className="col-12 text-center">
<h2 className="mb-3">The Forge Location</h2>
<p className="text-muted mb-4">Where digital creations are hammered into existence</p>
</div>
</div>
<div className="row">
<div className="col-12">
<div className={style.mapContainer}>
<iframe
className={style.mapFrame}
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d82077.2903513271!2d21.9433946!3d50.0411861!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x473cfae3cc14d449%3A0xd2240d31b33eb2ed!2zUnplc3rDs3c!5e0!3m2!1sen!2spl!4v1651813309336!5m2!1sen!2spl"
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
></iframe>
</div>
</div>
</div>
</div>
</section >
</>
);
};
+128
View File
@@ -0,0 +1,128 @@
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
import styles from '@styles/Home.module.scss';
import SkillCard from '../components/SkillCard'
import { skillCardData } from '../data/skillCardData';
import { randomCodeLineData } from '../data/randomCodeLineData';
export const Home = () => {
const skills = skillCardData;
const randomCodeLines = randomCodeLineData;
useEffect(() => {
const createBinaryBg = () => {
const binaryBg = document.getElementById('binary-bg');
if (!binaryBg) return;
const linesCount = 20;
binaryBg.innerHTML = '';
for (let i = 0; i < linesCount; i++) {
const line = document.createElement('div');
line.className = styles.binaryLine;
line.style.left = `${Math.random() * 100}%`;
line.style.animationDuration = `${Math.random() * 10 + 15}s`;
let lineNumber = Math.floor(Math.random() * randomCodeLines.length);
let binaryString = randomCodeLines[lineNumber];
line.textContent = binaryString;
binaryBg.appendChild(line);
}
};
const createParticles = () => {
const particlesContainer = document.getElementById('particles');
if (!particlesContainer) return;
particlesContainer.innerHTML = '';
const particlesCount = 50;
for (let i = 0; i < particlesCount; i++) {
setTimeout(() => {
const particle = document.createElement('div');
particle.className = styles.particle;
const posX = Math.random() * window.innerWidth;
const posY = window.innerHeight - 100 + (Math.random() * 50);
particle.style.left = `${posX}px`;
particle.style.top = `${posY}px`;
const size = Math.random() * 3 + 1;
particle.style.width = `${size}px`;
particle.style.height = `${size}px`;
const xDrift = (Math.random() - 0.5) * 100;
particle.style.setProperty('--x', `${xDrift}px`);
const duration = Math.random() * 3 + 2;
particle.style.animationDuration = `${duration}s`;
particlesContainer.appendChild(particle);
setTimeout(() => {
particle.remove();
}, duration * 1000);
}, i * 100);
}
};
createBinaryBg();
createParticles();
const particleInterval = setInterval(() => {
const particlesContainer = document.getElementById('particles');
if (particlesContainer && particlesContainer.childElementCount < 30) {
createParticles();
}
}, 5000);
// Cleanup
return () => {
clearInterval(particleInterval);
};
}, []);
return (
<>
<section className={styles.hero} id="home">
<div id="binary-bg" className={styles.binaryBg}></div>
<div className={`container text-center ${styles.heroContainer}`}>
<div className="row justify-content-center">
<div className="col-lg-10">
<h1 className={`${styles.titleMain}`}>SecCodeSmith</h1>
<div className={`${styles.blinkingCursor}`}></div>
<p className={`subtitle mt-3 ${styles.subtitle}`}>Forging Digital Solutions in the Fires of Innovation</p>
<Link to="/projects" className={`btn btn-primary mt-4 btn-primary ${styles.btnPrimary}`}>
<i className="fas fa-fire-alt me-2"></i> View My Forge
</Link>
</div>
</div>
</div>
<div id="particles" className={`particles ${styles.particles}`}></div>
</section>
<section id="skills" className={`skills-section ${styles.skillsSection}`}>
<div className="container">
<div className="row justify-content-center mb-5">
<div className="col-lg-6 text-center">
<h2 className={`section-title ${styles.sectionTitle}`}>Technical Arsenal</h2>
</div>
</div>
<div className="row g-4">
{skills.map((skill) => (
<SkillCard
categoryIcon={skill.categoryIcon}
categoryTitle={skill.categoryTitle}
skills={skill.skills}
/>
))}
</div>
</div>
</section>
</>
);
};
+92
View File
@@ -0,0 +1,92 @@
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
export const NotFound = () => {
useEffect(() => {
// Create ember particles effect
const createEmberParticles = () => {
const particlesContainer = document.getElementById('ember-particles');
if (!particlesContainer) return;
// Clear existing particles
particlesContainer.innerHTML = '';
const emberCount = 50;
for (let i = 0; i < emberCount; i++) {
const ember = document.createElement('div');
ember.className = 'ember-particle';
// Random position
const startPosX = Math.random() * window.innerWidth;
// Random x-axis drift
const xDrift = (Math.random() - 0.5) * 200;
ember.style.setProperty('--x', `${xDrift}px`);
ember.style.left = `${startPosX}px`;
ember.style.bottom = '0';
// Random size
const size = Math.random() * 3 + 1;
ember.style.width = `${size}px`;
ember.style.height = `${size}px`;
// Random animation duration
const duration = Math.random() * 7 + 3;
ember.style.animationDuration = `${duration}s`;
// Random animation delay
const delay = Math.random() * 5;
ember.style.animationDelay = `${delay}s`;
particlesContainer.appendChild(ember);
// Remove and recreate ember after animation
setTimeout(() => {
ember.remove();
createEmberParticles();
}, (duration + delay) * 1000);
}
};
createEmberParticles();
// Cleanup
return () => {
const particlesContainer = document.getElementById('ember-particles');
if (particlesContainer) {
particlesContainer.innerHTML = '';
}
};
}, []);
return (
<div className="binary-bg">
<div className="binary-line" style={{ top: '20%', left: '10%' }}>01000101 01110010 01110010 01101111 01110010</div>
<div className="binary-line" style={{ bottom: '30%', right: '15%' }}>01001110 01101111 01110100 01000110 01101111 01110101 01101110 01100100</div>
<div className="binary-line" style={{ top: '50%', left: '80%' }}>01000110 01100001 01101001 01101100 01100101 01100100</div>
<div className="binary-line" style={{ bottom: '10%', left: '40%' }}>01000011 01101111 01100100 01100101 01000100 01100001 01101101 01100001 01100111 01100101 01100100</div>
<div className="ember-particles" id="ember-particles"></div>
<div className="container text-center error-container">
<div className="broken-sword">
<i className="fas fa-khanda"></i>
</div>
<div className="error-code-container">
<h1 className="error-code">404</h1>
</div>
<h2 className="error-title">The Artifact Was Not Found</h2>
<p className="error-text">The digital creation you seek has either been consumed by the void, moved to a different realm, or was never forged in the first place. Check your path and try again, or return to the main forge.</p>
<Link to="/" className="home-button">
<i className="fas fa-home"></i> Return to the Forge
</Link>
</div>
</div>
);
};
+98
View File
@@ -0,0 +1,98 @@
import { useState } from 'react';
import { PageHeader } from '../components/PageHeader';
import { ProjectCard } from '../components/ProjectCard';
import { ProjectModal } from '../components/ProjectModal';
import { projectsData, Categories } from '../data/projectsData';
import type {ProjectProps} from '../untils/ProjectProps';
import style from '@styles/Project.module.scss'
export const Projects = () => {
const [activeFilter, setActiveFilter] = useState('all');
const [selectedProject, setSelectedProject] = useState<ProjectProps | null>(null);
const filteredProjects = activeFilter === 'all'
? projectsData
: projectsData.filter(project => project.category.some(cat => cat.shortName.toLowerCase() === activeFilter));
if (selectedProject == null && filteredProjects.length > 0)
setSelectedProject(filteredProjects[0]);
const categories = Categories;
const featuredProject = projectsData.find(project => project.featured);
const regularProjects = filteredProjects.filter(project => !project.featured);
const handleFilterClick = (filter: string) => {
setActiveFilter(filter);
};
const handleOpenDetails = (id: string) => {
const project = projectsData.find(p => p.id === id);
if (project) {
setSelectedProject(project);
}
};
return (
<>
<PageHeader
title="The Digital Forge"
subtitle="Artifacts crafted with code, hammered with precision, and tempered in the fires of innovation"
/>
<div className="container mt-5">
{/* Projects Filter */}
<div className={`filter-container ${style.filterContainer} text-center`}>
<button
className={`filter-button ${style.filterButton} ${activeFilter === 'all' ? style.active : ''}`}
onClick={() => handleFilterClick('all')}
>
All Artifacts
</button>
{Object.values(categories).map((category) => (
<button
key={category.shortName}
className={`filter-button ${style.filterButton} ${activeFilter === category.shortName.toLowerCase() ? style.active : ''}`}
onClick={() => handleFilterClick(category.shortName.toLowerCase())}
>
{category.fullName}
</button>
))}
</div>
<div className="row">
{featuredProject && (activeFilter === 'all' || featuredProject.category.some(cat => cat.shortName.toLowerCase() === activeFilter)) && (
<div className="col-12 mb-4">
<ProjectCard project={featuredProject} onOpenDetails={handleOpenDetails} />
</div>
)}
{regularProjects.map(project => (
<ProjectCard project={project} onOpenDetails={handleOpenDetails} />
))}
</div>
{filteredProjects.length === 0 && (
<div className="text-center mt-5">
<h3>No projects found for this category.</h3>
</div>
)}
<div className="text-center mt-4 mb-5">
<a href="https://github.com/SecCodeSmith" className={`btn btn-primary ${style.btnPrimary}`}>
<i className="fab fa-github me-2"></i> View All Projects on GitHub
</a>
</div>
</div>
{selectedProject && (
<ProjectModal project={selectedProject} />
)}
</>
);
};
+30
View File
@@ -0,0 +1,30 @@
export interface AboutProps {
coreValues: CoreValue[];
technicalArsenal: TechnicalArsenal[];
professionalJourney: ProfessionalJourney[];
testimonials: Testimonial[];
}
export interface CoreValue {
title: string;
description: string;
icon: string;
}
export interface ProfessionalJourney {
title: string;
description: string;
duration: string;
}
export interface TechnicalArsenal {
title: string;
icon: string;
skills: string[];
}
export interface Testimonial {
content: string;
author: string;
position: string;
}
+16
View File
@@ -0,0 +1,16 @@
export interface BlogPostProps {
id: string;
slug: string;
title: string;
excerpt: string;
image: string;
category: string;
date: string;
author: string;
commentCount: number;
readTime: string;
featured?: boolean;
tags: string[];
content: string;
}
+19
View File
@@ -0,0 +1,19 @@
export interface ContactProps {
email: string;
businessEmail: string;
socialLinks: SocialLink[];
questionsAndAnswers: QuestionAndAnswer[];
MapIframeUrl: string;
phone: string;
}
export interface SocialLink {
platform: string;
url: string;
icon: string;
}
export interface QuestionAndAnswer {
question: string;
answer: string;
}
+46
View File
@@ -0,0 +1,46 @@
export interface ProjectProps {
id: string;
title: string;
description: string;
projectDetails?: ProjectDetails;
image: string;
category: Category[];
featured?: boolean;
technologies: ProjectTech[];
links: {
github?: string;
demo?: string;
documentation?: string;
};
}
export interface ProjectDetails {
descriptions?: string[];
startDate?: string;
endDate?: string;
dateFormatted?: string;
role?: string;
status?: string;
client?: string;
keyFeatures?: string[];
gallery?: string[];
fullTechStack?: FullTechStack[];
}
export interface Category {
[key: string]: string;
fullName: string;
shortName: string;
icon: string;
}
export interface ProjectTech {
name: string;
icon: string;
}
export interface FullTechStack {
name: string;
icons: string;
}
+10
View File
@@ -0,0 +1,10 @@
export interface Skill {
name: string;
icon: React.ReactNode; // Or string if using CSS classes for icons
}
export interface SkillCardProps {
categoryTitle: string;
categoryIcon: React.ReactNode; // Or string
skills: Skill[];
}
+4
View File
@@ -0,0 +1,4 @@
export interface SocialLink {
icon: string;
url: string;
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+66
View File
@@ -0,0 +1,66 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { Header } from '../src/components/Header';
import { Footer } from '../src/components/Footer';
import { PageHeader } from '../src/components/PageHeader';
// Setup wrapper for components that use router
const renderWithRouter = (ui: React.ReactElement) => {
return render(ui, { wrapper: BrowserRouter });
};
describe('Header Component', () => {
beforeEach(() => {
renderWithRouter(<Header />);
});
it('renders the logo text', () => {
expect(screen.getByText('SecCodeSmith')).toBeInTheDocument();
});
it('renders navigation links', () => {
expect(screen.getByText('Home')).toBeInTheDocument();
expect(screen.getByText('Blog')).toBeInTheDocument();
expect(screen.getByText('About')).toBeInTheDocument();
expect(screen.getByText('Projects')).toBeInTheDocument();
expect(screen.getByText('Contact')).toBeInTheDocument();
});
});
describe('Footer Component', () => {
beforeEach(() => {
render(<Footer />);
});
it('renders social links', () => {
const socialLinks = document.querySelectorAll('.social-icon');
expect(socialLinks.length).toBeGreaterThan(0);
});
it('renders copyright text with current year', () => {
const currentYear = new Date().getFullYear();
expect(screen.getByText(`© ${currentYear} SecCodeSmith. All rights forged in digital fire.`)).toBeInTheDocument();
});
});
describe('PageHeader Component', () => {
it('renders with title and subtitle', () => {
render(
<PageHeader
title="Test Title"
subtitle="Test Subtitle"
/>
);
expect(screen.getByText('Test Title')).toBeInTheDocument();
expect(screen.getByText('Test Subtitle')).toBeInTheDocument();
});
it('renders with only title when no subtitle is provided', () => {
render(<PageHeader title="Only Title" />);
expect(screen.getByText('Only Title')).toBeInTheDocument();
expect(screen.queryByText('Test Subtitle')).not.toBeInTheDocument();
});
});
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom';
+27
View File
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
+46
View File
@@ -0,0 +1,46 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
],
"compilerOptions": {
"resolveJsonModule": true,
"esModuleInterop": true,
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
],
"@assets/*": [
"src/assets/*"
],
"@components/*": [
"src/components/*"
],
"@pages/*": [
"src/pages/*"
],
"@data/*": [
"src/data/*"
],
"@styles/*": [
"src/styles/*"
],
"@tests/*": [
"src/tests/*"
],
"@images/*": [
"src/assets/images/*"
],
},
"moduleResolution": "node",
"types": [
"node"
]
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
+31
View File
@@ -0,0 +1,31 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vite.dev/config/
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "/src"),
'@assets': path.resolve(__dirname, '/src/assets'),
'@components': path.resolve(__dirname, '/src/components'),
'@pages': path.resolve(__dirname, '/src/pages'),
'@data': path.resolve(__dirname, '/src/data'),
'@styles': path.resolve(__dirname, '/src/assets/styles'),
'@public': path.resolve(__dirname, '/src/public'),
'@tests': path.resolve(__dirname, '/src/tests'),
'@images': path.resolve(__dirname, '/src/assets/images'),
}
},
plugins: [
react(),
],
publicDir: 'public',
build: {
target: ['es2022', 'chrome100', 'firefox100', 'safari15']
},
})