diff --git a/.gitignore b/.gitignore index 1437c53..8b78257 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ # next.js /.next/ /out/ +package-lock.json # production /build @@ -32,3 +33,5 @@ yarn-error.log* # vercel .vercel + +/tmp \ No newline at end of file diff --git a/README.md b/README.md index 3b47cba..53b17a4 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,51 @@ npm run dev - Feel free to use it as your own portfolio - Contributions are welcome +## Notebooks rendering with Quarto + +1. Transform the jupyter notebook into a qmd file: + +``` +quarto convert isic-2024_kaggle.ipynb -o isic-2024_kaggle.qmd +``` + +2. Transform the qmd file to suit your layout needs, for example add this at the head of the qmd file: + +``` +--- +format: + html: + title: '' + toc: true + toc-title: "Table of contents" + toc-location: right + toc-depth: 5 + toc-expand: true + toc-float: + collapsed: true + smooth-scroll: true + width: 300px + theme: cosmo + code-fold: false + code-fold-show: true + page-layout: full + number-sections: true + code-tools: false + code-line-numbers: false + code-summary: "Show Code" + code-block-bg: true + include-in-header: styles/toc-scrollbar-hide.html +execute: + enabled: false +--- +``` + +3. Convert the qmd into an html file: + +``` +quarto render isic-2024_kaggle.qmd +``` + ### License [MIT](https://github.com/realstoman/nextjs-tailwindcss-portfolio/blob/main/LICENSE) diff --git a/clean_project.sh b/clean_project.sh new file mode 100644 index 0000000..17f8c79 --- /dev/null +++ b/clean_project.sh @@ -0,0 +1,34 @@ +rm -rf .next node_modules package-lock.json yarn.lock +npm cache clean --force +npm install +npm run build +npm run dev + + +npm install next@latest react@latest react-dom@latest --force +npm install +npm run build +npm run dev + + + +npm install framer-motion@latest +rm -rf node_modules package-lock.json .next +npm install +npm run build +npm run dev + + +brew install trash +trash node_modules +rm -rf ~/.Trash/* + +npm install + + + +trash node_modules +rm -rf ~/.Trash/* +rm -rf .next node_modules package-lock.json yarn.lock +npm install +npm run dev \ No newline at end of file diff --git a/components/HireMeModal.jsx b/components/HireMeModal.jsx index c9d0784..11dbfca 100644 --- a/components/HireMeModal.jsx +++ b/components/HireMeModal.jsx @@ -3,10 +3,11 @@ import { FiX } from 'react-icons/fi'; import Button from './reusable/Button'; const selectOptions = [ - 'Web Application', - 'Mobile Application', - 'UI/UX Design', - 'Branding', + 'Data Science', + 'Generative AI', + 'Data Engineering', + 'Software Engineering', + 'Web Application' ]; function HireMeModal({ onClose, onRequest }) { @@ -25,23 +26,22 @@ function HireMeModal({ onClose, onRequest }) {
-
+
What project are you looking for?
-
{ - e.preventDefault(); - }} - className="max-w-xl m-4 text-left" - > +
- -
diff --git a/components/PagesMetaHead.jsx b/components/PagesMetaHead.jsx index cebe2b8..804cf93 100644 --- a/components/PagesMetaHead.jsx +++ b/components/PagesMetaHead.jsx @@ -10,7 +10,7 @@ function PagesMetaHead({ title, keywords, description }) { - + {title} ); @@ -22,4 +22,4 @@ PagesMetaHead.defaultProps = { keywords: 'Simple and multi-page next.js and react application', }; -export default PagesMetaHead; +export default PagesMetaHead; \ No newline at end of file diff --git a/components/TableOfContents.jsx b/components/TableOfContents.jsx new file mode 100644 index 0000000..81b80e5 --- /dev/null +++ b/components/TableOfContents.jsx @@ -0,0 +1,93 @@ +'use client' + +import { useEffect, useState, useRef } from 'react' + +export default function TableOfContents({ className = '' }) { + const [headings, setHeadings] = useState([]) + const [activeId, setActiveId] = useState(null) + const itemRefs = useRef({}) + + useEffect(() => { + const timeout = setTimeout(() => { + const elements = Array.from(document.querySelectorAll('h2[id], h3[id]')) + const newHeadings = elements.map((el) => ({ + id: el.id, + text: el.textContent || '', + level: el.tagName === 'H2' ? 2 : 3, + })) + setHeadings(newHeadings) + }, 500) + + return () => clearTimeout(timeout) + }, []) + + useEffect(() => { + if (!headings.length) return + + const observer = new IntersectionObserver( + (entries) => { + const visible = entries + .filter((entry) => entry.isIntersecting) + .sort((a, b) => a.target.offsetTop - b.target.offsetTop) + + if (visible.length > 0) { + setActiveId(visible[0].target.id) + } + }, + { + rootMargin: '-40% 0% -40% 0%', + threshold: 0.1, + } + ) + + const elements = document.querySelectorAll('h2[id], h3[id]') + elements.forEach((el) => observer.observe(el)) + + return () => { + elements.forEach((el) => observer.unobserve(el)) + } + }, [headings]) + + if (!headings.length) return null + + return ( + + ) +} diff --git a/components/ThemeToggle.js b/components/ThemeToggle.js new file mode 100644 index 0000000..2607846 --- /dev/null +++ b/components/ThemeToggle.js @@ -0,0 +1,21 @@ +// components/ThemeToggle.js +import { useTheme } from 'next-themes'; +import { useEffect, useState } from 'react'; + +export default function ThemeToggle() { + const { theme, setTheme, resolvedTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + + useEffect(() => setMounted(true), []); + + if (!mounted) return null; // avoids hydration error + + return ( + + ); +} diff --git a/components/about/AboutClientSingle.jsx b/components/about/AboutClientSingle.jsx index 92f733f..a91d138 100644 --- a/components/about/AboutClientSingle.jsx +++ b/components/about/AboutClientSingle.jsx @@ -2,14 +2,14 @@ import Image from 'next/image'; function AboutClientSingle({ title, image }) { return ( -
+
{title} +
); } diff --git a/components/about/AboutCounter.jsx b/components/about/AboutCounter.jsx index 61a1d31..316fbe8 100644 --- a/components/about/AboutCounter.jsx +++ b/components/about/AboutCounter.jsx @@ -2,40 +2,38 @@ import { useCountUp } from 'react-countup'; import CounterItem from './CounterItem'; function AboutCounter() { - useCountUp({ ref: 'experienceCounter', end: 12, duration: 2 }); - useCountUp({ ref: 'githubStarsCounter', end: 20, duration: 2 }); - useCountUp({ ref: 'feedbackCounter', end: 92, duration: 2 }); - useCountUp({ ref: 'projectsCounter', end: 77, duration: 2 }); + useCountUp({ ref: 'experienceCounter', end: 9, duration: 2 }); + useCountUp({ ref: 'projectsRealizedCounter', end: 50, duration: 2 }); + useCountUp({ ref: 'feedbackCounter', end: 94, duration: 2 }); + useCountUp({ ref: 'projectsCounter', end: 83, duration: 2 }); - return ( -
-
- } - measurement="" - /> - - } - measurement="k+" - /> - - } - measurement="%" - /> - - } - measurement="%" - /> -
-
- ); + return ( +
+ {/* inner container for centering */} +
+ } + measurement="" + /> + } + measurement="+" + /> + } + measurement="%" + /> + } + measurement="%" + /> +
+
+ ); } export default AboutCounter; diff --git a/components/about/AboutMeBio.jsx b/components/about/AboutMeBio.jsx index a44feeb..e7ff656 100644 --- a/components/about/AboutMeBio.jsx +++ b/components/about/AboutMeBio.jsx @@ -1,30 +1,64 @@ import Image from 'next/image'; import { useState } from 'react'; import { aboutMeData } from '../../data/aboutMeData'; +import { motion } from 'framer-motion'; function AboutMeBio() { - const [aboutMe, setAboutMe] = useState(aboutMeData); - return ( -
-
- Profile Image -
+ const [aboutMe] = useState(aboutMeData); -
- {aboutMe.map((bio) => ( -

- {bio.bio} + return ( +

+ {/* Intro block (not animated) */} + {aboutMe[0] && ( +
+

+ {aboutMe[0].bio}

- ))} +
+ )} + + {/* Profile image + timeline */} +
+ {/* Profile image on the left */} +
+
+ Profile Image +
+
+ + {/* Timeline on the right */} +
+ {/* Vertical timeline line */} +
+ + {/* Timeline entries */} +
+ {aboutMe.slice(1).map((bio, index) => ( + + {/* Timeline dot */} + + + {/* Text block */} +

+ {bio.bio} +

+
+ ))} +
+
); diff --git a/components/blog/BlogGrid.js b/components/blog/BlogGrid.js new file mode 100644 index 0000000..5f07099 --- /dev/null +++ b/components/blog/BlogGrid.js @@ -0,0 +1,65 @@ +// components/blog/BlogGrid.js +import { useState } from 'react'; +import BlogSingle from './BlogSingle'; +import FilterDropdown from '../shared/FilterDropdown'; +import SearchInput from '../shared/SearchInput'; + +const selectOptions = [ + 'Machine Learning', + 'Data Science', + 'AI Engineering', + 'Deep Learning', + 'Sport', + 'Reading', + 'Travel', +]; + +function BlogGrid({ blogs, isBlog = true }) { + const [searchBlog, setSearchBlog] = useState(''); + const [selectCategory, setSelectCategory] = useState('all'); + + const filteredBlogs = blogs.filter((blog) => { + const matchesSearch = blog.title.toLowerCase().includes(searchBlog.toLowerCase()); + const matchesCategory = selectCategory === 'all' || blog.category === selectCategory; + return matchesSearch && matchesCategory; + }); + + return ( +
+
+

+ Blog Articles +

+
+ +
+

+ Search articles by title or filter by category +

+ +
+ +
+ +
+
+
+ +
+ {filteredBlogs.map((blog, index) => ( + + ))} +
+
+ ); +} + +export default BlogGrid; diff --git a/components/blog/BlogSingle.js b/components/blog/BlogSingle.js new file mode 100644 index 0000000..f9939cd --- /dev/null +++ b/components/blog/BlogSingle.js @@ -0,0 +1,46 @@ +import { FiTag } from 'react-icons/fi'; +import Image from 'next/image'; +import Link from 'next/link'; + +function BlogSingle({ title, img, publishDate, slug, tags = [] }) { + return ( + +
+
+ {title} +
+
+

+ {title} +

+ + {publishDate} + + + {Array.isArray(tags) && tags.length > 0 && ( +
+ + {tags.map((tag) => ( + + {tag} + + ))} +
+ )} +
+
+ + ); +} + +export default BlogSingle; + \ No newline at end of file diff --git a/components/blog/RelatedBlogs.js b/components/blog/RelatedBlogs.js new file mode 100644 index 0000000..e69de29 diff --git a/components/blog/layouts/DefaultBlogLayout.js b/components/blog/layouts/DefaultBlogLayout.js new file mode 100644 index 0000000..cf6250d --- /dev/null +++ b/components/blog/layouts/DefaultBlogLayout.js @@ -0,0 +1,30 @@ +import PagesMetaHead from '../../PagesMetaHead'; +import BackButton from '../../reusable/BackButton'; +import Container from '../../layout/Container'; +import AppHeader from '../../shared/AppHeader'; +import AppFooter from '../../shared/AppFooter'; + +export default function DefaultBlogLayout({ children, isBlog = true}) { + return ( + + <> + + + + + + + + +
{children}
+ +
+ + + + + ); +} + + + diff --git a/components/contact/ContactDetails.jsx b/components/contact/ContactDetails.jsx index ffa6244..7bcfc24 100644 --- a/components/contact/ContactDetails.jsx +++ b/components/contact/ContactDetails.jsx @@ -3,17 +3,17 @@ import { FiPhone, FiMapPin, FiMail } from 'react-icons/fi'; const contacts = [ { id: 1, - name: 'Your Address, Your City, Your Country', + name: '114 Avenue de la republique, 94300, Vincennes', icon: , }, { id: 2, - name: 'email@domain.com', + name: 'mamanesarki@yahoo.fr', icon: , }, { id: 3, - name: '555 8888 888', + name: '+(33)6.52.78.18.48', icon: , }, ]; diff --git a/components/contact/ContactForm.jsx b/components/contact/ContactForm.jsx index c7fb515..221d468 100644 --- a/components/contact/ContactForm.jsx +++ b/components/contact/ContactForm.jsx @@ -6,11 +6,11 @@ function ContactForm() {
{ - e.preventDefault(); - }} + action="https://formspree.io/f/mldjbkwy" + method="POST" className="max-w-xl m-4 p-6 sm:p-10 bg-secondary-light dark:bg-secondary-dark rounded-xl shadow-xl text-left" > +

Contact Form

diff --git a/components/layout/Container.jsx b/components/layout/Container.jsx new file mode 100644 index 0000000..aa49a45 --- /dev/null +++ b/components/layout/Container.jsx @@ -0,0 +1,9 @@ +export default function Container({ children, isBlog = false, fullWidth = false }) { + return ( +
+
+ {children} +
+
+ ); +} diff --git a/components/layout/DefaultLayout.jsx b/components/layout/DefaultLayout.jsx index 4a22961..4f9d20a 100644 --- a/components/layout/DefaultLayout.jsx +++ b/components/layout/DefaultLayout.jsx @@ -1,16 +1,26 @@ +// components/layout/DefaultLayout.jsx + import AppHeader from '../shared/AppHeader'; import AppFooter from '../shared/AppFooter'; import PagesMetaHead from '../PagesMetaHead'; +import Container from './Container'; + const DefaultLayout = ({ children }) => { return ( - <> - - -
{children}
- - + +
+ + + {children} + + + + +
+ ); -}; + }; + export default DefaultLayout; diff --git a/components/projects/NotebookViewer.jsx b/components/projects/NotebookViewer.jsx new file mode 100644 index 0000000..18d1851 --- /dev/null +++ b/components/projects/NotebookViewer.jsx @@ -0,0 +1,53 @@ +import { useEffect, useRef, useState } from 'react'; +import { Fullscreen, Minimize } from 'lucide-react'; + +const NotebookViewer = ({ notebookFile }) => { + const containerRef = useRef(null); + const [isFullscreen, setIsFullscreen] = useState(false); + + const toggleFullscreen = () => { + if (!document.fullscreenElement) { + containerRef.current?.requestFullscreen(); + } else { + document.exitFullscreen(); + } + }; + + useEffect(() => { + const handleFullscreenChange = () => { + setIsFullscreen(!!document.fullscreenElement); + }; + + document.addEventListener('fullscreenchange', handleFullscreenChange); + return () => { + document.removeEventListener('fullscreenchange', handleFullscreenChange); + }; + }, []); + + // Prevent rendering if no notebook file is provided + if (!notebookFile) return null; + + return ( +
+ + +
+ ); +}; + +export default NotebookViewer; diff --git a/components/projects/ProjectHeader.jsx b/components/projects/ProjectHeader.jsx new file mode 100644 index 0000000..7dd681d --- /dev/null +++ b/components/projects/ProjectHeader.jsx @@ -0,0 +1,29 @@ +import { FiClock, FiTag } from "react-icons/fi"; + +export default function ProjectHeader({ project }) { + return ( +
+

+ {project.ProjectHeader.title} +

+
+
+ + + {project.ProjectHeader.publishDate} + +
+
+ + + {project.ProjectHeader.tags} + +
+
+
+ ); +} + + + + diff --git a/components/projects/ProjectSingle.jsx b/components/projects/ProjectSingle.jsx index 76f69e2..00364b1 100644 --- a/components/projects/ProjectSingle.jsx +++ b/components/projects/ProjectSingle.jsx @@ -1,10 +1,15 @@ import { motion } from 'framer-motion'; import Image from 'next/image'; import Link from 'next/link'; +import { useRef, useState } from 'react'; +import { FiTag } from 'react-icons/fi'; const imageStyle = { maxWidth: '100%', height: 'auto' }; const ProjectSingle = (props) => { + const iframeRef = useRef(null); + const [isFullscreen, setIsFullscreen] = useState(false); + return ( { passHref >
-
+
Single Project
+
-

- {props.title} -

- - {props.category} +

{props.title}

+
+ + + + + + {props.ProjectHeader.tags}
+ +
+ +
); }; + + export default ProjectSingle; diff --git a/components/projects/ProjectTabs.jsx b/components/projects/ProjectTabs.jsx new file mode 100644 index 0000000..36e5108 --- /dev/null +++ b/components/projects/ProjectTabs.jsx @@ -0,0 +1,108 @@ +import { useState, useEffect } from "react"; +import NotebookViewer from "./NotebookViewer"; +import ReactMarkdown from "react-markdown"; + +const DEFAULT_NOTEBOOK_TABS = [ + "Overview", + "Key Impact", + "Challenge Highlights", + "Goal", + "Tools & Technologies", + "Implementation" +]; + +export default function ProjectTabs({ project }) { + const tabs = project.ProjectTabs?.length > 0 + ? project.ProjectTabs + : project.type === "notebook" + ? DEFAULT_NOTEBOOK_TABS + : []; + + const [activeTab, setActiveTab] = useState(tabs[0]); + + useEffect(() => { + setActiveTab(tabs[0]); + }, [project]); + + return ( +
+ {/* Tab bar */} +
+
+ {tabs.map((tab) => ( + + ))} +
+
+ + {/* Tab content */} +
+ {activeTab in project.ProjectInfo && ( + Array.isArray(project.ProjectInfo[activeTab]) ? ( + activeTab === "Tools & Technologies" ? ( +

{project.ProjectInfo[activeTab].join(", ")}

+ ) : ( +
    + {project.ProjectInfo[activeTab].map((item) => ( +
  • + + {item.title} + : {item.details} +
  • + ))} +
+ ) + ) : ( +
+

{children}

, + h1: ({ children }) =>

{children}

, + h2: ({ children }) =>

{children}

, + h3: ({ children }) =>

{children}

, + ul: ({ children }) => ( +
    + {children} +
+ ), + li: ({ children }) => ( +
  • + {children} +
  • + ), + ol: ({ children }) =>
      {children}
    , + a: ({ href, children }) => ( + + {children} + + ), + strong: ({ children }) => {children}, + em: ({ children }) => {children}, + }} + > + {project.ProjectInfo[activeTab]} +
    +
    + ) + )} + + {activeTab === "Implementation" && project.Notebook?.file && ( + + )} +
    +
    + ); +} diff --git a/components/projects/ProjectsFilter.jsx b/components/projects/ProjectsFilter.jsx deleted file mode 100644 index 95bacf5..0000000 --- a/components/projects/ProjectsFilter.jsx +++ /dev/null @@ -1,43 +0,0 @@ -const selectOptions = [ - 'Web Application', - 'Mobile Application', - 'UI/UX Design', - 'Branding', -]; - -function ProjectsFilter({ setSelectProject }) { - return ( - - ); -} - -export default ProjectsFilter; diff --git a/components/projects/ProjectsGrid.jsx b/components/projects/ProjectsGrid.jsx index 05f700b..6eda0ce 100644 --- a/components/projects/ProjectsGrid.jsx +++ b/components/projects/ProjectsGrid.jsx @@ -1,122 +1,68 @@ +// components/projects/ProjectsGrid.js import { useState } from 'react'; -import { FiSearch } from 'react-icons/fi'; import ProjectSingle from './ProjectSingle'; import { projectsData } from '../../data/projectsData'; -import ProjectsFilter from './ProjectsFilter'; +import FilterDropdown from '../shared/FilterDropdown'; +import SearchInput from '../shared/SearchInput'; -function ProjectsGrid() { - const [searchProject, setSearchProject] = useState(); - const [selectProject, setSelectProject] = useState(); +const selectOptions = [ + 'Data Science', + 'Generative AI', + 'Data Engineering', + 'Software Engineering', + 'Web Application', +]; - // @todo - To be fixed - // const searchProjectsByTitle = projectsData.filter((item) => { - // const result = item.title - // .toLowerCase() - // .includes(searchProject.toLowerCase()) - // ? item - // : searchProject == '' - // ? item - // : ''; - // return result; - // }); +function ProjectsGrid() { + const [searchProject, setSearchProject] = useState(''); + const [selectProject, setSelectProject] = useState('all'); - const selectProjectsByCategory = projectsData.filter((item) => { - let category = - item.category.charAt(0).toUpperCase() + item.category.slice(1); - return category.includes(selectProject); - }); + const filteredProjects = projectsData.filter((item) => { + const matchesCategory = + selectProject === 'all' || item.category.toLowerCase() === selectProject.toLowerCase(); + const matchesSearch = + item.title.toLowerCase().includes(searchProject.toLowerCase()); + return matchesCategory && matchesSearch; + }); - return ( -
    -
    -

    - Projects portfolio -

    -
    + return ( +
    +
    +

    + Projects Portfolio +

    +
    -
    -

    - Search projects by title or filter by category -

    -
    -
    - - - - { - setSearchProject(e.target.value); - }} - className=" - ont-general-medium - pl-3 - pr-1 - sm:px-4 - py-2 - border - border-gray-200 - dark:border-secondary-dark - rounded-lg - text-sm - sm:text-md - bg-secondary-light - dark:bg-ternary-dark - text-primary-dark - dark:text-ternary-light - " - id="name" - name="name" - type="search" - required="" - placeholder="Search Projects" - aria-label="Name" - /> -
    +
    +

    + Search projects by title or filter by category +

    - -
    -
    +
    +
    + +
    + +
    +
    +
    +
    -
    - {selectProject - ? selectProjectsByCategory.map((project, index) => { - return ; - }) - : projectsData.map((project, index) => ( - - ))} -
    -
    - ); +
    + {filteredProjects.map((project, index) => ( + + ))} +
    +
    + ); } export default ProjectsGrid; diff --git a/components/projects/RelatedProjects.jsx b/components/projects/RelatedProjects.jsx index f1a3c83..64ba77b 100644 --- a/components/projects/RelatedProjects.jsx +++ b/components/projects/RelatedProjects.jsx @@ -1,55 +1,58 @@ import Image from 'next/image'; -import { v4 as uuidv4 } from 'uuid'; +import Link from 'next/link'; +import { projectsData } from '../../data/projectsData'; +import { FiTag } from 'react-icons/fi'; -const RelatedProject = { - title: 'Related Projects', - Projects: [ - { - id: uuidv4(), - title: 'Mobile UI', - img: '/images/ui-project-2.jpg', - }, - { - id: uuidv4(), - title: 'Web Application', - img: '/images/mobile-project-1.jpg', - }, - { - id: uuidv4(), - title: 'UI Design', - img: '/images/web-project-1.jpg', - }, - { - id: uuidv4(), - title: 'Kabul Mobile App UI', - img: '/images/mobile-project-2.jpg', - }, - ], -}; +function RelatedProjects({ currentProject, isBlog = false }) { + if (!currentProject) return null; -function RelatedProjects() { - return ( -
    -

    - {RelatedProject.title} -

    + const relatedProjects = projectsData.filter( + (project) => + project.category === currentProject.category && + project.id !== currentProject.id + ); -
    - {RelatedProject.Projects.map((project) => { - return ( - {project.title} - ); - })} -
    -
    - ); + if (relatedProjects.length === 0) return null; + + return ( +
    +
    +

    + Related Projects +

    + +
    + {relatedProjects.map((project) => ( + +
    +
    + {project.title} +
    + +
    +

    + {project.title} +

    +
    + + + {project.ProjectHeader.tags} + +
    +
    +
    + + ))} +
    +
    + +
    + ); } export default RelatedProjects; diff --git a/components/projects/layouts/DefaultProjectLayout.jsx b/components/projects/layouts/DefaultProjectLayout.jsx new file mode 100644 index 0000000..25c3bcf --- /dev/null +++ b/components/projects/layouts/DefaultProjectLayout.jsx @@ -0,0 +1,18 @@ +// components/projects/layouts/DefaultProjectLayout.jsx + +import ProjectHeader from "../ProjectHeader"; +import ProjectTabs from "../ProjectTabs"; +import BackButton from "../../reusable/BackButton"; +import RelatedProjects from "../RelatedProjects"; +import Container from "../../layout/Container"; + +export default function DefaultProjectLayout({ project }) { + return ( + + + + + + + ); +} diff --git a/components/projects/layouts/NotebookProjectLayout.jsx b/components/projects/layouts/NotebookProjectLayout.jsx new file mode 100644 index 0000000..0183116 --- /dev/null +++ b/components/projects/layouts/NotebookProjectLayout.jsx @@ -0,0 +1,36 @@ +import ProjectHeader from "../ProjectHeader"; +import ProjectTabs from "../ProjectTabs"; +import BackButton from "../../reusable/BackButton"; +import RelatedProjects from "../RelatedProjects"; +import Container from '../../layout/Container'; +import AppFooter from '../../shared/AppFooter'; +import PagesMetaHead from '../../PagesMetaHead'; +import AppHeader from '../../shared/AppHeader'; + + +export default function NotebookProjectLayout({ project, isBlog=false}) { + return ( + <> + + + + + + + + + + + + + + + + + ); +} + + + + + diff --git a/components/projects/layouts/WebAppProjectLayout.jsx b/components/projects/layouts/WebAppProjectLayout.jsx new file mode 100644 index 0000000..a967139 --- /dev/null +++ b/components/projects/layouts/WebAppProjectLayout.jsx @@ -0,0 +1,26 @@ +import ProjectHeader from "../ProjectHeader"; +import ProjectTabs from "../ProjectTabs"; +import BackButton from "../../reusable/BackButton"; +import RelatedProjects from "../RelatedProjects"; + +export default function WebAppProjectLayout({ project }) { + return ( +
    + + + + {/* Web Apps : just a demo */} +
    +