mirror of
https://github.com/khairul169/code-share.git
synced 2025-04-28 16:49:36 +07:00
feat: project bootstrap
This commit is contained in:
parent
1de8ccd2fd
commit
6b278b092e
17
components.json
Normal file
17
components.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": false,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
27
package.json
27
package.json
@ -9,19 +9,38 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.2.1",
|
||||
"@codemirror/lang-html": "^6.4.8",
|
||||
"@codemirror/lang-javascript": "^6.2.1",
|
||||
"@tanstack/react-query": "^5.21.7",
|
||||
"@trpc/client": "11.0.0-next-beta.289",
|
||||
"@trpc/next": "11.0.0-next-beta.289",
|
||||
"@trpc/react-query": "11.0.0-next-beta.289",
|
||||
"@trpc/server": "11.0.0-next-beta.289",
|
||||
"@uiw/codemirror-theme-tokyo-night": "^4.21.22",
|
||||
"@uiw/react-codemirror": "^4.21.22",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"lucide-react": "^0.331.0",
|
||||
"next": "14.1.0",
|
||||
"prettier": "^3.2.5",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"next": "14.1.0"
|
||||
"react-resizable-panels": "^2.0.9",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"usehooks-ts": "^2.14.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.1.0",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.1.0"
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
544
pnpm-lock.yaml
generated
544
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
13
src/app/api/trpc/[trpc]/route.ts
Normal file
13
src/app/api/trpc/[trpc]/route.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
||||
import { appRouter } from "@/server/routers/_app";
|
||||
import { createContext } from "@/server/context";
|
||||
|
||||
const handler = (req: Request) =>
|
||||
fetchRequestHandler({
|
||||
endpoint: "/api/trpc",
|
||||
req,
|
||||
router: appRouter,
|
||||
createContext,
|
||||
});
|
||||
|
||||
export { handler as GET, handler as POST };
|
@ -2,32 +2,10 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))
|
||||
)
|
||||
rgb(var(--background-start-rgb));
|
||||
@apply bg-slate-400 text-white;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
.cm-theme {
|
||||
@apply h-full;
|
||||
}
|
||||
|
@ -1,22 +1,27 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import Providers from "./providers";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Code Share",
|
||||
description: "Code sharing app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
};
|
||||
|
||||
const RootLayout = ({ children }: Props) => {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
<html lang="en" className="dark">
|
||||
<body className={inter.className}>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default RootLayout;
|
||||
|
252
src/app/page.tsx
252
src/app/page.tsx
@ -1,113 +1,151 @@
|
||||
import Image from "next/image";
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "@/components/ui/resizable";
|
||||
import { useMediaQuery } from "@/hooks/useMediaQuery";
|
||||
import Panel from "@/components/ui/panel";
|
||||
import CodeEditor from "@/components/ui/code-editor";
|
||||
import { ReactCodeMirrorRef } from "@uiw/react-codemirror";
|
||||
|
||||
import prettier from "prettier/standalone";
|
||||
import prettierHtmlPlugin from "prettier/plugins/html";
|
||||
|
||||
const HomePage = () => {
|
||||
const codeMirror = useRef<ReactCodeMirrorRef>(null);
|
||||
const frameRef = useRef<HTMLIFrameElement>(null);
|
||||
const [isMounted, setMounted] = useState(false);
|
||||
const isMobile = useMediaQuery("(max-width: 639px)");
|
||||
const [data, setData] = useState("");
|
||||
const [lang, setLang] = useState("html");
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (frameRef.current) {
|
||||
frameRef.current.srcdoc = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<title>Code-Share</title>
|
||||
</head>
|
||||
<body>
|
||||
${data}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onFormat = useCallback(async () => {
|
||||
const cursor = codeMirror.current?.view?.state.selection.main.head || 0;
|
||||
|
||||
const { formatted, cursorOffset } = await prettier.formatWithCursor(data, {
|
||||
parser: "html",
|
||||
plugins: [prettierHtmlPlugin],
|
||||
cursorOffset: cursor,
|
||||
});
|
||||
|
||||
const cm = codeMirror.current?.view;
|
||||
setData(formatted);
|
||||
cm?.dispatch({
|
||||
changes: { from: 0, to: cm?.state.doc.length, insert: formatted },
|
||||
});
|
||||
cm?.dispatch({
|
||||
selection: { anchor: cursorOffset },
|
||||
});
|
||||
|
||||
// setTimeout(() => {
|
||||
// }, 100);
|
||||
}, [data, setData]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: any) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
|
||||
event.preventDefault();
|
||||
onFormat();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [onFormat]);
|
||||
|
||||
if (!isMounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
||||
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
|
||||
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
|
||||
Get started by editing
|
||||
<code className="font-mono font-bold">src/app/page.tsx</code>
|
||||
</p>
|
||||
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
|
||||
<a
|
||||
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<ResizablePanelGroup
|
||||
autoSaveId="main-panel"
|
||||
direction={isMobile ? "vertical" : "horizontal"}
|
||||
className="w-full !h-dvh"
|
||||
>
|
||||
<ResizablePanel
|
||||
defaultSize={isMobile ? 50 : 60}
|
||||
minSize={20}
|
||||
className="p-4 pr-0"
|
||||
>
|
||||
<Panel>
|
||||
<ResizablePanelGroup
|
||||
autoSaveId="editor-panel"
|
||||
direction="horizontal"
|
||||
className="border-t border-t-slate-900"
|
||||
>
|
||||
By{" "}
|
||||
<Image
|
||||
src="/vercel.svg"
|
||||
alt="Vercel Logo"
|
||||
className="dark:invert"
|
||||
width={100}
|
||||
height={24}
|
||||
priority
|
||||
<ResizablePanel
|
||||
defaultSize={25}
|
||||
minSize={10}
|
||||
collapsible
|
||||
collapsedSize={0}
|
||||
className="bg-[#1e2536]"
|
||||
>
|
||||
File List
|
||||
</ResizablePanel>
|
||||
<ResizableHandle className="bg-slate-900" />
|
||||
<ResizablePanel defaultSize={75}>
|
||||
<button onClick={() => setLang("html")}>HTML</button>
|
||||
<button onClick={() => setLang("css")}>CSS</button>
|
||||
<button onClick={() => setLang("js")}>Javascript</button>
|
||||
<button onClick={onFormat}>Format</button>
|
||||
|
||||
<CodeEditor
|
||||
ref={codeMirror}
|
||||
lang={lang}
|
||||
value={data}
|
||||
onChange={setData}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</Panel>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle className="bg-transparent hover:bg-slate-500 transition-colors mx-1 my-4 w-2 rounded-lg" />
|
||||
<ResizablePanel
|
||||
defaultSize={isMobile ? 50 : 40}
|
||||
collapsible
|
||||
collapsedSize={0}
|
||||
minSize={10}
|
||||
>
|
||||
<div className="w-full h-full p-4 pl-0">
|
||||
<Panel>
|
||||
<iframe
|
||||
ref={frameRef}
|
||||
className="border-none w-full h-full bg-white"
|
||||
/>
|
||||
</a>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-full sm:before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full sm:after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
|
||||
<Image
|
||||
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js Logo"
|
||||
width={180}
|
||||
height={37}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
|
||||
<a
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Docs{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Find in-depth information about Next.js features and API.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Learn{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Learn about Next.js in an interactive course with quizzes!
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Templates{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Explore starter templates for Next.js.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Deploy{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50 text-balance`}>
|
||||
Instantly deploy your Next.js site to a shareable URL with Vercel.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
|
43
src/app/providers.tsx
Normal file
43
src/app/providers.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import trpc, { getBaseUrl } from "@/lib/trpc";
|
||||
import { httpBatchLink } from "@trpc/react-query";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const Providers = ({ children }: Props) => {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
const [trpcClient] = useState(() =>
|
||||
trpc.createClient({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: getBaseUrl() + "/api/trpc",
|
||||
headers() {
|
||||
return {};
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</trpc.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Providers;
|
62
src/components/ui/code-editor.tsx
Normal file
62
src/components/ui/code-editor.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import ReactCodeMirror, { EditorView } from "@uiw/react-codemirror";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { css } from "@codemirror/lang-css";
|
||||
import { html } from "@codemirror/lang-html";
|
||||
import { forwardRef, useEffect, useMemo, useState } from "react";
|
||||
import { tokyoNight } from "@uiw/codemirror-theme-tokyo-night";
|
||||
import { useDebounceCallback } from "usehooks-ts";
|
||||
|
||||
type Props = {
|
||||
lang?: string;
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
};
|
||||
|
||||
const CodeEditor = forwardRef(({ lang, value, onChange }: Props, ref: any) => {
|
||||
const [data, setData] = useState(value);
|
||||
const debounceValue = useDebounceCallback(onChange, 100);
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
const e: any[] = [];
|
||||
|
||||
switch (lang) {
|
||||
case "html":
|
||||
e.push(html({ selfClosingTags: true }));
|
||||
case "css":
|
||||
e.push(css());
|
||||
case "jsx":
|
||||
case "js":
|
||||
case "ts":
|
||||
case "tsx":
|
||||
e.push(
|
||||
javascript({
|
||||
jsx: ["jsx", "tsx"].includes(lang),
|
||||
typescript: ["tsx", "ts"].includes(lang),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return e;
|
||||
}, [lang]);
|
||||
|
||||
useEffect(() => {
|
||||
setData(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<ReactCodeMirror
|
||||
ref={ref}
|
||||
extensions={[EditorView.lineWrapping, ...extensions]}
|
||||
value={data}
|
||||
onChange={(val) => {
|
||||
setData(val);
|
||||
debounceValue(val);
|
||||
}}
|
||||
height="100%"
|
||||
theme={tokyoNight}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default CodeEditor;
|
20
src/components/ui/panel.tsx
Normal file
20
src/components/ui/panel.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const Panel = ({ children }: Props) => {
|
||||
return (
|
||||
<div className="bg-slate-800 rounded-lg pt-9 w-full h-full relative shadow-lg overflow-hidden">
|
||||
<div className="flex gap-2 absolute top-3 left-4">
|
||||
<div className="bg-red-500 rounded-full h-3 w-3" />
|
||||
<div className="bg-yellow-500 rounded-full h-3 w-3" />
|
||||
<div className="bg-green-500 rounded-full h-3 w-3" />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Panel;
|
45
src/components/ui/resizable.tsx
Normal file
45
src/components/ui/resizable.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { GripVertical } from "lucide-react";
|
||||
import * as ResizablePrimitive from "react-resizable-panels";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ResizablePanelGroup = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
|
||||
<ResizablePrimitive.PanelGroup
|
||||
className={cn(
|
||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const ResizablePanel = ResizablePrimitive.Panel;
|
||||
|
||||
const ResizableHandle = ({
|
||||
withHandle,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||
withHandle?: boolean;
|
||||
}) => (
|
||||
<ResizablePrimitive.PanelResizeHandle
|
||||
className={cn(
|
||||
"relative flex w-px items-center justify-center bg-slate-200 after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{withHandle && (
|
||||
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border border-slate-200 bg-slate-200 dark:border-slate-800 dark:bg-slate-800">
|
||||
<GripVertical className="h-2.5 w-2.5" />
|
||||
</div>
|
||||
)}
|
||||
</ResizablePrimitive.PanelResizeHandle>
|
||||
);
|
||||
|
||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
|
95
src/hooks/useMediaQuery.ts
Normal file
95
src/hooks/useMediaQuery.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { useState } from "react";
|
||||
import { useIsomorphicLayoutEffect } from "usehooks-ts";
|
||||
|
||||
type UseMediaQueryOptions = {
|
||||
defaultValue?: boolean;
|
||||
initializeWithValue?: boolean;
|
||||
};
|
||||
|
||||
const IS_SERVER = typeof window === "undefined";
|
||||
|
||||
export function useMediaQuery(
|
||||
query: string,
|
||||
options?: UseMediaQueryOptions
|
||||
): boolean;
|
||||
/**
|
||||
* Custom hook for tracking the state of a media query.
|
||||
* @deprecated - this useMediaQuery's signature is deprecated, it now accepts an query parameter and an options object.
|
||||
* @param {string} query - The media query to track.
|
||||
* @param {?boolean} [defaultValue] - The default value to return if the hook is being run on the server (default is `false`).
|
||||
* @returns {boolean} The current state of the media query (true if the query matches, false otherwise).
|
||||
* @see [Documentation](https://usehooks-ts.com/react-hook/use-media-query)
|
||||
* @see [MDN Match Media](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia)
|
||||
* @example
|
||||
* const isSmallScreen = useMediaQuery('(max-width: 600px)');
|
||||
* // Use `isSmallScreen` to conditionally apply styles or logic based on the screen size.
|
||||
*/
|
||||
export function useMediaQuery(query: string, defaultValue: boolean): boolean; // defaultValue should be false by default
|
||||
/**
|
||||
* Custom hook for tracking the state of a media query.
|
||||
* @param {string} query - The media query to track.
|
||||
* @param {boolean | ?UseMediaQueryOptions} [options] - The default value to return if the hook is being run on the server (default is `false`).
|
||||
* @param {?boolean} [options.defaultValue] - The default value to return if the hook is being run on the server (default is `false`).
|
||||
* @param {?boolean} [options.initializeWithValue] - If `true` (default), the hook will initialize reading the media query. In SSR, you should set it to `false`, returning `options.defaultValue` or `false` initially.
|
||||
* @returns {boolean} The current state of the media query (true if the query matches, false otherwise).
|
||||
* @see [Documentation](https://usehooks-ts.com/react-hook/use-media-query)
|
||||
* @see [MDN Match Media](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia)
|
||||
* @example
|
||||
* const isSmallScreen = useMediaQuery('(max-width: 600px)');
|
||||
* // Use `isSmallScreen` to conditionally apply styles or logic based on the screen size.
|
||||
*/
|
||||
export function useMediaQuery(
|
||||
query: string,
|
||||
options?: boolean | UseMediaQueryOptions
|
||||
): boolean {
|
||||
// TODO: Refactor this code after the deprecated signature has been removed.
|
||||
const defaultValue =
|
||||
typeof options === "boolean" ? options : options?.defaultValue ?? false;
|
||||
const initializeWithValue =
|
||||
typeof options === "boolean"
|
||||
? undefined
|
||||
: options?.initializeWithValue ?? undefined;
|
||||
|
||||
const [matches, setMatches] = useState<boolean>(() => {
|
||||
if (initializeWithValue) {
|
||||
return getMatches(query);
|
||||
}
|
||||
return defaultValue;
|
||||
});
|
||||
|
||||
const getMatches = (query: string): boolean => {
|
||||
if (IS_SERVER) {
|
||||
return defaultValue;
|
||||
}
|
||||
return window.matchMedia(query).matches;
|
||||
};
|
||||
|
||||
/** Handles the change event of the media query. */
|
||||
function handleChange() {
|
||||
setMatches(getMatches(query));
|
||||
}
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
const matchMedia = window.matchMedia(query);
|
||||
|
||||
// Triggered at the first client-side load and if query changes
|
||||
handleChange();
|
||||
|
||||
// Use deprecated `addListener` and `removeListener` to support Safari < 14 (#135)
|
||||
if (matchMedia.addListener) {
|
||||
matchMedia.addListener(handleChange);
|
||||
} else {
|
||||
matchMedia.addEventListener("change", handleChange);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (matchMedia.removeListener) {
|
||||
matchMedia.removeListener(handleChange);
|
||||
} else {
|
||||
matchMedia.removeEventListener("change", handleChange);
|
||||
}
|
||||
};
|
||||
}, [query]);
|
||||
|
||||
return matches;
|
||||
}
|
11
src/lib/trpc.ts
Normal file
11
src/lib/trpc.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { createTRPCReact } from "@trpc/react-query";
|
||||
import type { AppRouter } from "@/server/routers/_app";
|
||||
|
||||
export const getBaseUrl = () => {
|
||||
if (typeof window !== "undefined") return "";
|
||||
return `http://localhost:${process.env.PORT ?? 3000}`;
|
||||
};
|
||||
|
||||
const trpc = createTRPCReact<AppRouter>({});
|
||||
|
||||
export default trpc;
|
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
10
src/server/context.ts
Normal file
10
src/server/context.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { headers as getHeaders, cookies as getCookies } from "next/headers";
|
||||
|
||||
export const createContext = async () => {
|
||||
// const headers = getHeaders();
|
||||
// const cookies = getCookies();
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export type Context = Awaited<ReturnType<typeof createContext>>;
|
5
src/server/index.ts
Normal file
5
src/server/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { createContext } from "./context";
|
||||
import { appRouter } from "./routers/_app";
|
||||
import { createCallerFactory } from "./trpc";
|
||||
|
||||
export const trpcServer = createCallerFactory(appRouter)(createContext);
|
19
src/server/routers/_app.ts
Normal file
19
src/server/routers/_app.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { z } from "zod";
|
||||
import { procedure, router } from "../trpc";
|
||||
|
||||
export const appRouter = router({
|
||||
hello: procedure
|
||||
.input(
|
||||
z.object({
|
||||
text: z.string(),
|
||||
})
|
||||
)
|
||||
.query((opts) => {
|
||||
return {
|
||||
greeting: `hello ${opts.input.text}`,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
export type AppRouter = typeof appRouter;
|
7
src/server/trpc.ts
Normal file
7
src/server/trpc.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { Context } from "./context";
|
||||
|
||||
const t = initTRPC.context<Context>().create();
|
||||
|
||||
// Base router and procedure helpers
|
||||
export const { router, procedure, createCallerFactory } = t;
|
@ -1,20 +1,40 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
import type { Config } from "tailwindcss"
|
||||
|
||||
const config: Config = {
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
backgroundImage: {
|
||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||
"gradient-conic":
|
||||
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
export default config;
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
} satisfies Config
|
||||
|
||||
export default config
|
Loading…
x
Reference in New Issue
Block a user