Skip to content

Vianch/feat/open toggle menu mobile #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/components/Aside/Aside.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import SignOut from "@/components/ui/icons/SignOut";
/* Lib */
import supabase from "@/lib/supabase/client";
import useMenuStore from "@/lib/store/menu.store";
import useViewPortStore from "@/lib/store/viewPort.store";

/* Utils */
import { useCloseOutsideCodeEditor } from "@/utils/ui.utils";
Expand Down Expand Up @@ -57,6 +58,9 @@ const Aside = ({
const toggleMainMenu = useMenuStore((state) => state.toggleMainMenu);
const closeMainMenu = useMenuStore((state) => state.closeMainMenu);
const closeSnippetList = useMenuStore((state) => state.closeSnippetList);
const openSnippetList = () =>
useMenuStore.setState({ snippetListOpen: true });
const isMobile = useViewPortStore((state) => state.isMobile);
const { menuType } = codeEditorStates;
const router = useRouter();

Expand Down Expand Up @@ -98,6 +102,12 @@ const Aside = ({
default:
break;
}

// If on mobile, open the snippet list and close the main menu after selecting a category
if (isMobile) {
closeMainMenu();
openSnippetList();
}
};

const handlerMobileOpenSnippetList = (): void => {
Expand Down
2 changes: 1 addition & 1 deletion app/components/Aside/aside.module.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
.container {
width: 100%;
background: var(--bg-color-dark);
max-width: 15.625rem;
border-right: 1px solid var(--border-color);
height: 100vh;
position: relative;
Expand Down Expand Up @@ -146,6 +145,7 @@
top: 0;
z-index: 5;
height: calc(100vh - 3.2rem);
max-width: 15.625rem;
transition:
width 0.1s,
visibility 0s;
Expand Down
2 changes: 1 addition & 1 deletion app/components/CodeEditor/codeEditor.module.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.codeEditorContainer {
width: calc(100% - 39.375rem);
width: 100%;
height: calc(100vh - 1.5625rem);
position: relative;
margin-bottom: -1.5625rem;
Expand Down
110 changes: 110 additions & 0 deletions app/components/ResizableLayout/ResizableLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"use client";

import { ReactElement, useRef, useCallback, useEffect, useState } from "react";
import useViewPortStore from "@/lib/store/viewPort.store";
import styles from "./resizableLayout.module.css";

type ResizableLayoutProps = {
aside: ReactElement;
snippetList: ReactElement;
codeEditor: ReactElement;
};

const ResizableLayout = ({
aside,
snippetList,
codeEditor,
}: ResizableLayoutProps): ReactElement => {
const isMobile = useViewPortStore((state) => state.isMobile);
const containerRef = useRef<HTMLDivElement>(null);
const [asideWidth, setAsideWidth] = useState(250);
const [snippetListWidth, setSnippetListWidth] = useState(380);
const [isDragging, setIsDragging] = useState<"aside" | "snippetList" | null>(
null
);

const handleMouseDown = useCallback((resizer: "aside" | "snippetList") => {
setIsDragging(resizer);
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
}, []);

const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (!isDragging || !containerRef.current) return;

const containerRect = containerRef.current.getBoundingClientRect();
const mouseX = e.clientX - containerRect.left;

if (isDragging === "aside") {
const newWidth = Math.max(200, Math.min(400, mouseX));

setAsideWidth(newWidth);
} else if (isDragging === "snippetList") {
const newWidth = Math.max(380, Math.min(600, mouseX - asideWidth));

setSnippetListWidth(newWidth);
}
},
[isDragging, asideWidth]
);

const handleMouseUp = useCallback(() => {
setIsDragging(null);
document.body.style.cursor = "";
document.body.style.userSelect = "";
}, []);

useEffect(() => {
if (isDragging) {
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);

return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}

return undefined;
}, [isDragging, handleMouseMove, handleMouseUp]);

// For mobile, return the original layout without resize functionality
if (isMobile) {
return (
<div className={styles.mobileContainer}>
{aside}
{snippetList}
{codeEditor}
</div>
);
}

return (
<div ref={containerRef} className={styles.container}>
<div className={styles.panel} style={{ width: `${asideWidth}px` }}>
{aside}
</div>

<div
className={styles.resizer}
onMouseDown={() => handleMouseDown("aside")}
/>

<div className={styles.panel} style={{ width: `${snippetListWidth}px` }}>
{snippetList}
</div>

<div
className={styles.resizer}
onMouseDown={() => handleMouseDown("snippetList")}
/>

<div className={styles.panel} style={{ flex: 1 }}>
{codeEditor}
</div>
</div>
);
};

export default ResizableLayout;
70 changes: 70 additions & 0 deletions app/components/ResizableLayout/resizableLayout.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
.container {
display: flex;
flex-direction: row;
height: 100vh;
overflow: hidden;
}

.mobileContainer {
display: flex;
flex-direction: row;
overflow: hidden;
}

/* On mobile, CodeEditor should have calculated width to account for fixed-width aside and snippet list */
@media (width < 1140px) {
.mobileContainer > *:last-child {
width: 100vw;
}
}

.panel {
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
}

.resizer {
width: 4px;
background: var(--border-color);
cursor: col-resize;
flex-shrink: 0;
position: relative;
transition: background-color 0.2s ease;
}

.resizer:hover {
background: var(--foreground-color);
}

.resizer:active {
background: var(--cyan-color);
}

/* Visual indicator for resize handle */
.resizer::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 2px;
height: 20px;
background: currentcolor;
opacity: 0.5;
}

/* Hide resizers on mobile */
@media (width < 1140px) {
.resizer {
display: none;
}

.container .panel {
width: auto !important;
flex: unset !important;
}
}
12 changes: 12 additions & 0 deletions app/components/SnippetItem/SnippetItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { FC, ReactElement, MouseEvent } from "react";
import Trash from "@/components/ui/icons/Trash";
import Restore from "@/components/ui/icons/Restore";

/* Stores */
import useMenuStore from "@/lib/store/menu.store";
import useViewPortStore from "@/lib/store/viewPort.store";

import styles from "@/components/SnippetList/snippetlist.module.css";

interface SnippetItemPropsComponent extends SnippetItemProps {
Expand All @@ -23,13 +27,21 @@ const SnippetItem: FC<SnippetItemPropsComponent> = ({
onDeleteSnippet,
onRestoreSnippet,
}: SnippetItemPropsComponent): ReactElement => {
const isMobile = useViewPortStore((state) => state.isMobile);
const closeSnippetList = useMenuStore((state) => state.closeSnippetList);

if (snippet) {
const snippetClickHandler = (
event: MouseEvent<HTMLLIElement>,
index: number
): void => {
event.preventDefault();
onActiveSnippet(index);

// Close the snippet list on mobile when a snippet is selected
if (isMobile) {
closeSnippetList();
}
};

const isSnippetActive = activeSnippetIndex === originalIndex;
Expand Down
16 changes: 12 additions & 4 deletions app/components/SnippetList/snippetlist.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
display: block;
width: 100%;
background: var(--bg-color-dark);
max-width: 23.75rem;
border-right: 1px solid var(--border-color);
height: 100vh;
transition:
Expand All @@ -55,6 +54,13 @@
visibility 0.14s;
}

/* Add max-width back for mobile compatibility */
@media (width < 1140px) {
.snippetsListContainer {
max-width: 23.75rem;
}
}

.fields {
display: flex;
flex-flow: row nowrap;
Expand Down Expand Up @@ -193,7 +199,9 @@
}

@media (width <= 480px) {
max-width: inherit;
height: auto;
width: 100%;
.snippetsListContainer {
max-width: inherit;
height: auto;
width: 100%;
}
}
6 changes: 6 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export default function RootLayout({
}) {
return (
<html lang="en">
<head>
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"
/>
</head>
<body className={inter.className}>
{children}
<Analytics />
Expand Down
2 changes: 1 addition & 1 deletion app/lib/config/languages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
enum SupportedLanguages {
const enum SupportedLanguages {
JavaScript = "JavaScript",
CSS = "css",
HTML = "html",
Expand Down
2 changes: 1 addition & 1 deletion app/lib/constants/core.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export enum MenuItems {
export const enum MenuItems {
All = "all",
Favorites = "favorites",
Trash = "trash",
Expand Down
2 changes: 1 addition & 1 deletion app/lib/constants/form.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export enum FormMessageTypes {
export const enum FormMessageTypes {
Error = "error",
Success = "success",
Warning = "warning",
Expand Down
4 changes: 2 additions & 2 deletions app/lib/constants/toast.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
export enum ToastType {
export const enum ToastType {
Default = "default",
Info = "info",
Success = "success",
Warning = "warning",
Error = "error",
}

export enum ToastPositions {
export const enum ToastPositions {
BottomRight = "bottom-right",
BottomLeft = "bottom-left",
BottomCenter = "bottom-center",
Expand Down
Loading