diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000000000000000000000000000000000..a07f74ba1ee16ed9c961ad3028fa2c644e58836d --- /dev/null +++ b/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": ["next", "next/core-web-vitals"], + "rules": { + "@next/next/no-img-element": "off" + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..07f221634d9a490a30ce4c8dcf2a08cf9e60da5b --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel + +.idea diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000000000000000000000000000000000..521a9f7c0773588848ad5fa8a074ceca964a6b41 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..8edeb6df9e9a96d4c689857d030119e55e7218a6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,60 @@ +FROM node:18 AS base + +# Install dependencies only when needed +FROM base AS deps + +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +RUN \ + if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm ci; \ + elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \ + else echo "Lockfile not found." && exit 1; \ + fi + +# Uncomment the following lines if you want to use a secret at buildtime, +# for example to access your private npm packages +# RUN --mount=type=secret,id=HF_EXAMPLE_SECRET,mode=0444,required=true \ +# $(cat /run/secrets/HF_EXAMPLE_SECRET) + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Next.js collects completely anonymous telemetry data about general usage. +# Learn more here: https://nextjs.org/telemetry +# Uncomment the following line in case you want to disable telemetry during the build. +# ENV NEXT_TELEMETRY_DISABLED 1 + +# RUN yarn build + +# If you use yarn, comment out this line and use the line above +RUN npm run build + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production +# Uncomment the following line in case you want to disable telemetry during runtime. +# ENV NEXT_TELEMETRY_DISABLED 1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 \ No newline at end of file diff --git a/README.md b/README.md index 98437a41079b1962040d9eb9698d414583f5b50d..4128c29224efb8cd67111bcdb16444daaff27b97 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,26 @@ ---- -title: Open Codetree -emoji: 💻 -colorFrom: pink -colorTo: pink -sdk: docker -pinned: false ---- - -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +

+ Next-gen online editor Playground, built with Next and hosted with Vercel +

+

+ Netlify Status +

+ +![](public/preview-image.png) + +# Codetree : Standalone version + +Codetree is a lightning fast ⚡️⚡️⚡️ code playground with automatic module detection as main feature. Unlike https://codepen.io/, Codetree is built on top of WebAssembly using esbuild, the code is compiled directly in your browser, without any backend and converted into machine language, allowing extremely fast execution and offline-mode. + +## Usage + +No need to install any npm package manually, codetree automatically detects the presence of import/require syntax in your file, downloads and installs npm package for you, for example just type `import React from "react"` for installing React library. + +## Features + +- Instant code compilation and preview (15x faster than codepen/codesanbox). + +- Automatic import of external library. + +- Auto-completion and intelliSense. + +- Offline mode. diff --git a/_types/compilerTypes.ts b/_types/compilerTypes.ts new file mode 100644 index 0000000000000000000000000000000000000000..0435b46b8806952873bd1ef9c9752ca39f891021 --- /dev/null +++ b/_types/compilerTypes.ts @@ -0,0 +1,9 @@ +export interface CompilerStatus { + isReady: boolean; + error: string; +} + +export interface CompilerOutput { + code: string; + error: string; +} diff --git a/_types/editorTypes.ts b/_types/editorTypes.ts new file mode 100644 index 0000000000000000000000000000000000000000..e1b588bf2de47924a2e51a0fd150315114af708b --- /dev/null +++ b/_types/editorTypes.ts @@ -0,0 +1,23 @@ +export interface LanguagePropsInterface { + title: string; + entryPoints: string; + monacoLanguage: string; + data: string; +} + +export interface IObjectKeys { + [key: string]: LanguagePropsInterface; +} + +export interface LanguagesInterface extends IObjectKeys { + javascript: LanguagePropsInterface; + css: LanguagePropsInterface; + html: LanguagePropsInterface; +} + +export interface EditorValueInterface { + name: string; + description: string; + public: boolean; + tabs: LanguagesInterface; +} diff --git a/additional.d.ts b/additional.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..8aaad0456d413f0c7ea25f5d5cf6cca5c6694fc2 --- /dev/null +++ b/additional.d.ts @@ -0,0 +1,21 @@ +import "iron-session"; +import { User } from "./graphql/generated/graphql"; +import { OauthInput, OauthProvider } from "./store/features/authSlice"; + +declare global { + interface Window { + withOauth: (input: OauthInput, provider: OauthProvider) => void; + } +} + +declare module "iron-session" { + interface IronSessionData { + user?: { + message?: string; + token?: string | null; + status: boolean; + data: User; + isLoggedIn?: boolean; + }; + } +} diff --git a/codegen.yml b/codegen.yml new file mode 100644 index 0000000000000000000000000000000000000000..95a4532a1c8de232986b0d16ee5f47302df65829 --- /dev/null +++ b/codegen.yml @@ -0,0 +1,12 @@ +overwrite: true +schema: "http://localhost:4000/graphql" +documents: null +generates: + graphql/generated/graphql.d.ts: + plugins: + - "typescript" + - "typescript-operations" + - "typescript-react-apollo" + ./graphql.schema.json: + plugins: + - "introspection" diff --git a/components/Avatar.tsx b/components/Avatar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d0e72360294547b486c716ce2075f8d7b533b70e --- /dev/null +++ b/components/Avatar.tsx @@ -0,0 +1,57 @@ +import Image from "next/image"; +import React from "react"; + +interface AvatarProps { + image?: string; + username?: string; + size?: number; + gradient?: boolean; + className?: string; + placeholderType?: "blur" | "empty" | undefined; +} + +export const Avatar = ({ + image, + username, + size = 45, + gradient, + className, + placeholderType = "blur", +}: AvatarProps) => { + if (image) + return ( +
+
+ avatar +
+
+ ); + + return ( +
+
+ {username?.charAt(0).toUpperCase()} +
+
+ ); +}; diff --git a/components/Dropdown.tsx b/components/Dropdown.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7def323af9461489fadaf43aad28ea8cb2bf1397 --- /dev/null +++ b/components/Dropdown.tsx @@ -0,0 +1,77 @@ +import React, { ReactNode, useEffect, useRef, useState } from "react"; +import { motion, Variants } from "framer-motion"; +import useOutsideRef from "../hooks/useOutsideRef"; +import Router from "next/router"; +import { useAppSelector } from "../store/hook"; +import { theme_state } from "../store/features/themeSlice"; + +interface DropdownProps { + trigger: ReactNode; + children: ReactNode; + classname: string; +} + +export const Dropdown = ({ trigger, children, classname }: DropdownProps) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + const { theme } = useAppSelector(theme_state); + + const { isOutsideRef } = useOutsideRef(dropdownRef); + + useEffect(() => { + if (isOutsideRef) { + setIsOpen(false); + } + }, [isOutsideRef]); + + const toggleMenu = () => { + setIsOpen(!isOpen); + }; + + Router.events.on("routeChangeStart", () => { + setIsOpen(false); + }); + + const animation: Variants = { + enter: { + opacity: 1, + scale: 1, + transformOrigin: "top right", + transition: { + duration: 0.25, + }, + display: "block", + }, + exit: { + opacity: 0, + scale: 0.7, + transformOrigin: "top right", + transition: { + duration: 0.2, + delay: 0.1, + }, + transitionEnd: { + display: "none", + }, + }, + }; + + return ( +
+ +
+ {trigger} +
+ + +
{children}
+
+
+
+ ); +}; diff --git a/components/Header.tsx b/components/Header.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9d2d13296333a74b3e9ca29c53d6e65298dac6fe --- /dev/null +++ b/components/Header.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { useAppDispatch, useAppSelector } from "../store/hook"; +import { theme_state } from "../store/features/themeSlice"; + +export const Header = () => { + const dispatch = useAppDispatch(); + const { theme } = useAppSelector(theme_state); + return ( +
+
+
+ Codetree AI +
+
+
+ ); +}; diff --git a/components/Loader.tsx b/components/Loader.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4be0f62969723895e409c78b9b143380418bd20f --- /dev/null +++ b/components/Loader.tsx @@ -0,0 +1,64 @@ +import React from "react"; + +interface LoaderProps { + size?: number; + color?: string; +} + +const Loader = ({ size = 50, color = "#FFFFFF" }: LoaderProps) => { + return ( +
+ + + + + + + + + + + + + + + +
+ ); +}; + +export default Loader; diff --git a/components/Modals/AuthModal.tsx b/components/Modals/AuthModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9141f9641ce07e88f4dea6e91bcf9c5e068fd035 --- /dev/null +++ b/components/Modals/AuthModal.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { useAppDispatch } from "../../store/hook"; +import { getGithubOAuthURL, getGoogleOAuthURL } from "../../utils/getOAuthUrl"; +import { nativePopup } from "../../utils/nativePopup"; +import { close_modal } from "../../store/features/modalSlice"; +import { modalVariant } from "./config"; +import { RiGithubFill, RiGoogleFill } from "react-icons/ri"; + +const AuthModal = () => { + const dispatch = useAppDispatch(); + + return ( + e.stopPropagation()} + > +
+
+ +
+ + + +
+
+ + ); +}; + +export default AuthModal; diff --git a/components/Modals/RootModal.tsx b/components/Modals/RootModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4010e1c7db4ecffa3fc0916672dde3e9b7963867 --- /dev/null +++ b/components/Modals/RootModal.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { motion, AnimatePresence } from "framer-motion"; + +import { useAppDispatch, useAppSelector } from "../../store/hook"; +import { + modal_state, + close_modal, + ModalEnum, +} from "../../store/features/modalSlice"; + +import AuthModal from "./AuthModal"; +import TemplateModal from "./TemplateModal"; +import SettingsModal from "./SettingsModal"; + +export const RootModal = () => { + const { type, visible } = useAppSelector(modal_state); + const dispatch = useAppDispatch(); + + const renderModal = (type: ModalEnum) => { + switch (type) { + case ModalEnum.AUTH: + return ; + + case ModalEnum.TEMPLATE: + return ; + + case ModalEnum.SETTINGS: + return ; + + case ModalEnum.IDLE: + return
; + } + }; + + return ( + null}> + {visible && ( + dispatch(close_modal())} + > + {renderModal(type)} + + )} + + ); +}; diff --git a/components/Modals/SettingsModal.tsx b/components/Modals/SettingsModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..91a8878e49b41ad695a1ecf704df407adfce08b9 --- /dev/null +++ b/components/Modals/SettingsModal.tsx @@ -0,0 +1,196 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { useForm } from "react-hook-form"; +import { useAppDispatch, useAppSelector } from "../../store/hook"; +import { editor_state, set_options } from "../../store/features/editorSlice"; +import { modalVariant } from "./config"; +import { theme_state } from "../../store/features/themeSlice"; + +const SettingsModal = () => { + const dispatch = useAppDispatch(); + const { options } = useAppSelector(editor_state); + const { theme } = useAppSelector(theme_state); + + const { register, handleSubmit } = useForm(); + + const onChangeOptions = ({ + fontSize, + fontWeight, + minimapEnabled, + minimapScale, + wordWrap, + autoClosingBrackets, + }: any) => { + const custom = { + fontSize: parseInt(fontSize), + fontWeight: fontWeight, + minimap: { + enabled: minimapEnabled, + scale: parseInt(minimapScale), + }, + wordWrap: wordWrap, + autoClosingBrackets: { autoClosingBrackets }, + showUnused: true, + automaticLayout: true, + tabSize: 2, + renderLineHighlight: "none", + scrollbar: { verticalScrollbarSize: 10, verticalSliderSize: 10 }, + }; + + dispatch(set_options(custom)); + }; + + return ( + e.stopPropagation()} + > +
+

Settings

+
+ +
+
+
+ {/* Fonts */} +
+
Font size :
+
Set the font size in pixels.
+ +
+ +
+
Font weight :
+
Defines how bold you text are.
+ +
+ + {/* Minimap */} +
+
Enabled minimap :
+
+ Control if the minimap should be shown. +
+
+ + +
+
+ +
+
Scale :
+
Set the size of the minimap.
+ +
+ + {/* Others */} +
+
Word wrap :
+
Control if lines should wrap.
+ +
+ +
+
Auto Closing Brackets :
+
+ Controls if brackets should close automatically +
+ +
+ + +
+
+
+
+ ); +}; + +export default SettingsModal; diff --git a/components/Modals/TemplateModal.tsx b/components/Modals/TemplateModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ba3248be01c4384ffdb22e1e9eebc9009bc5da65 --- /dev/null +++ b/components/Modals/TemplateModal.tsx @@ -0,0 +1,65 @@ +import { motion } from "framer-motion"; +import { useTree } from "../../hooks"; +import { treeTemplates } from "../../constants"; +import { useAppSelector } from "../../store/hook"; +import { theme_state } from "../../store/features/themeSlice"; +import { compiler_state } from "../../store/features/compilerSlice"; +import { TemplateSelectionSkeleton } from "../Skeleton/TemplateSelectionSkeleton"; +import { modalVariant } from "./config"; + +const TemplateModal = () => { + const { theme } = useAppSelector(theme_state); + const { esbuildStatus } = useAppSelector(compiler_state); + const { setTree } = useTree(); + + let arr = []; + + for (const item of Object.entries(treeTemplates)) { + arr.push(item); + } + + const templates = arr.map((template, key) => ( + + )); + + return ( + e.stopPropagation()} + > +
+

Templates

+
+
+ {esbuildStatus.isReady ? templates : } +
+
+ ); +}; + +export default TemplateModal; diff --git a/components/Modals/config.ts b/components/Modals/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..18391c22ae517ea11993c64a56f4fa15bbe6d0d8 --- /dev/null +++ b/components/Modals/config.ts @@ -0,0 +1,7 @@ +import { Variants } from "framer-motion"; + +export const modalVariant: Variants = { + initial: { scale: 0.95, opacity: 0 }, + animate: { scale: 1, opacity: 1 }, + exit: { scale: 0.98, opacity: 0 }, +}; diff --git a/components/Playground/ConsoleLog.tsx b/components/Playground/ConsoleLog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7591d843c3643fa0ac98c079bef779adfd0f6b68 --- /dev/null +++ b/components/Playground/ConsoleLog.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import dynamic from "next/dynamic"; +import { useAppDispatch, useAppSelector } from "../../store/hook"; +import { clear_logs } from "../../store/features/editorSlice"; + +import { theme_state } from "../../store/features/themeSlice"; + +const Console = dynamic(import("console-feed/lib/Component"), { ssr: false }); + +interface LogsProps { + logs: any; +} + +const Logs = ({ logs }: LogsProps) => { + const { theme } = useAppSelector(theme_state); + const dispatch = useAppDispatch(); + + return ( +
+
+ +
+ +
+ +
+
+ ); +}; + +const ConsoleLog = React.memo(Logs); + +export default ConsoleLog; diff --git a/components/Playground/Footer.tsx b/components/Playground/Footer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..263e32dec1c151c476b4cd3850e67592c9aeda4d --- /dev/null +++ b/components/Playground/Footer.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { useAppDispatch, useAppSelector } from "../../store/hook"; +import { + editor_state, + toggle_logs_tab, +} from "../../store/features/editorSlice"; +import { compiler_state } from "../../store/features/compilerSlice"; +import { RiTerminalLine } from "react-icons/ri"; + +const Footer = () => { + const { logs } = useAppSelector(editor_state); + const { isCompiling } = useAppSelector(compiler_state); + const dispatch = useAppDispatch(); + + return ( +
+
+ {isCompiling && ( +
+ +
+ )} +
+ +
+
dispatch(toggle_logs_tab())} + className="flex items-center cursor-pointer text-gray-400" + > + +
+ +
+ {logs.length > 0 && ( +
+
+
+
+ )} +
+
+
+ ); +}; + +export default Footer; diff --git a/components/Playground/Header.tsx b/components/Playground/Header.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ef61d00ef468784ef24a18817fddd3d45acf4419 --- /dev/null +++ b/components/Playground/Header.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { RiAddFill, RiSettings3Fill } from "react-icons/ri"; +import { useAppDispatch } from "../../store/hook"; +import { ModalEnum, open_modal } from "../../store/features/modalSlice"; + +const Header = () => { + const dispatch = useAppDispatch(); + + return ( +
+
+ dispatch(open_modal(ModalEnum.TEMPLATE))} + /> + dispatch(open_modal(ModalEnum.SETTINGS))} + /> +
+
+ ); +}; + +export default Header; diff --git a/components/Playground/Iframe.tsx b/components/Playground/Iframe.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c173f8fe06d0d303460f9ce436c8ce2f13e87293 --- /dev/null +++ b/components/Playground/Iframe.tsx @@ -0,0 +1,101 @@ +import React, { useRef, useEffect } from "react"; + +import { useAppDispatch } from "../../store/hook"; +import { update_logs } from "../../store/features/editorSlice"; +import { getCompileCode } from "../../store/features/compilerSlice"; +import { createIframeContent } from "../../utils/createIframeContent"; +import { IframeLoaderScreen } from "./IframeLoaderScreen"; +import { IframeErrorScreen } from "./IframeErrorScreen"; +import { LanguagesInterface } from "../../_types/editorTypes"; +import { CompilerOutput, CompilerStatus } from "../../_types/compilerTypes"; + +interface IframeProps { + tabs: LanguagesInterface; + output: CompilerOutput; + isCompiling: boolean; + esbuildStatus: CompilerStatus; +} + +const IframePanel = ({ + tabs, + output, + isCompiling, + esbuildStatus, +}: IframeProps) => { + const iframe = useRef(); + const dispatch = useAppDispatch(); + + const htmlFrameContent = createIframeContent(tabs.css?.data, tabs.html?.data); + + //=== incoming message + useEffect(() => { + window.onmessage = function (response: MessageEvent) { + if (response.data && response.data.source === "iframe") { + let errorObject = { + method: "error", + id: Date.now(), + data: [`${response.data.message}`], + }; + dispatch(update_logs(errorObject)); + } + }; + + if (tabs.javascript && esbuildStatus.isReady) { + setTimeout(async () => { + dispatch( + getCompileCode(tabs.javascript.data, tabs.javascript.entryPoints) + ); + }, 50); + } + }, [dispatch, tabs, esbuildStatus.isReady]); + + //=== outgoing massage + useEffect(() => { + iframe.current.srcdoc = htmlFrameContent; + + setTimeout(async () => { + iframe?.current?.contentWindow?.postMessage(output.code, "*"); + }, 40); + }, [htmlFrameContent, output]); + + return ( +
+ {/* build error */} + {output.error ? : ""} + + {/* Loading screen */} + {isCompiling ? ( +
+ +
+ ) : ( + "" + )} + +