Compare commits
51 Commits
64fbc970ff
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cfdd61cc2 | |||
| d9bdf12328 | |||
| d00712c287 | |||
| 59dd998a5d | |||
| 8d440c1993 | |||
| b855cc9127 | |||
| fe463c4fcb | |||
| 29218b0e38 | |||
| cd684dbaf7 | |||
| 6b9522153f | |||
| 805bd07eb4 | |||
| e08c2d4013 | |||
| 56e8c2c252 | |||
| b1816ec604 | |||
| e8a3611dfb | |||
| 8e2030b0ab | |||
| c539be3c87 | |||
| bce2276dfa | |||
| 7b2566d985 | |||
| e526fac14a | |||
| 5bf0ffbad2 | |||
| 9ae26fbd9f | |||
| 6d32ddbfd0 | |||
| 6ea40432f7 | |||
| 35c0a17f8a | |||
| 9b2c295dd8 | |||
| 45ebea5dab | |||
| 7cab6ddea5 | |||
| 574e87bf99 | |||
| 4cc133fffa | |||
| 142772a08a | |||
| 3da8c4bddf | |||
| 7d5a678008 | |||
| b6894fac9b | |||
| 77ba446d4d | |||
| 762fc64806 | |||
| 3b86302a6e | |||
| 29b9dd93e2 | |||
| d3f7368ba8 | |||
| 7e91215af1 | |||
| 03a9f61a2a | |||
| fdb79c70ad | |||
| fb84bf87de | |||
| 848318a23e | |||
| bb3f67a83f | |||
| f612ee1520 | |||
| 35b34ab5aa | |||
| 2e8777b5c3 | |||
| 1656b07225 | |||
| bdc073f5d7 | |||
| c741dc73bb |
@@ -1,2 +1,151 @@
|
|||||||
# seccodesmith-frontend
|
# SecCodeSmith Frontend Portfolio
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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 🐲
|
||||||
@@ -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 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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!"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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": ""
|
||||||
|
}
|
||||||
@@ -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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
After Width: | Height: | Size: 518 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 847 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
@@ -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
|
||||||
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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">© {currentYear} SecCodeSmith. All rights forged in digital fire.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
@@ -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();
|
||||||
@@ -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();
|
||||||
@@ -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;
|
||||||
|
})
|
||||||
|
}));
|
||||||
@@ -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);',
|
||||||
|
]
|
||||||
@@ -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();
|
||||||
@@ -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();
|
||||||
@@ -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>,
|
||||||
|
)
|
||||||
@@ -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 >)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 >
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface SocialLink {
|
||||||
|
icon: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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']
|
||||||
|
},
|
||||||
|
|
||||||
|
})
|
||||||