matt HOFFNER commited on
Commit
3c3f089
1 Parent(s): 65ba288
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .eslintrc +6 -0
  2. .gitignore +37 -0
  3. .npmrc +1 -0
  4. Dockerfile +60 -0
  5. README.md +26 -10
  6. _types/compilerTypes.ts +9 -0
  7. _types/editorTypes.ts +23 -0
  8. additional.d.ts +21 -0
  9. codegen.yml +12 -0
  10. components/Avatar.tsx +57 -0
  11. components/Dropdown.tsx +77 -0
  12. components/Header.tsx +20 -0
  13. components/Loader.tsx +64 -0
  14. components/Modals/AuthModal.tsx +73 -0
  15. components/Modals/RootModal.tsx +47 -0
  16. components/Modals/SettingsModal.tsx +196 -0
  17. components/Modals/TemplateModal.tsx +65 -0
  18. components/Modals/config.ts +7 -0
  19. components/Playground/ConsoleLog.tsx +47 -0
  20. components/Playground/Footer.tsx +49 -0
  21. components/Playground/Header.tsx +27 -0
  22. components/Playground/Iframe.tsx +101 -0
  23. components/Playground/IframeErrorScreen.tsx +9 -0
  24. components/Playground/IframeLoaderScreen.tsx +32 -0
  25. components/Playground/InputCodeTab.tsx +36 -0
  26. components/Playground/Monaco.tsx +34 -0
  27. components/Playground/Pane.tsx +41 -0
  28. components/Playground/index.tsx +170 -0
  29. components/Skeleton/TemplateSelectionSkeleton.tsx +19 -0
  30. constants/icon.tsx +18 -0
  31. constants/index.ts +2 -0
  32. constants/monacoOptions.ts +16 -0
  33. constants/templates.ts +271 -0
  34. editor.d.ts +2 -0
  35. esbuild/plugins/index.ts +2 -0
  36. esbuild/plugins/unpkg-fecth-plugin.ts +85 -0
  37. esbuild/plugins/unpkg-path-plugin.ts +31 -0
  38. graphql.schema.json +0 -0
  39. graphql/definitions/auth.definition.ts +152 -0
  40. graphql/definitions/comment.definition.ts +97 -0
  41. graphql/definitions/notification.definition.ts +31 -0
  42. graphql/definitions/pageView.definition.ts +14 -0
  43. graphql/definitions/project.definition.ts +148 -0
  44. graphql/definitions/tag.definition.ts +12 -0
  45. graphql/definitions/upload.definition.ts +10 -0
  46. graphql/definitions/user.definitions.ts +54 -0
  47. graphql/generated/graphql.d.ts +563 -0
  48. hooks/index.ts +2 -0
  49. hooks/useMonaco.ts +117 -0
  50. hooks/useOutsideRef.ts +26 -0
.eslintrc ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "extends": ["next", "next/core-web-vitals"],
3
+ "rules": {
4
+ "@next/next/no-img-element": "off"
5
+ }
6
+ }
.gitignore ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+
8
+ # testing
9
+ /coverage
10
+
11
+ # next.js
12
+ /.next/
13
+ /out/
14
+
15
+ # production
16
+ /build
17
+
18
+ # misc
19
+ .DS_Store
20
+ *.pem
21
+
22
+ # debug
23
+ npm-debug.log*
24
+ yarn-debug.log*
25
+ yarn-error.log*
26
+
27
+ # local env files
28
+ .env
29
+ .env.local
30
+ .env.development.local
31
+ .env.test.local
32
+ .env.production.local
33
+
34
+ # vercel
35
+ .vercel
36
+
37
+ .idea
.npmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ legacy-peer-deps=true
Dockerfile ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18 AS base
2
+
3
+ # Install dependencies only when needed
4
+ FROM base AS deps
5
+
6
+ WORKDIR /app
7
+
8
+ # Install dependencies based on the preferred package manager
9
+ COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
10
+ RUN \
11
+ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
12
+ elif [ -f package-lock.json ]; then npm ci; \
13
+ elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
14
+ else echo "Lockfile not found." && exit 1; \
15
+ fi
16
+
17
+ # Uncomment the following lines if you want to use a secret at buildtime,
18
+ # for example to access your private npm packages
19
+ # RUN --mount=type=secret,id=HF_EXAMPLE_SECRET,mode=0444,required=true \
20
+ # $(cat /run/secrets/HF_EXAMPLE_SECRET)
21
+
22
+ # Rebuild the source code only when needed
23
+ FROM base AS builder
24
+ WORKDIR /app
25
+ COPY --from=deps /app/node_modules ./node_modules
26
+ COPY . .
27
+
28
+ # Next.js collects completely anonymous telemetry data about general usage.
29
+ # Learn more here: https://nextjs.org/telemetry
30
+ # Uncomment the following line in case you want to disable telemetry during the build.
31
+ # ENV NEXT_TELEMETRY_DISABLED 1
32
+
33
+ # RUN yarn build
34
+
35
+ # If you use yarn, comment out this line and use the line above
36
+ RUN npm run build
37
+
38
+ # Production image, copy all the files and run next
39
+ FROM base AS runner
40
+ WORKDIR /app
41
+
42
+ ENV NODE_ENV production
43
+ # Uncomment the following line in case you want to disable telemetry during runtime.
44
+ # ENV NEXT_TELEMETRY_DISABLED 1
45
+
46
+ RUN addgroup --system --gid 1001 nodejs
47
+ RUN adduser --system --uid 1001 nextjs
48
+
49
+ COPY --from=builder /app/public ./public
50
+
51
+ # Automatically leverage output traces to reduce image size
52
+ # https://nextjs.org/docs/advanced-features/output-file-tracing
53
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
54
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
55
+
56
+ USER nextjs
57
+
58
+ EXPOSE 3000
59
+
60
+ ENV PORT 3000
README.md CHANGED
@@ -1,10 +1,26 @@
1
- ---
2
- title: Open Codetree
3
- emoji: 💻
4
- colorFrom: pink
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <p align="center">
2
+ Next-gen online editor Playground, built with <a href="https://nextjs.org/" target="_blank">Next</a> and hosted with <a href="https://vercel.com/" target="_blank">Vercel</a>
3
+ </p>
4
+ <p align="center">
5
+ <img src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat" alt="Netlify Status" />
6
+ </p>
7
+
8
+ ![](public/preview-image.png)
9
+
10
+ # Codetree : Standalone version
11
+
12
+ 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.
13
+
14
+ ## Usage
15
+
16
+ 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.
17
+
18
+ ## Features
19
+
20
+ - Instant code compilation and preview (15x faster than codepen/codesanbox).
21
+
22
+ - Automatic import of external library.
23
+
24
+ - Auto-completion and intelliSense.
25
+
26
+ - Offline mode.
_types/compilerTypes.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ export interface CompilerStatus {
2
+ isReady: boolean;
3
+ error: string;
4
+ }
5
+
6
+ export interface CompilerOutput {
7
+ code: string;
8
+ error: string;
9
+ }
_types/editorTypes.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface LanguagePropsInterface {
2
+ title: string;
3
+ entryPoints: string;
4
+ monacoLanguage: string;
5
+ data: string;
6
+ }
7
+
8
+ export interface IObjectKeys {
9
+ [key: string]: LanguagePropsInterface;
10
+ }
11
+
12
+ export interface LanguagesInterface extends IObjectKeys {
13
+ javascript: LanguagePropsInterface;
14
+ css: LanguagePropsInterface;
15
+ html: LanguagePropsInterface;
16
+ }
17
+
18
+ export interface EditorValueInterface {
19
+ name: string;
20
+ description: string;
21
+ public: boolean;
22
+ tabs: LanguagesInterface;
23
+ }
additional.d.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import "iron-session";
2
+ import { User } from "./graphql/generated/graphql";
3
+ import { OauthInput, OauthProvider } from "./store/features/authSlice";
4
+
5
+ declare global {
6
+ interface Window {
7
+ withOauth: (input: OauthInput, provider: OauthProvider) => void;
8
+ }
9
+ }
10
+
11
+ declare module "iron-session" {
12
+ interface IronSessionData {
13
+ user?: {
14
+ message?: string;
15
+ token?: string | null;
16
+ status: boolean;
17
+ data: User;
18
+ isLoggedIn?: boolean;
19
+ };
20
+ }
21
+ }
codegen.yml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ overwrite: true
2
+ schema: "http://localhost:4000/graphql"
3
+ documents: null
4
+ generates:
5
+ graphql/generated/graphql.d.ts:
6
+ plugins:
7
+ - "typescript"
8
+ - "typescript-operations"
9
+ - "typescript-react-apollo"
10
+ ./graphql.schema.json:
11
+ plugins:
12
+ - "introspection"
components/Avatar.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Image from "next/image";
2
+ import React from "react";
3
+
4
+ interface AvatarProps {
5
+ image?: string;
6
+ username?: string;
7
+ size?: number;
8
+ gradient?: boolean;
9
+ className?: string;
10
+ placeholderType?: "blur" | "empty" | undefined;
11
+ }
12
+
13
+ export const Avatar = ({
14
+ image,
15
+ username,
16
+ size = 45,
17
+ gradient,
18
+ className,
19
+ placeholderType = "blur",
20
+ }: AvatarProps) => {
21
+ if (image)
22
+ return (
23
+ <div
24
+ className={` ${
25
+ gradient && `bg-gradient-to-t from-violet-400 to-violet-100 p-0.5`
26
+ } relative flex justify-center items-center rounded-full shadow-xl overflow-hidden ${className}`}
27
+ >
28
+ <div className="flex">
29
+ <Image
30
+ src={image}
31
+ alt="avatar"
32
+ className="cursor-pointer rounded-full"
33
+ objectFit="cover"
34
+ width={size}
35
+ height={size}
36
+ placeholder={placeholderType}
37
+ quality={100}
38
+ blurDataURL={placeholderType === "blur" ? image : undefined}
39
+ />
40
+ </div>
41
+ </div>
42
+ );
43
+
44
+ return (
45
+ <div className="flex justify-center items-center relative">
46
+ <div
47
+ style={{
48
+ width: size,
49
+ height: size,
50
+ }}
51
+ className={`flex items-center justify-center bg-zinc-50 max-w-xs rounded-full mb-1 cursor-pointer border ${className}`}
52
+ >
53
+ {username?.charAt(0).toUpperCase()}
54
+ </div>
55
+ </div>
56
+ );
57
+ };
components/Dropdown.tsx ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { ReactNode, useEffect, useRef, useState } from "react";
2
+ import { motion, Variants } from "framer-motion";
3
+ import useOutsideRef from "../hooks/useOutsideRef";
4
+ import Router from "next/router";
5
+ import { useAppSelector } from "../store/hook";
6
+ import { theme_state } from "../store/features/themeSlice";
7
+
8
+ interface DropdownProps {
9
+ trigger: ReactNode;
10
+ children: ReactNode;
11
+ classname: string;
12
+ }
13
+
14
+ export const Dropdown = ({ trigger, children, classname }: DropdownProps) => {
15
+ const [isOpen, setIsOpen] = useState(false);
16
+ const dropdownRef = useRef(null);
17
+ const { theme } = useAppSelector(theme_state);
18
+
19
+ const { isOutsideRef } = useOutsideRef(dropdownRef);
20
+
21
+ useEffect(() => {
22
+ if (isOutsideRef) {
23
+ setIsOpen(false);
24
+ }
25
+ }, [isOutsideRef]);
26
+
27
+ const toggleMenu = () => {
28
+ setIsOpen(!isOpen);
29
+ };
30
+
31
+ Router.events.on("routeChangeStart", () => {
32
+ setIsOpen(false);
33
+ });
34
+
35
+ const animation: Variants = {
36
+ enter: {
37
+ opacity: 1,
38
+ scale: 1,
39
+ transformOrigin: "top right",
40
+ transition: {
41
+ duration: 0.25,
42
+ },
43
+ display: "block",
44
+ },
45
+ exit: {
46
+ opacity: 0,
47
+ scale: 0.7,
48
+ transformOrigin: "top right",
49
+ transition: {
50
+ duration: 0.2,
51
+ delay: 0.1,
52
+ },
53
+ transitionEnd: {
54
+ display: "none",
55
+ },
56
+ },
57
+ };
58
+
59
+ return (
60
+ <div ref={dropdownRef}>
61
+ <motion.div className="menu-item relative">
62
+ <div className="cursor-pointer" onClick={toggleMenu}>
63
+ {trigger}
64
+ </div>
65
+
66
+ <motion.div
67
+ className={`${classname} absolute shadow rounded-lg z-50 mt-3 glassmorphism`}
68
+ initial="exit"
69
+ animate={isOpen ? "enter" : "exit"}
70
+ variants={animation}
71
+ >
72
+ <div>{children}</div>
73
+ </motion.div>
74
+ </motion.div>
75
+ </div>
76
+ );
77
+ };
components/Header.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { useAppDispatch, useAppSelector } from "../store/hook";
3
+ import { theme_state } from "../store/features/themeSlice";
4
+
5
+ export const Header = () => {
6
+ const dispatch = useAppDispatch();
7
+ const { theme } = useAppSelector(theme_state);
8
+ return (
9
+ <div
10
+ style={{ height: "3rem", background: theme.background }}
11
+ className="flex items-center pl-5 pr-12 justify-between"
12
+ >
13
+ <div>
14
+ <div className="text-2xl font-medium text-gray-200">
15
+ Codetree AI
16
+ </div>
17
+ </div>
18
+ </div>
19
+ );
20
+ };
components/Loader.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ interface LoaderProps {
4
+ size?: number;
5
+ color?: string;
6
+ }
7
+
8
+ const Loader = ({ size = 50, color = "#FFFFFF" }: LoaderProps) => {
9
+ return (
10
+ <div>
11
+ <svg
12
+ width={size}
13
+ height={size}
14
+ viewBox="0 0 16 16"
15
+ color={color}
16
+ fill="none"
17
+ xmlns="http://www.w3.org/2000/svg"
18
+ >
19
+ <path
20
+ d="M3.05022 3.05026 C 2.65969 3.44078 2.65969 4.07394 3.05022 4.46447 C 3.44074 4.85499 4.07391 4.85499 4.46443 4.46447 L 3.05022 3.05026Z M 4.46443 4.46447 C 5.16369 3.76521 6.05461 3.289 7.02451 3.09608 L 6.63433 1.13451 C 5.27647 1.4046 4.02918 2.07129 3.05022 3.05026 L 4.46443 4.46447Z M 7.02451 3.09608 C 7.99442 2.90315 8.99975 3.00217 9.91338 3.3806 L 10.6787 1.53285C9.39967 1.00303 7.9922 0.864409 6.63433 1.13451 L 7.02451 3.09608ZM9.91338 3.3806C10.827 3.75904 11.6079 4.39991 12.1573 5.22215L13.8203 4.11101C13.0511 2.95987 11.9578 2.06266 10.6787 1.53285L9.91338 3.3806ZM12.1573 5.22215C12.7067 6.0444 13 7.01109 13 8L15 8C15 6.61553 14.5894 5.26215 13.8203 4.11101L12.1573 5.22215Z"
21
+ fill="url(#paint0_linear_11825_47664)"
22
+ />
23
+ <path
24
+ d="M3 8C3 7.44772 2.55228 7 2 7C1.44772 7 1 7.44772 1 8L3 8ZM1 8C1 8.91925 1.18106 9.82951 1.53284 10.6788L3.3806 9.91342C3.12933 9.30679 3 8.65661 3 8L1 8ZM1.53284 10.6788C1.88463 11.5281 2.40024 12.2997 3.05025 12.9497L4.46447 11.5355C4.00017 11.0712 3.63188 10.52 3.3806 9.91342L1.53284 10.6788ZM3.05025 12.9497C3.70026 13.5998 4.47194 14.1154 5.32122 14.4672L6.08658 12.6194C5.47996 12.3681 4.92876 11.9998 4.46447 11.5355L3.05025 12.9497ZM5.32122 14.4672C6.1705 14.8189 7.08075 15 8 15L8 13C7.34339 13 6.69321 12.8707 6.08658 12.6194L5.32122 14.4672ZM8 15C8.91925 15 9.82951 14.8189 10.6788 14.4672L9.91342 12.6194C9.30679 12.8707 8.65661 13 8 13L8 15ZM10.6788 14.4672C11.5281 14.1154 12.2997 13.5998 12.9497 12.9497L11.5355 11.5355C11.0712 11.9998 10.52 12.3681 9.91342 12.6194L10.6788 14.4672ZM12.9497 12.9497C13.5998 12.2997 14.1154 11.5281 14.4672 10.6788L12.6194 9.91342C12.3681 10.52 11.9998 11.0712 11.5355 11.5355L12.9497 12.9497ZM14.4672 10.6788C14.8189 9.8295 15 8.91925 15 8L13 8C13 8.65661 12.8707 9.30679 12.6194 9.91342L14.4672 10.6788Z"
25
+ fill="url(#paint1_linear_11825_47664)"
26
+ />
27
+ <defs>
28
+ <linearGradient
29
+ id="paint0_linear_11825_47664"
30
+ x1="14"
31
+ y1="8"
32
+ x2="2"
33
+ y2="8"
34
+ gradientUnits="userSpaceOnUse"
35
+ >
36
+ <stop stopColor="currentColor" stopOpacity="0.5" />
37
+ <stop offset="1" stopColor="currentColor" stopOpacity="0" />
38
+ </linearGradient>
39
+ <linearGradient
40
+ id="paint1_linear_11825_47664"
41
+ x1="2"
42
+ y1="8"
43
+ x2="14"
44
+ y2="8"
45
+ gradientUnits="userSpaceOnUse"
46
+ >
47
+ <stop stopColor="currentColor" />
48
+ <stop offset="1" stopColor="currentColor" stopOpacity="0.5" />
49
+ </linearGradient>
50
+ </defs>
51
+ <animateTransform
52
+ from="0 0 0"
53
+ to="360 0 0"
54
+ attributeName="transform"
55
+ type="rotate"
56
+ repeatCount="indefinite"
57
+ dur="1300ms"
58
+ />
59
+ </svg>
60
+ </div>
61
+ );
62
+ };
63
+
64
+ export default Loader;
components/Modals/AuthModal.tsx ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { motion } from "framer-motion";
3
+ import { useAppDispatch } from "../../store/hook";
4
+ import { getGithubOAuthURL, getGoogleOAuthURL } from "../../utils/getOAuthUrl";
5
+ import { nativePopup } from "../../utils/nativePopup";
6
+ import { close_modal } from "../../store/features/modalSlice";
7
+ import { modalVariant } from "./config";
8
+ import { RiGithubFill, RiGoogleFill } from "react-icons/ri";
9
+
10
+ const AuthModal = () => {
11
+ const dispatch = useAppDispatch();
12
+
13
+ return (
14
+ <motion.div
15
+ className="sm:mt-10 mx-auto h-full sm:h-auto sm:w-8/12 lg:w-6/12 xl:w-4/12"
16
+ variants={modalVariant}
17
+ initial="initial"
18
+ animate="animate"
19
+ exit="exit"
20
+ transition={{ ease: "easeOut", duration: 0.3 }}
21
+ onClick={(e) => e.stopPropagation()}
22
+ >
23
+ <div className="h-full flex flex-col shadow-xl sm:rounded-xl overflow-hidden">
24
+ <div
25
+ className="h-64 w-full"
26
+ style={{
27
+ backgroundImage: `url(https://cdn.dribbble.com/userupload/3396431/file/original-d865684d8f3afbc99fcab7ab38f9ee9e.png?compress=1&resize=1504x1128)`,
28
+ backgroundPosition: "center",
29
+ backgroundSize: "cover",
30
+ }}
31
+ />
32
+
33
+ <div className="flex h-52 flex-col justify-center items-center gap-7 glassmorphism">
34
+ <button
35
+ onClick={() => {
36
+ nativePopup({
37
+ pageURL: getGoogleOAuthURL(),
38
+ pageTitle: "Codetree authentication",
39
+ popupWinWidth: 490,
40
+ popupWinHeight: 600,
41
+ });
42
+
43
+ dispatch(close_modal());
44
+ }}
45
+ className="px-5 border border-gray-400 flex gap-x-5 items-center justify-center w-80 h-12 rounded-xl text-gray-300"
46
+ >
47
+ <RiGoogleFill size={24} />
48
+ <div>Sign in with google</div>
49
+ </button>
50
+
51
+ <button
52
+ onClick={() => {
53
+ nativePopup({
54
+ pageURL: getGithubOAuthURL(),
55
+ pageTitle: "Codetree authentication",
56
+ popupWinWidth: 490,
57
+ popupWinHeight: 600,
58
+ });
59
+
60
+ dispatch(close_modal());
61
+ }}
62
+ className="px-5 border border-gray-400 flex gap-x-5 items-center justify-center w-80 h-12 rounded-xl text-gray-300"
63
+ >
64
+ <RiGithubFill size={24} />
65
+ <div>Sign in with github</div>
66
+ </button>
67
+ </div>
68
+ </div>
69
+ </motion.div>
70
+ );
71
+ };
72
+
73
+ export default AuthModal;
components/Modals/RootModal.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+
4
+ import { useAppDispatch, useAppSelector } from "../../store/hook";
5
+ import {
6
+ modal_state,
7
+ close_modal,
8
+ ModalEnum,
9
+ } from "../../store/features/modalSlice";
10
+
11
+ import AuthModal from "./AuthModal";
12
+ import TemplateModal from "./TemplateModal";
13
+ import SettingsModal from "./SettingsModal";
14
+
15
+ export const RootModal = () => {
16
+ const { type, visible } = useAppSelector(modal_state);
17
+ const dispatch = useAppDispatch();
18
+
19
+ const renderModal = (type: ModalEnum) => {
20
+ switch (type) {
21
+ case ModalEnum.AUTH:
22
+ return <AuthModal />;
23
+
24
+ case ModalEnum.TEMPLATE:
25
+ return <TemplateModal />;
26
+
27
+ case ModalEnum.SETTINGS:
28
+ return <SettingsModal />;
29
+
30
+ case ModalEnum.IDLE:
31
+ return <div />;
32
+ }
33
+ };
34
+
35
+ return (
36
+ <AnimatePresence exitBeforeEnter onExitComplete={() => null}>
37
+ {visible && (
38
+ <motion.div
39
+ className="backdrop"
40
+ onClick={() => dispatch(close_modal())}
41
+ >
42
+ {renderModal(type)}
43
+ </motion.div>
44
+ )}
45
+ </AnimatePresence>
46
+ );
47
+ };
components/Modals/SettingsModal.tsx ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { motion } from "framer-motion";
3
+ import { useForm } from "react-hook-form";
4
+ import { useAppDispatch, useAppSelector } from "../../store/hook";
5
+ import { editor_state, set_options } from "../../store/features/editorSlice";
6
+ import { modalVariant } from "./config";
7
+ import { theme_state } from "../../store/features/themeSlice";
8
+
9
+ const SettingsModal = () => {
10
+ const dispatch = useAppDispatch();
11
+ const { options } = useAppSelector(editor_state);
12
+ const { theme } = useAppSelector(theme_state);
13
+
14
+ const { register, handleSubmit } = useForm();
15
+
16
+ const onChangeOptions = ({
17
+ fontSize,
18
+ fontWeight,
19
+ minimapEnabled,
20
+ minimapScale,
21
+ wordWrap,
22
+ autoClosingBrackets,
23
+ }: any) => {
24
+ const custom = {
25
+ fontSize: parseInt(fontSize),
26
+ fontWeight: fontWeight,
27
+ minimap: {
28
+ enabled: minimapEnabled,
29
+ scale: parseInt(minimapScale),
30
+ },
31
+ wordWrap: wordWrap,
32
+ autoClosingBrackets: { autoClosingBrackets },
33
+ showUnused: true,
34
+ automaticLayout: true,
35
+ tabSize: 2,
36
+ renderLineHighlight: "none",
37
+ scrollbar: { verticalScrollbarSize: 10, verticalSliderSize: 10 },
38
+ };
39
+
40
+ dispatch(set_options(custom));
41
+ };
42
+
43
+ return (
44
+ <motion.div
45
+ className="sm:mt-10 mx-auto h-full sm:h-auto sm:w-8/12 lg:w-6/12 sm:rounded-xl overflow-hidden glassmorphism"
46
+ variants={modalVariant}
47
+ initial="initial"
48
+ animate="animate"
49
+ exit="exit"
50
+ transition={{ ease: "easeOut", duration: 0.3 }}
51
+ onClick={(e) => e.stopPropagation()}
52
+ >
53
+ <div
54
+ style={{ backgroundColor: theme.foreground }}
55
+ className="border-b border-black h-10 flex items-center px-7"
56
+ >
57
+ <h1 className="text-xl"> Settings</h1>
58
+ </div>
59
+
60
+ <div className="overflow-auto h-full">
61
+ <div className="pt-6 px-7 ">
62
+ <form
63
+ className=" mb-12 overflow-auto"
64
+ onSubmit={handleSubmit(onChangeOptions)}
65
+ >
66
+ {/* Fonts */}
67
+ <div className="editor-sub-settings">
68
+ <div className="title">Font size : </div>
69
+ <div className="description">Set the font size in pixels.</div>
70
+ <select
71
+ {...register("fontSize")}
72
+ id="fontSize"
73
+ className="editor-select"
74
+ defaultValue={options.fontSize}
75
+ style={{
76
+ backgroundColor: theme.foreground,
77
+ }}
78
+ >
79
+ <option value={9}>9</option>
80
+ <option value={10}>10</option>
81
+ <option value={11}>11</option>
82
+ <option value={12}>12</option>
83
+ <option value={13}>13</option>
84
+ <option value={14}>14</option>
85
+ <option value={15}>15</option>
86
+ <option value={16}>16</option>
87
+ <option value={17}>17</option>
88
+ </select>
89
+ </div>
90
+
91
+ <div className="editor-sub-settings">
92
+ <div className="title">Font weight : </div>
93
+ <div className="description">Defines how bold you text are.</div>
94
+ <select
95
+ {...register("fontWeight")}
96
+ id="fontWeight"
97
+ className="editor-select"
98
+ defaultValue={options.fontWeight}
99
+ style={{
100
+ backgroundColor: theme.foreground,
101
+ }}
102
+ >
103
+ <option value="500">Regular</option>
104
+ <option value="600">Bold</option>
105
+ </select>
106
+ </div>
107
+
108
+ {/* Minimap */}
109
+ <div className="editor-sub-settings">
110
+ <div className="title">Enabled minimap : </div>
111
+ <div className="description">
112
+ Control if the minimap should be shown.
113
+ </div>
114
+ <div className="flex items-center">
115
+ <input
116
+ {...register("minimapEnabled")}
117
+ id="minimap-enabled"
118
+ className="editor-select mr-2"
119
+ type="checkbox"
120
+ defaultChecked={options.minimap?.enabled}
121
+ style={{
122
+ backgroundColor: theme.foreground,
123
+ }}
124
+ />
125
+ <label>On </label>
126
+ </div>
127
+ </div>
128
+
129
+ <div className="editor-sub-settings">
130
+ <div className="title">Scale : </div>
131
+ <div className="description">Set the size of the minimap.</div>
132
+ <select
133
+ {...register("minimapScale")}
134
+ id="minimap-scale"
135
+ className="editor-select"
136
+ defaultValue={options.minimap?.scale}
137
+ style={{
138
+ backgroundColor: theme.foreground,
139
+ }}
140
+ >
141
+ <option value={1}>1</option>
142
+ <option value={2}>2</option>
143
+ <option value={3}>3</option>
144
+ </select>
145
+ </div>
146
+
147
+ {/* Others */}
148
+ <div className="editor-sub-settings">
149
+ <div className="title">Word wrap : </div>
150
+ <div className="description">Control if lines should wrap.</div>
151
+ <select
152
+ {...register("wordWrap")}
153
+ id="wordWrap"
154
+ className="editor-select"
155
+ defaultValue={options.wordWrap}
156
+ style={{
157
+ backgroundColor: theme.foreground,
158
+ }}
159
+ >
160
+ <option value="on">On</option>
161
+ <option value="off">Off</option>
162
+ </select>
163
+ </div>
164
+
165
+ <div className="editor-sub-settings">
166
+ <div className="title">Auto Closing Brackets : </div>
167
+ <div className="description">
168
+ Controls if brackets should close automatically
169
+ </div>
170
+ <select
171
+ {...register("autoClosingBrackets")}
172
+ id="autoClosingBrackets"
173
+ className="editor-select"
174
+ defaultValue={options.autoClosingBrackets}
175
+ style={{
176
+ backgroundColor: theme.foreground,
177
+ }}
178
+ >
179
+ <option value="always">always</option>
180
+ <option value="languageDefined">languageDefined</option>
181
+ <option value="beforeWhitespace">beforeWhitespace</option>
182
+ <option value="never">never</option>
183
+ </select>
184
+ </div>
185
+
186
+ <button className="editor-button h-9 mt-6" type="submit">
187
+ Save settings
188
+ </button>
189
+ </form>
190
+ </div>
191
+ </div>
192
+ </motion.div>
193
+ );
194
+ };
195
+
196
+ export default SettingsModal;
components/Modals/TemplateModal.tsx ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { motion } from "framer-motion";
2
+ import { useTree } from "../../hooks";
3
+ import { treeTemplates } from "../../constants";
4
+ import { useAppSelector } from "../../store/hook";
5
+ import { theme_state } from "../../store/features/themeSlice";
6
+ import { compiler_state } from "../../store/features/compilerSlice";
7
+ import { TemplateSelectionSkeleton } from "../Skeleton/TemplateSelectionSkeleton";
8
+ import { modalVariant } from "./config";
9
+
10
+ const TemplateModal = () => {
11
+ const { theme } = useAppSelector(theme_state);
12
+ const { esbuildStatus } = useAppSelector(compiler_state);
13
+ const { setTree } = useTree();
14
+
15
+ let arr = [];
16
+
17
+ for (const item of Object.entries(treeTemplates)) {
18
+ arr.push(item);
19
+ }
20
+
21
+ const templates = arr.map((template, key) => (
22
+ <button
23
+ key={key}
24
+ name={template[1].name}
25
+ onClick={() => setTree(template[1])}
26
+ className="p-2 rounded-sm"
27
+ >
28
+ <div className="flex pointer-events-none">
29
+ <img
30
+ alt={template[1].name}
31
+ src={template[1].iconSrc}
32
+ className="h-9 w-9 rounded-sm"
33
+ />
34
+ <div className="flex flex-col items-start justify-start pl-4">
35
+ <div>{template[1].name}</div>
36
+ <div className="text-xs text-gray-400">{template[1].description}</div>
37
+ </div>
38
+ </div>
39
+ </button>
40
+ ));
41
+
42
+ return (
43
+ <motion.div
44
+ className="sm:mt-44 mx-auto h-full sm:h-auto sm:w-8/12 lg:w-6/12 sm:pb-20 sm:rounded-xl overflow-hidden glassmorphism"
45
+ variants={modalVariant}
46
+ initial="initial"
47
+ animate="animate"
48
+ exit="exit"
49
+ transition={{ ease: "easeOut", duration: 0.3 }}
50
+ onClick={(e) => e.stopPropagation()}
51
+ >
52
+ <div
53
+ style={{ backgroundColor: theme.foreground }}
54
+ className="border-b border-black h-10 flex items-center px-7"
55
+ >
56
+ <h1 className="text-xl">Templates</h1>
57
+ </div>
58
+ <div className="pt-6 px-7 flex flex-wrap gap-x-14 gap-y-8">
59
+ {esbuildStatus.isReady ? templates : <TemplateSelectionSkeleton />}
60
+ </div>
61
+ </motion.div>
62
+ );
63
+ };
64
+
65
+ export default TemplateModal;
components/Modals/config.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { Variants } from "framer-motion";
2
+
3
+ export const modalVariant: Variants = {
4
+ initial: { scale: 0.95, opacity: 0 },
5
+ animate: { scale: 1, opacity: 1 },
6
+ exit: { scale: 0.98, opacity: 0 },
7
+ };
components/Playground/ConsoleLog.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import dynamic from "next/dynamic";
3
+ import { useAppDispatch, useAppSelector } from "../../store/hook";
4
+ import { clear_logs } from "../../store/features/editorSlice";
5
+
6
+ import { theme_state } from "../../store/features/themeSlice";
7
+
8
+ const Console = dynamic(import("console-feed/lib/Component"), { ssr: false });
9
+
10
+ interface LogsProps {
11
+ logs: any;
12
+ }
13
+
14
+ const Logs = ({ logs }: LogsProps) => {
15
+ const { theme } = useAppSelector(theme_state);
16
+ const dispatch = useAppDispatch();
17
+
18
+ return (
19
+ <div className="text-gray-300 relative h-full">
20
+ <div className="flex items-center justify-between px-3 h-9">
21
+ <button
22
+ onClick={() => dispatch(clear_logs())}
23
+ className="editor-button h-5 mr-3"
24
+ >
25
+ Clear
26
+ </button>
27
+ </div>
28
+
29
+ <div className="h-80 overflow-auto">
30
+ <Console
31
+ styles={{
32
+ BASE_FONT_FAMILY: '"Rubik", sans-serif;',
33
+ BASE_FONT_SIZE: 14,
34
+ BASE_BACKGROUND_COLOR: theme.background,
35
+ LOG_BORDER: theme.border,
36
+ }}
37
+ logs={logs}
38
+ variant="dark"
39
+ />
40
+ </div>
41
+ </div>
42
+ );
43
+ };
44
+
45
+ const ConsoleLog = React.memo(Logs);
46
+
47
+ export default ConsoleLog;
components/Playground/Footer.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { useAppDispatch, useAppSelector } from "../../store/hook";
3
+ import {
4
+ editor_state,
5
+ toggle_logs_tab,
6
+ } from "../../store/features/editorSlice";
7
+ import { compiler_state } from "../../store/features/compilerSlice";
8
+ import { RiTerminalLine } from "react-icons/ri";
9
+
10
+ const Footer = () => {
11
+ const { logs } = useAppSelector(editor_state);
12
+ const { isCompiling } = useAppSelector(compiler_state);
13
+ const dispatch = useAppDispatch();
14
+
15
+ return (
16
+ <div
17
+ style={{ height: "2rem" }}
18
+ className="h-full w-full flex items-center justify-between bg-tree-hard border-t-2 border-black text-gray-300 pl-5 pb-0.5 flex-shrink-0 z-50"
19
+ >
20
+ <div className="flex items-center">
21
+ {isCompiling && (
22
+ <div className="ml-10">
23
+ <span className="loader --1" />
24
+ </div>
25
+ )}
26
+ </div>
27
+
28
+ <div className="flex justify-center items-center">
29
+ <div
30
+ onClick={() => dispatch(toggle_logs_tab())}
31
+ className="flex items-center cursor-pointer text-gray-400"
32
+ >
33
+ <RiTerminalLine size={20} />
34
+ </div>
35
+
36
+ <div className="ml-2 w-6">
37
+ {logs.length > 0 && (
38
+ <div className="h-5 w-5 flex justify-center items-center">
39
+ <div className="absolute p-1.5 bg-teal-400 rounded-full animate-ping" />
40
+ <div className="absolute p-1 bg-teal-400 border-white rounded-full" />
41
+ </div>
42
+ )}
43
+ </div>
44
+ </div>
45
+ </div>
46
+ );
47
+ };
48
+
49
+ export default Footer;
components/Playground/Header.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { RiAddFill, RiSettings3Fill } from "react-icons/ri";
3
+ import { useAppDispatch } from "../../store/hook";
4
+ import { ModalEnum, open_modal } from "../../store/features/modalSlice";
5
+
6
+ const Header = () => {
7
+ const dispatch = useAppDispatch();
8
+
9
+ return (
10
+ <div style={{ height: "3rem" }} className="flex justify-end px-12">
11
+ <div className="flex gap-4 items-center ">
12
+ <RiAddFill
13
+ size={24}
14
+ className="cursor-pointer text-gray-400"
15
+ onClick={() => dispatch(open_modal(ModalEnum.TEMPLATE))}
16
+ />
17
+ <RiSettings3Fill
18
+ size={20}
19
+ className="cursor-pointer text-gray-400"
20
+ onClick={() => dispatch(open_modal(ModalEnum.SETTINGS))}
21
+ />
22
+ </div>
23
+ </div>
24
+ );
25
+ };
26
+
27
+ export default Header;
components/Playground/Iframe.tsx ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useRef, useEffect } from "react";
2
+
3
+ import { useAppDispatch } from "../../store/hook";
4
+ import { update_logs } from "../../store/features/editorSlice";
5
+ import { getCompileCode } from "../../store/features/compilerSlice";
6
+ import { createIframeContent } from "../../utils/createIframeContent";
7
+ import { IframeLoaderScreen } from "./IframeLoaderScreen";
8
+ import { IframeErrorScreen } from "./IframeErrorScreen";
9
+ import { LanguagesInterface } from "../../_types/editorTypes";
10
+ import { CompilerOutput, CompilerStatus } from "../../_types/compilerTypes";
11
+
12
+ interface IframeProps {
13
+ tabs: LanguagesInterface;
14
+ output: CompilerOutput;
15
+ isCompiling: boolean;
16
+ esbuildStatus: CompilerStatus;
17
+ }
18
+
19
+ const IframePanel = ({
20
+ tabs,
21
+ output,
22
+ isCompiling,
23
+ esbuildStatus,
24
+ }: IframeProps) => {
25
+ const iframe = useRef<any>();
26
+ const dispatch = useAppDispatch();
27
+
28
+ const htmlFrameContent = createIframeContent(tabs.css?.data, tabs.html?.data);
29
+
30
+ //=== incoming message
31
+ useEffect(() => {
32
+ window.onmessage = function (response: MessageEvent) {
33
+ if (response.data && response.data.source === "iframe") {
34
+ let errorObject = {
35
+ method: "error",
36
+ id: Date.now(),
37
+ data: [`${response.data.message}`],
38
+ };
39
+ dispatch(update_logs(errorObject));
40
+ }
41
+ };
42
+
43
+ if (tabs.javascript && esbuildStatus.isReady) {
44
+ setTimeout(async () => {
45
+ dispatch(
46
+ getCompileCode(tabs.javascript.data, tabs.javascript.entryPoints)
47
+ );
48
+ }, 50);
49
+ }
50
+ }, [dispatch, tabs, esbuildStatus.isReady]);
51
+
52
+ //=== outgoing massage
53
+ useEffect(() => {
54
+ iframe.current.srcdoc = htmlFrameContent;
55
+
56
+ setTimeout(async () => {
57
+ iframe?.current?.contentWindow?.postMessage(output.code, "*");
58
+ }, 40);
59
+ }, [htmlFrameContent, output]);
60
+
61
+ return (
62
+ <div className="iframe-container">
63
+ {/* build error */}
64
+ {output.error ? <IframeErrorScreen err={output.error} /> : ""}
65
+
66
+ {/* Loading screen */}
67
+ {isCompiling ? (
68
+ <div className="absolute h-full w-full bg-gray-50 z-40">
69
+ <IframeLoaderScreen />
70
+ </div>
71
+ ) : (
72
+ ""
73
+ )}
74
+
75
+ <iframe
76
+ id="super-iframe"
77
+ sandbox="allow-downloads allow-forms allow-modals allow-pointer-lock allow-popups allow-presentation allow-same-origin allow-scripts allow-top-navigation-by-user-activation"
78
+ allow="accelerometer; camera; encrypted-media; geolocation; gyroscope; microphone; midi; clipboard-read; clipboard-write"
79
+ scrolling="auto"
80
+ frameBorder="0"
81
+ ref={iframe}
82
+ title="previewWindow"
83
+ srcDoc={htmlFrameContent}
84
+ onLoad={async () => {
85
+ const Hook = (await import("console-feed")).Hook;
86
+ Hook(
87
+ iframe.current.contentWindow.console,
88
+ (log) => {
89
+ dispatch(update_logs(log));
90
+ },
91
+ false
92
+ );
93
+ }}
94
+ />
95
+ </div>
96
+ );
97
+ };
98
+
99
+ const Iframe = React.memo(IframePanel);
100
+
101
+ export default Iframe;
components/Playground/IframeErrorScreen.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ export const IframeErrorScreen = ({ err }: any) => {
4
+ return (
5
+ <div className="px-5 pt-10 text-red-600 tracking-wide absolute h-full w-full z-50 backdrop-filter backdrop-blur">
6
+ {err}
7
+ </div>
8
+ );
9
+ };
components/Playground/IframeLoaderScreen.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ export const IframeLoaderScreen = () => {
4
+ return (
5
+ <div className="boxes">
6
+ <div className="box">
7
+ <div />
8
+ <div />
9
+ <div />
10
+ <div />
11
+ </div>
12
+ <div className="box">
13
+ <div />
14
+ <div />
15
+ <div />
16
+ <div />
17
+ </div>
18
+ <div className="box">
19
+ <div />
20
+ <div />
21
+ <div />
22
+ <div />
23
+ </div>
24
+ <div className="box">
25
+ <div />
26
+ <div />
27
+ <div />
28
+ <div />
29
+ </div>
30
+ </div>
31
+ );
32
+ };
components/Playground/InputCodeTab.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState } from "react";
2
+ import Tabs, { TabPane } from "rc-tabs";
3
+ import { Monaco } from "./Monaco";
4
+ import { EditorValueInterface } from "../../_types/editorTypes";
5
+
6
+ const InputCode = ({ editorValue: parentEditorValue }: { editorValue: EditorValueInterface }) => {
7
+ const [editorValue, setEditorValue] = useState(parentEditorValue);
8
+
9
+ // Update local state when parent state updates
10
+ useEffect(() => {
11
+ setEditorValue(parentEditorValue);
12
+ }, [parentEditorValue]);
13
+
14
+ const dataToMap = Object.entries(editorValue.tabs);
15
+
16
+ const tabPane = dataToMap.map((item, key) => (
17
+ <TabPane tab={<div>{item[1].title}</div>} key={key}>
18
+ <Monaco tab={item[0]} monacoLanguage={item[1].monacoLanguage} />
19
+ </TabPane>
20
+ ));
21
+
22
+ return (
23
+ <Tabs
24
+ className="editor-input-tabs"
25
+ tabPosition="top"
26
+ tabBarGutter={16}
27
+ defaultActiveKey="javascript"
28
+ >
29
+ {tabPane}
30
+ </Tabs>
31
+ );
32
+ };
33
+
34
+ const InputCodeTab = React.memo(InputCode);
35
+
36
+ export default InputCodeTab;
components/Playground/Monaco.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect } from "react";
2
+ import MonacoEditor from "@monaco-editor/react";
3
+ import { useMonaco } from "../../hooks";
4
+ import { useAppDispatch, useAppSelector } from "../../store/hook";
5
+ import {
6
+ editor_state,
7
+ update_editor_code,
8
+ } from "../../store/features/editorSlice";
9
+
10
+ type MonacoType = { monacoLanguage: string | undefined; tab: string };
11
+
12
+ export const Monaco = ({ monacoLanguage, tab }: MonacoType) => {
13
+ const dispatch = useAppDispatch();
14
+ const { monacoInputValue, options } = useAppSelector(editor_state);
15
+ const { onChange, onMount, code } = useMonaco();
16
+
17
+ useEffect(() => {
18
+ if (code && code?.length >= 1)
19
+ dispatch(update_editor_code({ type: tab, content: code }));
20
+ }, [code, dispatch, tab]);
21
+
22
+ return (
23
+ <MonacoEditor
24
+ onChange={onChange}
25
+ onMount={onMount}
26
+ language={monacoLanguage}
27
+ theme="vs-dark"
28
+ options={options}
29
+ className="h-full"
30
+ // @ts-ignore
31
+ value={monacoInputValue.tabs[tab]?.data || ''}
32
+ />
33
+ );
34
+ };
components/Playground/Pane.tsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { ReactNode } from "react";
2
+ import { Allotment } from "allotment";
3
+ import { LayoutPriority } from "allotment/dist/types/src/split-view";
4
+
5
+ interface Pane {
6
+ panelA: ReactNode;
7
+ panelB: ReactNode;
8
+ panelC: ReactNode;
9
+ lastPanelVisibility: boolean;
10
+ }
11
+
12
+ const EditorPanel = ({ panelA, panelB, panelC, lastPanelVisibility }: Pane) => {
13
+ return (
14
+ <div style={{ height: "calc(100vh - 8rem)" }}>
15
+ <Allotment>
16
+ <Allotment.Pane>{panelA}</Allotment.Pane>
17
+
18
+ <Allotment onVisibleChange={function noRefCheck() {}} vertical={true}>
19
+ <Allotment.Pane
20
+ priority={"HIGH" as LayoutPriority}
21
+ preferredSize="70%"
22
+ visible
23
+ >
24
+ {panelB}
25
+ </Allotment.Pane>
26
+
27
+ <Allotment.Pane
28
+ preferredSize="30%"
29
+ priority={"LOW" as LayoutPriority}
30
+ snap
31
+ visible={lastPanelVisibility}
32
+ >
33
+ {panelC}
34
+ </Allotment.Pane>
35
+ </Allotment>
36
+ </Allotment>
37
+ </div>
38
+ );
39
+ };
40
+
41
+ export default EditorPanel;
components/Playground/index.tsx ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useMemo, useRef, useEffect, useState, ChangeEvent, FormEvent } from "react";
2
+ import { useChat, Message } from "ai/react";
3
+ import { FunctionCallHandler, nanoid } from "ai";
4
+ import { useAppDispatch, useAppSelector } from "../../store/hook";
5
+ import {
6
+ compiler_state,
7
+ initEsbuild,
8
+ } from "../../store/features/compilerSlice";
9
+ import { editor_state, set_monaco_input_value, update_editor_code } from "../../store/features/editorSlice";
10
+ import { theme_state } from "../../store/features/themeSlice";
11
+ import { ModalEnum, open_modal } from "../../store/features/modalSlice";
12
+
13
+ import ConsoleLog from "./ConsoleLog";
14
+ import Iframe from "./Iframe";
15
+ import InputCodeTab from "./InputCodeTab";
16
+ import Footer from "./Footer";
17
+ import Pane from "./Pane";
18
+ import { SendIcon } from "../../constants/icon";
19
+ import ReactMarkdown from "react-markdown";
20
+
21
+ const Playground = () => {
22
+ const dispatch = useAppDispatch();
23
+ const formRef = useRef<HTMLFormElement>(null);
24
+ const inputRef = useRef<HTMLTextAreaElement>(null);
25
+ const { theme } = useAppSelector(theme_state);
26
+ const { esbuildStatus, isCompiling, output } = useAppSelector(compiler_state);
27
+ const { logs, editorValue, isLogTabOpen } = useAppSelector(editor_state);
28
+ const [markdownCode, setMarkdownCode] = useState('');
29
+ const [prevMarkdownCode, setPrevMarkdownCode] = useState(markdownCode);
30
+
31
+ const isValidCodeBlock = (markdownCode: string) => {
32
+ return markdownCode && markdownCode.length > 10 && markdownCode.includes('\n');
33
+ }
34
+
35
+ useEffect(() => {
36
+ const timer = setInterval(() => {
37
+ if (isValidCodeBlock(markdownCode) && markdownCode !== prevMarkdownCode) {
38
+ dispatch(update_editor_code({ type: 'javascript', content: markdownCode }));
39
+ setPrevMarkdownCode(markdownCode);
40
+ }
41
+ }, 2000);
42
+
43
+ return () => {
44
+ clearInterval(timer);
45
+ };
46
+ }, [markdownCode, prevMarkdownCode, dispatch]);
47
+
48
+
49
+ const { messages, input, setInput, handleSubmit } = useChat({
50
+ onError: (error) => {
51
+ console.error(error);
52
+ },
53
+ });
54
+
55
+ useEffect(() => {
56
+ if (!esbuildStatus.isReady) {
57
+ dispatch(initEsbuild());
58
+ }
59
+ }, [dispatch, esbuildStatus]);
60
+
61
+ useEffect(() => {
62
+ dispatch(open_modal(ModalEnum.TEMPLATE));
63
+ }, [dispatch]);
64
+
65
+ useEffect(() => {
66
+ if(isValidCodeBlock(markdownCode)) {
67
+ const newEditorValue = {
68
+ name: "React",
69
+ description: "By codetree",
70
+ public: true,
71
+ iconSrc: "/icons/reactjs.svg",
72
+ tabs: {
73
+ javascript: {
74
+ title: "JS/JSX",
75
+ entryPoints: "index.js",
76
+ monacoLanguage: "javascript",
77
+ data: markdownCode
78
+ },
79
+ html: {
80
+ title: "index.html",
81
+ entryPoints: "index.html",
82
+ monacoLanguage: "html",
83
+ data: ""
84
+ },
85
+ css: {
86
+ title: "main.css",
87
+ entryPoints: "main.css",
88
+ monacoLanguage: "css",
89
+ data: ""
90
+ }
91
+ }
92
+ };
93
+
94
+ dispatch(set_monaco_input_value(newEditorValue as any));
95
+ }
96
+ }, [markdownCode, dispatch]);
97
+
98
+ return (
99
+ <div style={{ background: theme.background }}>
100
+ <div className="flex flex-col">
101
+ <div className="px-4 pb-2 pt-3 shadow-lg sm:pb-3 sm:pt-4">
102
+ <form
103
+ ref={formRef}
104
+ onSubmit={handleSubmit}
105
+ className="relative w-full"
106
+ >
107
+ <textarea ref={inputRef} onChange={(e) => setInput(e.target.value)}
108
+ placeholder="Chat with GPT-4 and code blocks will automatically render in the editor! Codetree uses WebAssembly and Esbuild to compile in the browser."
109
+ onKeyDown={(e) => {
110
+ if (e.key === "Enter" && !e.shiftKey) {
111
+ formRef.current?.requestSubmit();
112
+ e.preventDefault();
113
+ }
114
+ }}
115
+ spellCheck={false} className="textarea" value={input} />
116
+ <button
117
+ type="submit"
118
+ className="absolute inset-y-0 right-3 my-auto flex h-8 w-8 items-center justify-center rounded-md transition-all"
119
+ >
120
+ <SendIcon
121
+ className={"h-4 w-4"}
122
+ />
123
+ </button>
124
+ </form>
125
+ </div>
126
+ <div className="flex flex-col items-start space-y-4 overflow-y-auto max-h-[20vh]">
127
+ {messages?.map((message, index) => (
128
+ <p key={index} className="messages-text">
129
+ <ReactMarkdown
130
+ className="prose mt-1 w-full break-words prose-p:leading-relaxed"
131
+ components={{
132
+ a: (props) => (
133
+ <a {...props} target="_blank" rel="noopener noreferrer" />
134
+ ),
135
+ // @ts-ignore
136
+ code: ({node, ...props}) => {
137
+ const codeValue = props.children[0] || '';
138
+ setMarkdownCode(codeValue as any);
139
+
140
+ return null;
141
+ }
142
+ }}
143
+ >
144
+ {message.content}
145
+ </ReactMarkdown>
146
+ </p>
147
+ ))}
148
+ </div>
149
+ </div>
150
+ <Pane
151
+ panelA={<InputCodeTab editorValue={editorValue} />}
152
+ panelB={
153
+ <Iframe
154
+ tabs={editorValue.tabs}
155
+ output={output}
156
+ isCompiling={isCompiling}
157
+ esbuildStatus={esbuildStatus}
158
+ />
159
+ }
160
+ panelC={<ConsoleLog logs={logs} />}
161
+ lastPanelVisibility={isLogTabOpen}
162
+ />
163
+
164
+ <Footer />
165
+ </div>
166
+
167
+ );
168
+ };
169
+
170
+ export default Playground;
components/Skeleton/TemplateSelectionSkeleton.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ export const TemplateSelectionSkeleton = () => {
4
+ let items = [];
5
+
6
+ for (let i = 0; i < 10; i++) {
7
+ items.push(
8
+ <div key={i} className="flex animate-pulse">
9
+ <div className="h-9 w-9 bg-gray-400 rounded-sm" />
10
+ <div className="flex flex-col items-start justify-start pl-4 space-y-2.5 mt-1.5">
11
+ <div className="h-2 w-12 bg-gray-400 rounded-sm" />
12
+ <div className="h-2 w-20 bg-gray-400 rounded-sm" />
13
+ </div>
14
+ </div>
15
+ );
16
+ }
17
+
18
+ return <>{items}</>;
19
+ };
constants/icon.tsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ export const SendIcon = ({ className }: { className?: string }) => {
4
+ return (
5
+ <svg
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ viewBox="0 0 16 16"
8
+ fill="none"
9
+ className={className}
10
+ strokeWidth="2"
11
+ >
12
+ <path
13
+ d="M.5 1.163A1 1 0 0 1 1.97.28l12.868 6.837a1 1 0 0 1 0 1.766L1.969 15.72A1 1 0 0 1 .5 14.836V10.33a1 1 0 0 1 .816-.983L8.5 8 1.316 6.653A1 1 0 0 1 .5 5.67V1.163Z"
14
+ fill="currentColor"
15
+ ></path>
16
+ </svg>
17
+ );
18
+ };
constants/index.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ export * from "./templates";
2
+ export * from "./monacoOptions";
constants/monacoOptions.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { editor } from "monaco-editor";
2
+
3
+ export const monacoOptions: editor.IStandaloneEditorConstructionOptions = {
4
+ fontSize: 12,
5
+ fontWeight: "500",
6
+ minimap: {
7
+ enabled: false,
8
+ },
9
+ wordWrap: "on",
10
+ autoClosingBrackets: "always",
11
+ showUnused: true,
12
+ automaticLayout: true,
13
+ tabSize: 2,
14
+ renderLineHighlight: "none",
15
+ scrollbar: { verticalScrollbarSize: 10, verticalSliderSize: 10 },
16
+ };
constants/templates.ts ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const treeTemplates = {
2
+ _empty: {
3
+ name: "Empty",
4
+ description: "By codetree",
5
+ public: true,
6
+ iconSrc: "/icons/vanilla.svg",
7
+ tabs: {
8
+ javascript: {
9
+ title: "app.jsx",
10
+ entryPoints: "index.js",
11
+ monacoLanguage: "javascript",
12
+ data: ``,
13
+ },
14
+ html: {
15
+ title: "index.html",
16
+ entryPoints: "index.html",
17
+ monacoLanguage: "html",
18
+ data: ``,
19
+ },
20
+ css: {
21
+ title: "main.css",
22
+ entryPoints: "main.css",
23
+ monacoLanguage: "css",
24
+ data: ``,
25
+ },
26
+ },
27
+ },
28
+ _vanilla: {
29
+ name: "Vanilla",
30
+ description: "By codetree",
31
+ public: true,
32
+ iconSrc: "/icons/vanilla.svg",
33
+ tabs: {
34
+ javascript: {
35
+ title: "JS/JSX",
36
+ entryPoints: "index.js",
37
+ monacoLanguage: "javascript",
38
+ data: `document.getElementById("app").innerHTML = \`
39
+ <h1>Vanilla</h1>
40
+ <div>
41
+ Bare minimal javascript template
42
+ </div>
43
+ \`;
44
+ `,
45
+ },
46
+ html: {
47
+ title: "html",
48
+ entryPoints: "index.html",
49
+ monacoLanguage: "html",
50
+ data: `<div id="app"></div>`,
51
+ },
52
+ css: {
53
+ title: "Css",
54
+ entryPoints: "main.css",
55
+ monacoLanguage: "css",
56
+ data: `body {
57
+ font-family: sans-serif;
58
+ }
59
+
60
+ #app {
61
+ display: flex;
62
+ flex-direction: column;
63
+ justify-content: center;
64
+ align-items: center;
65
+ }
66
+ `,
67
+ },
68
+ },
69
+ },
70
+ _typescript: {
71
+ name: "Typescript",
72
+ description: "By codetree",
73
+ public: true,
74
+ iconSrc: "/icons/typescript.svg",
75
+ tabs: {
76
+ javascript: {
77
+ title: "Ts/Tsx",
78
+ entryPoints: "index.ts",
79
+ monacoLanguage: "typescript",
80
+ data: `function add(x: number, y: number): number {
81
+ return x + y;
82
+ }
83
+
84
+ const result = add(2,5)
85
+
86
+ console.log(result)`,
87
+ },
88
+ html: {
89
+ title: "Html",
90
+ entryPoints: "index.html",
91
+ monacoLanguage: "html",
92
+ data: `<div id="app">
93
+ <h1>Typescript</h1>
94
+ <div>Bare minimal Typescript template 🚀</div>
95
+ </div>
96
+ `,
97
+ },
98
+ css: {
99
+ title: "Css",
100
+ entryPoints: "main.css",
101
+ monacoLanguage: "css",
102
+ data: `body {
103
+ font-family: sans-serif;
104
+ }
105
+
106
+ #app {
107
+ display: flex;
108
+ flex-direction: column;
109
+ justify-content: center;
110
+ align-items: center;
111
+ }
112
+ `,
113
+ },
114
+ },
115
+ },
116
+ _p5: {
117
+ name: "P5js",
118
+ description: "By codetree",
119
+ public: true,
120
+ iconSrc: "/icons/p5-dot-js.svg",
121
+ tabs: {
122
+ javascript: {
123
+ title: "JS/JSX",
124
+ entryPoints: "index.js",
125
+ monacoLanguage: "javascript",
126
+ data: `import p5 from "p5";
127
+
128
+ let sketch = function (p) {
129
+ p.setup = function () {
130
+ p.createCanvas(window.innerWidth, window.innerHeight);
131
+ p.background(0);
132
+ };
133
+ p.draw = () => {
134
+ // sketch.background(100);
135
+ p.fill(255);
136
+ p.ellipse(p.mouseX, p.mouseY, 50, 50);
137
+ };
138
+ };
139
+
140
+ new p5(sketch, window.document.getElementById("container"));
141
+ `,
142
+ },
143
+ html: {
144
+ title: "Html",
145
+ entryPoints: "index.html",
146
+ monacoLanguage: "html",
147
+ data: ``,
148
+ },
149
+ css: {
150
+ title: "Css",
151
+ entryPoints: "main.css",
152
+ monacoLanguage: "css",
153
+ data: `body {
154
+ margin: 0px;
155
+ font-family: sans-serif;
156
+ }
157
+ canvas {
158
+ margin: 0px;
159
+ }
160
+ `,
161
+ },
162
+ },
163
+ },
164
+ _react: {
165
+ name: "React",
166
+ description: "By codetree",
167
+ public: true,
168
+ iconSrc: "/icons/reactjs.svg",
169
+ tabs: {
170
+ javascript: {
171
+ title: "JS/JSX",
172
+ entryPoints: "index.js",
173
+ monacoLanguage: "javascript",
174
+ data: `import React, { useState } from "react";
175
+ import ReactDOM from "react-dom";
176
+
177
+ function App() {
178
+ const [count, setCount] = useState(0);
179
+
180
+ return (
181
+ <div className="App">
182
+ <h1>Hello ReactTree</h1>
183
+ <h2>You clicked {count} times!</h2>
184
+
185
+ <button onClick={() => setCount(count - 1)}>Decrement</button>
186
+ <button onClick={() => setCount(count + 1)}>Increment</button>
187
+ </div>
188
+ );
189
+ }
190
+
191
+ const rootElement = document.getElementById("root");
192
+ ReactDOM.render(<App />, rootElement);
193
+ `,
194
+ },
195
+ html: {
196
+ title: "index.html",
197
+ entryPoints: "index.html",
198
+ monacoLanguage: "html",
199
+ data: ``,
200
+ },
201
+ css: {
202
+ title: "main.css",
203
+ entryPoints: "main.css",
204
+ monacoLanguage: "css",
205
+ data: `.App {
206
+ font-family: sans-serif;
207
+ text-align: center;
208
+ }
209
+ `,
210
+ },
211
+ },
212
+ },
213
+ _gsap: {
214
+ name: "Gsap",
215
+ description: "By codetree",
216
+ public: true,
217
+ iconSrc: "/icons/gsap-greensock.svg",
218
+ tabs: {
219
+ javascript: {
220
+ title: "JS/JSX",
221
+ entryPoints: "index.js",
222
+ monacoLanguage: "javascript",
223
+ data: `import gsap from "gsap"
224
+
225
+ var tl = gsap.timeline({ repeat: -1 });
226
+ tl.to("h1", 30, { backgroundPosition: "-960px 0" });
227
+ `,
228
+ },
229
+ html: {
230
+ title: "Html",
231
+ entryPoints: "index.html",
232
+ monacoLanguage: "html",
233
+ data: `<div class="wrapper">
234
+ <h1 class="title">Codetree<h1>
235
+ </div> `,
236
+ },
237
+ css: {
238
+ title: "Css",
239
+ entryPoints: "main.css",
240
+ monacoLanguage: "css",
241
+ data: `body {
242
+ background-color: #1d1d1d;
243
+ margin: 0;
244
+ padding: 0;
245
+ }
246
+
247
+ .wrapper {
248
+ display: flex;
249
+ height: 100vh;
250
+ justify-content: center;
251
+ align-items: center;
252
+ }
253
+
254
+ h1 {
255
+ max-width: 75%;
256
+ font-size: 100px;
257
+ text-align: center;
258
+ font-family: "Montserrat", sans-serif;
259
+ color: #540032;
260
+ }
261
+
262
+ .title {
263
+ background-image: url(https://cdn.pixabay.com/photo/2017/07/03/20/17/abstract-2468874_960_720.jpg);
264
+ background-attachment: fixed;
265
+ -webkit-text-fill-color: transparent;
266
+ -webkit-background-clip: text;
267
+ }`,
268
+ },
269
+ },
270
+ },
271
+ };
editor.d.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ declare module "react-split";
2
+ declare module "monaco-jsx-highlighter";
esbuild/plugins/index.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ export * from "./unpkg-fecth-plugin";
2
+ export * from "./unpkg-path-plugin";
esbuild/plugins/unpkg-fecth-plugin.ts ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as esbuild from "esbuild-wasm";
2
+ import axios from "axios";
3
+ import localForage from "localforage";
4
+
5
+ const fileCache = localForage.createInstance({
6
+ name: "fileCache",
7
+ });
8
+
9
+ export const unpkgFetchPlugin = (
10
+ inputCode: string | undefined,
11
+ entryPoint: string
12
+ ): esbuild.Plugin => {
13
+ return {
14
+ name: "unpkg-fetch-plugin",
15
+ setup(build: esbuild.PluginBuild) {
16
+ //match entrypoint
17
+ if (entryPoint === "index.ts") {
18
+ build.onLoad({ filter: /(^index\.ts$)/ }, () => {
19
+ return {
20
+ loader: "tsx",
21
+ contents: inputCode,
22
+ };
23
+ });
24
+ } else {
25
+ build.onLoad({ filter: /(^index\.js$)/ }, () => {
26
+ return {
27
+ loader: "jsx",
28
+ contents: inputCode,
29
+ };
30
+ });
31
+ }
32
+
33
+ build.onLoad({ filter: /.*/ }, async (args: esbuild.OnLoadArgs) => {
34
+ const cacheResult = await fileCache.getItem<esbuild.OnLoadResult>(
35
+ args.path
36
+ );
37
+ if (cacheResult) {
38
+ return cacheResult;
39
+ }
40
+ });
41
+
42
+ //match css file
43
+ build.onLoad({ filter: /.css$/ }, async (args: esbuild.OnLoadArgs) => {
44
+ const { data, request } = await axios.get(args.path);
45
+
46
+ const escapedData = data
47
+ .replace(/\n/g, "")
48
+ .replace(/"/g, '\\"')
49
+ .replace(/'/g, "\\'");
50
+
51
+ const contents = `const style = document.createElement("style");
52
+ style.innerText = '${escapedData}';
53
+ document.head.appendChild(style);`;
54
+
55
+ const result: esbuild.OnLoadResult = {
56
+ loader: "jsx",
57
+ contents,
58
+ //specify the place where the content was found
59
+ resolveDir: new URL("./", request.responseURL).pathname,
60
+ };
61
+ //store response in cache
62
+ await fileCache.setItem(args.path, result);
63
+ return result;
64
+ });
65
+
66
+ //=================================================
67
+
68
+ build.onLoad({ filter: /.*/ }, async (args: esbuild.OnLoadArgs) => {
69
+ console.log(`...fetching ${args.path}`);
70
+ const { data, request } = await axios.get(args.path);
71
+
72
+ const result: esbuild.OnLoadResult = {
73
+ loader: "jsx",
74
+ contents: data,
75
+ //specify the place where the content was found
76
+ resolveDir: new URL("./", request.responseURL).pathname,
77
+ };
78
+ //store response in cache
79
+ await fileCache.setItem(args.path, result);
80
+ console.log("end of fetching");
81
+ return result;
82
+ });
83
+ },
84
+ };
85
+ };
esbuild/plugins/unpkg-path-plugin.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as esbuild from "esbuild-wasm";
2
+
3
+ export const unpkgPathPlugin = (): esbuild.Plugin => {
4
+ return {
5
+ name: "unpkg-path-plugin",
6
+ setup(build: esbuild.PluginBuild) {
7
+ //
8
+ build.onResolve({ filter: /.*/ }, (args) => {
9
+ if (args.kind === "entry-point") {
10
+ return { path: args.path, namespace: "a" };
11
+ }
12
+ });
13
+
14
+ //match relative path in a module "./" or "../"
15
+ build.onResolve({ filter: /^\.+\// }, (args: esbuild.OnResolveArgs) => {
16
+ return {
17
+ namespace: "a",
18
+ path: new URL(args.path, `https://unpkg.com${args.resolveDir}/`).href,
19
+ };
20
+ });
21
+
22
+ //match main file in a module
23
+ build.onResolve({ filter: /.*/ }, async (args: esbuild.OnResolveArgs) => {
24
+ return {
25
+ namespace: "a",
26
+ path: `https://unpkg.com/${args.path}`,
27
+ };
28
+ });
29
+ },
30
+ };
31
+ };
graphql.schema.json ADDED
The diff for this file is too large to render. See raw diff
 
graphql/definitions/auth.definition.ts ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { gql } from "@apollo/client";
2
+
3
+ export const meQuery = gql(`query Me {
4
+ me {
5
+ data {
6
+ id
7
+ username
8
+ email
9
+ avatar
10
+ bio
11
+ website
12
+ verifiedAt
13
+ createdAt
14
+ updatedAt
15
+ showProfession
16
+ projects {
17
+ id
18
+ title
19
+ description
20
+ }
21
+ firstname
22
+ lastname
23
+ jobTitle
24
+ country
25
+ }
26
+ message
27
+ status
28
+ }
29
+ }`);
30
+
31
+ export const registerMutation =
32
+ gql(`mutation Register($input: UserRegisterInput!) {
33
+ register(input: $input) {
34
+ data {
35
+ id
36
+ email
37
+ username
38
+ avatar
39
+ bio
40
+ website
41
+ verifiedAt
42
+ createdAt
43
+ updatedAt
44
+ }
45
+ token
46
+ message
47
+ status
48
+ }
49
+ }`);
50
+
51
+ export const loginMutation = gql(`mutation Login($input: UserLoginInput!) {
52
+ login(input: $input) {
53
+ data {
54
+ id
55
+ email
56
+ username
57
+ avatar
58
+ bio
59
+ website
60
+ verifiedAt
61
+ createdAt
62
+ updatedAt
63
+ }
64
+ token
65
+ message
66
+ status
67
+ }
68
+ }`);
69
+
70
+ export const verifiedUserMutation =
71
+ gql(`mutation VerifyUser($input: UserVerifyInput!) {
72
+ verifyUser(input: $input) {
73
+ message
74
+ status
75
+ }
76
+ }`);
77
+
78
+ export const googleOauthMutation =
79
+ gql(`mutation Mutation($input: GoogleAuthInput!) {
80
+ googleOauth(input: $input) {
81
+ data {
82
+ id
83
+ email
84
+ username
85
+ avatar
86
+ bio
87
+ website
88
+ verifiedAt
89
+ createdAt
90
+ updatedAt
91
+ }
92
+ token
93
+ message
94
+ status
95
+ }
96
+ }`);
97
+
98
+ export const githubOauthMutation =
99
+ gql(`mutation Mutation($input: GithubAuthInput!) {
100
+ githubOauth(input: $input) {
101
+ data {
102
+ id
103
+ email
104
+ username
105
+ avatar
106
+ bio
107
+ website
108
+ verifiedAt
109
+ createdAt
110
+ updatedAt
111
+ }
112
+ token
113
+ message
114
+ status
115
+ }
116
+ }`);
117
+
118
+ export const updateProfileMutation =
119
+ gql(`mutation UpdateProfile($input: UpdateProfileInput) {
120
+ updateProfile(input: $input) {
121
+ data {
122
+ avatar
123
+ username
124
+ }
125
+ message
126
+ status
127
+ }
128
+ }`);
129
+
130
+ export const resetPasswordMutation =
131
+ gql(`mutation ResetPassword($input: UserResetPasswordInput!) {
132
+ resetPassword(input: $input) {
133
+ status
134
+ message
135
+ data {
136
+ id
137
+ email
138
+ token
139
+ expiredAt
140
+ createdAt
141
+ updatedAt
142
+ }
143
+ }
144
+ }`);
145
+
146
+ export const forgotPasswordMutation =
147
+ gql(`mutation ForgotPassword($input: UserForgotPasswordInput!) {
148
+ forgotPassword(input: $input) {
149
+ message
150
+ status
151
+ }
152
+ }`);
graphql/definitions/comment.definition.ts ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { gql } from "@apollo/client";
2
+
3
+ export const CommentsQuery = gql(`query Comments($input: CommentsInput!) {
4
+ comments(input: $input) {
5
+ data {
6
+ id
7
+ message
8
+ parentId
9
+ authorId
10
+ createdAt
11
+ updatedAt
12
+ author {
13
+ id
14
+ email
15
+ username
16
+ avatar
17
+ }
18
+ }
19
+ message
20
+ status
21
+ }
22
+ }`);
23
+
24
+ export const CreateCommentMutation =
25
+ gql(`mutation CreateComment($input: CreateCommentInput!) {
26
+ createComment(input: $input) {
27
+ data {
28
+ id
29
+ message
30
+ parentId
31
+ authorId
32
+ createdAt
33
+ updatedAt
34
+ author {
35
+ id
36
+ email
37
+ username
38
+ avatar
39
+ }
40
+ }
41
+ message
42
+ status
43
+ }
44
+ }`);
45
+
46
+ export const UpdateCommentMutation =
47
+ gql(`mutation UpdateComment($input: UpdateCommentInput!) {
48
+ updateComment(input: $input) {
49
+ data {
50
+ id
51
+ message
52
+ parentId
53
+ authorId
54
+ createdAt
55
+ updatedAt
56
+ author {
57
+ id
58
+ email
59
+ username
60
+ avatar
61
+ }
62
+ }
63
+ message
64
+ status
65
+ }
66
+ }`);
67
+
68
+ export const DeleteCommentMutation =
69
+ gql(`mutation DeleteComment($input: DeleteCommentInput!) {
70
+ deleteComment(input: $input) {
71
+ data {
72
+ id
73
+ }
74
+ message
75
+ status
76
+ }
77
+ }`);
78
+
79
+ export const CommentCreatedSubscription = gql(`subscription CommentCreated {
80
+ commentCreated {
81
+ type
82
+ message
83
+ data {
84
+ id
85
+ type
86
+ receiverId
87
+ projectId
88
+ content
89
+ viewed
90
+ createdAt
91
+ sender {
92
+ avatar
93
+ username
94
+ }
95
+ }
96
+ }
97
+ }`);
graphql/definitions/notification.definition.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { gql } from "@apollo/client";
2
+
3
+ export const NotificationQuery =
4
+ gql(`query Notifications($input: NotificationInput!) {
5
+ notifications(input: $input) {
6
+ message
7
+ status
8
+ data {
9
+ id
10
+ type
11
+ receiverId
12
+ projectId
13
+ projectSlug
14
+ content
15
+ viewed
16
+ createdAt
17
+ sender {
18
+ avatar
19
+ username
20
+ }
21
+ }
22
+ }
23
+ }`);
24
+
25
+ export const ViewNotificationMutation =
26
+ gql(`mutation ViewNotification($input: ViewNotificationInput!) {
27
+ viewNotification(input: $input) {
28
+ message
29
+ status
30
+ }
31
+ }`);
graphql/definitions/pageView.definition.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { gql } from "@apollo/client";
2
+
3
+ export const PageViewQuery = gql(`query pageViews {
4
+ pageViews {
5
+ data {
6
+ id
7
+ ip
8
+ createdAt
9
+ updatedAt
10
+ }
11
+ message
12
+ status
13
+ }
14
+ }`);
graphql/definitions/project.definition.ts ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { gql } from "@apollo/client";
2
+
3
+ export const ProjectsQuery = gql(`query Projects($input: ProjectsInput!) {
4
+ projects(input: $input) {
5
+ data {
6
+ id
7
+ createdAt
8
+ title
9
+ slug
10
+ description
11
+ content
12
+ updatedAt
13
+ hasvoted
14
+ isProjectOwner
15
+ files
16
+ tags {
17
+ id
18
+ value
19
+ }
20
+ _count {
21
+ votes
22
+ comments
23
+ }
24
+ author {
25
+ id
26
+ username
27
+ avatar
28
+ }
29
+ comments {
30
+ author {
31
+ id
32
+ username
33
+ avatar
34
+ }
35
+ }
36
+ }
37
+ message
38
+ status
39
+ }
40
+ }`);
41
+
42
+ export const ProjectsIDsQuery = gql(`query Projects($input: ProjectsInput!) {
43
+ projects(input: $input) {
44
+ data {
45
+ id
46
+ }
47
+ message
48
+ status
49
+ }
50
+ }`);
51
+
52
+ export const ProjectQuery = gql(`query Project($input: ProjectInput!) {
53
+ project(input: $input) {
54
+ message
55
+ status
56
+ data {
57
+ id
58
+ title
59
+ slug
60
+ description
61
+ content
62
+ files
63
+ author {
64
+ id
65
+ username
66
+ avatar
67
+ }
68
+ tags {
69
+ id
70
+ value
71
+ }
72
+ _count {
73
+ votes
74
+ comments
75
+ }
76
+ createdAt
77
+ updatedAt
78
+ hasvoted
79
+ isProjectOwner
80
+ votes {
81
+ author {
82
+ id
83
+ username
84
+ avatar
85
+ }
86
+ }
87
+ }
88
+ }
89
+ usersWhoComment(input: $input) {
90
+ data {
91
+ id
92
+ email
93
+ username
94
+ avatar
95
+ }
96
+ message
97
+ status
98
+ }
99
+ }`);
100
+
101
+ export const HasVotedProjectQuery =
102
+ gql(`query HasVotedProjectQuery($input: ProjectInput!) {
103
+ project(input: $input) {
104
+ data {
105
+ hasvoted
106
+ _count {
107
+ votes
108
+ }
109
+ }
110
+ message
111
+ status
112
+ }
113
+ }`);
114
+
115
+ export const CreateProjectMutation =
116
+ gql(`mutation CreateProject($input: CreateProjectInput!) {
117
+ createProject(input: $input) {
118
+ message
119
+ status
120
+ data {
121
+ id
122
+ title
123
+ slug
124
+ description
125
+ content
126
+ files
127
+ }
128
+ }
129
+ }`);
130
+
131
+ export const UpdateProjectMutation =
132
+ gql(`mutation UpdateProject($input: UpdateProjectInput!) {
133
+ updateProject(input: $input) {
134
+ message
135
+ status
136
+ }
137
+ }`);
138
+
139
+ export const DeleteProjectMutation =
140
+ gql(`mutation DeleteProject($input: DeleteProjectInput!) {
141
+ deleteProject(input: $input) {
142
+ data {
143
+ id
144
+ }
145
+ message
146
+ status
147
+ }
148
+ }`);
graphql/definitions/tag.definition.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { gql } from "@apollo/client";
2
+
3
+ export const TagsQuery = gql(`query Tags {
4
+ tags {
5
+ data {
6
+ id
7
+ value
8
+ }
9
+ message
10
+ status
11
+ }
12
+ }`);
graphql/definitions/upload.definition.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { gql } from "@apollo/client";
2
+
3
+ export const uploadFileMutation =
4
+ gql(`mutation UploadFile($input: UploadInput!) {
5
+ uploadFile(input: $input) {
6
+ url
7
+ message
8
+ status
9
+ }
10
+ }`);
graphql/definitions/user.definitions.ts ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { gql } from "@apollo/client";
2
+
3
+ export const UsersIDsQuery = gql(`query UsersIDsQuery($input: UsersInput!) {
4
+ users(input: $input) {
5
+ message
6
+ status
7
+ data {
8
+ username
9
+ }
10
+ }
11
+ }`);
12
+
13
+ export const UserQuery = gql(`query User($input: UserInput!) {
14
+ user(input: $input) {
15
+ message
16
+ status
17
+ data {
18
+ id
19
+ email
20
+ username
21
+ firstname
22
+ lastname
23
+ avatar
24
+ bio
25
+ website
26
+ jobTitle
27
+ country
28
+ showProfession
29
+ points
30
+ projects {
31
+ id
32
+ title
33
+ slug
34
+ tags {
35
+ id
36
+ value
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }`);
42
+
43
+ export const MostActiveUsersQuery = gql(`query MostActiveUsers {
44
+ mostActiveUsers {
45
+ status
46
+ message
47
+ data {
48
+ id
49
+ avatar
50
+ username
51
+ bio
52
+ }
53
+ }
54
+ }`);
graphql/generated/graphql.d.ts ADDED
@@ -0,0 +1,563 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { gql } from '@apollo/client';
2
+ export type Maybe<T> = T | null;
3
+ export type InputMaybe<T> = Maybe<T>;
4
+ export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
5
+ export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
6
+ export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
7
+ /** All built-in and custom scalars, mapped to their actual values */
8
+ export type Scalars = {
9
+ ID: string;
10
+ String: string;
11
+ Boolean: boolean;
12
+ Int: number;
13
+ Float: number;
14
+ DateTime: any;
15
+ JSON: any;
16
+ Upload: any;
17
+ };
18
+
19
+ export type Account = {
20
+ __typename?: 'Account';
21
+ createdAt: Scalars['DateTime'];
22
+ email: Scalars['String'];
23
+ id: Scalars['String'];
24
+ token: Scalars['String'];
25
+ updatedAt: Scalars['DateTime'];
26
+ };
27
+
28
+ export type AuthResponse = Response & {
29
+ __typename?: 'AuthResponse';
30
+ data?: Maybe<User>;
31
+ exp?: Maybe<Scalars['DateTime']>;
32
+ message?: Maybe<Scalars['String']>;
33
+ status: Scalars['Boolean'];
34
+ token?: Maybe<Scalars['String']>;
35
+ };
36
+
37
+ export type Comment = {
38
+ __typename?: 'Comment';
39
+ author?: Maybe<User>;
40
+ authorId: Scalars['String'];
41
+ createdAt?: Maybe<Scalars['DateTime']>;
42
+ id: Scalars['Int'];
43
+ message?: Maybe<Scalars['String']>;
44
+ parentId?: Maybe<Scalars['Int']>;
45
+ project?: Maybe<Project>;
46
+ projectId?: Maybe<Scalars['String']>;
47
+ updatedAt?: Maybe<Scalars['DateTime']>;
48
+ };
49
+
50
+ export type CommentResponse = Response & {
51
+ __typename?: 'CommentResponse';
52
+ data?: Maybe<Comment>;
53
+ message?: Maybe<Scalars['String']>;
54
+ status: Scalars['Boolean'];
55
+ };
56
+
57
+ export type CommentSubscriptionResponse = {
58
+ __typename?: 'CommentSubscriptionResponse';
59
+ data?: Maybe<Notification>;
60
+ message?: Maybe<Scalars['String']>;
61
+ type?: Maybe<PushNotificationType>;
62
+ };
63
+
64
+ export type CommentsInput = {
65
+ projectId: Scalars['String'];
66
+ };
67
+
68
+ export type CommentsResponse = Response & {
69
+ __typename?: 'CommentsResponse';
70
+ data?: Maybe<Array<Maybe<Comment>>>;
71
+ message?: Maybe<Scalars['String']>;
72
+ status: Scalars['Boolean'];
73
+ };
74
+
75
+ export type CreateCommentInput = {
76
+ mentions?: InputMaybe<Array<InputMaybe<MentionsInput>>>;
77
+ message?: InputMaybe<Scalars['String']>;
78
+ parentId?: InputMaybe<Scalars['Int']>;
79
+ projectId: Scalars['String'];
80
+ };
81
+
82
+ export type CreatePageViewInput = {
83
+ ip: Scalars['String'];
84
+ };
85
+
86
+ export type CreateProjectInput = {
87
+ content?: InputMaybe<Scalars['JSON']>;
88
+ description?: InputMaybe<Scalars['String']>;
89
+ file?: InputMaybe<Scalars['Upload']>;
90
+ tags?: InputMaybe<Array<InputMaybe<TagInput>>>;
91
+ title?: InputMaybe<Scalars['String']>;
92
+ };
93
+
94
+ export type DeleteCommentInput = {
95
+ commentId: Scalars['Int'];
96
+ };
97
+
98
+ export type DeleteProjectInput = {
99
+ projectId: Scalars['String'];
100
+ };
101
+
102
+ export type GithubAuthInput = {
103
+ code?: InputMaybe<Scalars['String']>;
104
+ };
105
+
106
+ export type GoogleAuthInput = {
107
+ authuser?: InputMaybe<Scalars['String']>;
108
+ code?: InputMaybe<Scalars['String']>;
109
+ prompt?: InputMaybe<Scalars['String']>;
110
+ scope?: InputMaybe<Scalars['String']>;
111
+ };
112
+
113
+ export type MentionsInput = {
114
+ email?: InputMaybe<Scalars['String']>;
115
+ userId?: InputMaybe<Scalars['String']>;
116
+ };
117
+
118
+ export type Mutation = {
119
+ __typename?: 'Mutation';
120
+ createComment: CommentResponse;
121
+ createProject: ProjectResponse;
122
+ deleteComment: CommentResponse;
123
+ deleteProject: ProjectResponse;
124
+ forgotPassword: SimpleAuthResponse;
125
+ githubOauth: AuthResponse;
126
+ googleOauth: AuthResponse;
127
+ login: AuthResponse;
128
+ register: AuthResponse;
129
+ resetPassword: ResetPasswordResponse;
130
+ toggleVote: VoteResponse;
131
+ updateComment: CommentResponse;
132
+ updateProfile: UserResponse;
133
+ updateProject: ProjectResponse;
134
+ uploadFile: UploadResponse;
135
+ verifyUser: AuthResponse;
136
+ viewNotification?: Maybe<Response>;
137
+ };
138
+
139
+
140
+ export type MutationCreateCommentArgs = {
141
+ input: CreateCommentInput;
142
+ };
143
+
144
+
145
+ export type MutationCreateProjectArgs = {
146
+ input: CreateProjectInput;
147
+ };
148
+
149
+
150
+ export type MutationDeleteCommentArgs = {
151
+ input: DeleteCommentInput;
152
+ };
153
+
154
+
155
+ export type MutationDeleteProjectArgs = {
156
+ input: DeleteProjectInput;
157
+ };
158
+
159
+
160
+ export type MutationForgotPasswordArgs = {
161
+ input: UserForgotPasswordInput;
162
+ };
163
+
164
+
165
+ export type MutationGithubOauthArgs = {
166
+ input: GithubAuthInput;
167
+ };
168
+
169
+
170
+ export type MutationGoogleOauthArgs = {
171
+ input: GoogleAuthInput;
172
+ };
173
+
174
+
175
+ export type MutationLoginArgs = {
176
+ input: UserLoginInput;
177
+ };
178
+
179
+
180
+ export type MutationRegisterArgs = {
181
+ input: UserRegisterInput;
182
+ };
183
+
184
+
185
+ export type MutationResetPasswordArgs = {
186
+ input: UserResetPasswordInput;
187
+ };
188
+
189
+
190
+ export type MutationToggleVoteArgs = {
191
+ input?: InputMaybe<ToggleVoteInput>;
192
+ };
193
+
194
+
195
+ export type MutationUpdateCommentArgs = {
196
+ input: UpdateCommentInput;
197
+ };
198
+
199
+
200
+ export type MutationUpdateProfileArgs = {
201
+ input?: InputMaybe<UpdateProfileInput>;
202
+ };
203
+
204
+
205
+ export type MutationUpdateProjectArgs = {
206
+ input: UpdateProjectInput;
207
+ };
208
+
209
+
210
+ export type MutationUploadFileArgs = {
211
+ input: UploadInput;
212
+ };
213
+
214
+
215
+ export type MutationVerifyUserArgs = {
216
+ input: UserVerifyInput;
217
+ };
218
+
219
+
220
+ export type MutationViewNotificationArgs = {
221
+ input: ViewNotificationInput;
222
+ };
223
+
224
+ export type Notification = {
225
+ __typename?: 'Notification';
226
+ content?: Maybe<Scalars['String']>;
227
+ createdAt?: Maybe<Scalars['DateTime']>;
228
+ id: Scalars['Int'];
229
+ projectId?: Maybe<Scalars['String']>;
230
+ projectSlug?: Maybe<Scalars['String']>;
231
+ receiverId?: Maybe<Scalars['String']>;
232
+ sender?: Maybe<User>;
233
+ senderId?: Maybe<Scalars['String']>;
234
+ type?: Maybe<Scalars['String']>;
235
+ updatedAt?: Maybe<Scalars['DateTime']>;
236
+ viewed?: Maybe<Scalars['Boolean']>;
237
+ };
238
+
239
+ export type NotificationInput = {
240
+ limit?: InputMaybe<Scalars['Int']>;
241
+ offset?: InputMaybe<Scalars['Int']>;
242
+ userId?: InputMaybe<Scalars['String']>;
243
+ };
244
+
245
+ export type NotificationResponse = Response & {
246
+ __typename?: 'NotificationResponse';
247
+ data?: Maybe<Notification>;
248
+ message?: Maybe<Scalars['String']>;
249
+ status: Scalars['Boolean'];
250
+ };
251
+
252
+ export type NotificationsResponse = Response & {
253
+ __typename?: 'NotificationsResponse';
254
+ data?: Maybe<Array<Maybe<Notification>>>;
255
+ message?: Maybe<Scalars['String']>;
256
+ status: Scalars['Boolean'];
257
+ };
258
+
259
+ export type PageView = {
260
+ __typename?: 'PageView';
261
+ createdAt?: Maybe<Scalars['DateTime']>;
262
+ id: Scalars['Int'];
263
+ ip: Scalars['String'];
264
+ updatedAt?: Maybe<Scalars['DateTime']>;
265
+ };
266
+
267
+ export type PageViewResponse = {
268
+ __typename?: 'PageViewResponse';
269
+ data?: Maybe<Array<Maybe<PageView>>>;
270
+ message?: Maybe<Scalars['String']>;
271
+ status: Scalars['Boolean'];
272
+ };
273
+
274
+ export type Project = {
275
+ __typename?: 'Project';
276
+ _count?: Maybe<ProjectCountPayload>;
277
+ author?: Maybe<User>;
278
+ comments?: Maybe<Array<Maybe<Comment>>>;
279
+ content?: Maybe<Scalars['JSON']>;
280
+ createdAt?: Maybe<Scalars['DateTime']>;
281
+ description?: Maybe<Scalars['String']>;
282
+ files?: Maybe<Array<Maybe<Scalars['String']>>>;
283
+ hasvoted?: Maybe<Scalars['Boolean']>;
284
+ id: Scalars['String'];
285
+ isProjectOwner?: Maybe<Scalars['Boolean']>;
286
+ rank?: Maybe<Scalars['Int']>;
287
+ slug?: Maybe<Scalars['String']>;
288
+ tags?: Maybe<Array<Maybe<Tag>>>;
289
+ title?: Maybe<Scalars['String']>;
290
+ updatedAt?: Maybe<Scalars['DateTime']>;
291
+ votes?: Maybe<Array<Maybe<Vote>>>;
292
+ };
293
+
294
+ export type ProjectCountPayload = {
295
+ __typename?: 'ProjectCountPayload';
296
+ comments?: Maybe<Scalars['Int']>;
297
+ votes?: Maybe<Scalars['Int']>;
298
+ };
299
+
300
+ export type ProjectInput = {
301
+ id: Scalars['String'];
302
+ vote_limit?: InputMaybe<Scalars['Int']>;
303
+ vote_offset?: InputMaybe<Scalars['Int']>;
304
+ };
305
+
306
+ export type ProjectResponse = Response & {
307
+ __typename?: 'ProjectResponse';
308
+ data?: Maybe<Project>;
309
+ message?: Maybe<Scalars['String']>;
310
+ status: Scalars['Boolean'];
311
+ };
312
+
313
+ export type ProjectsInput = {
314
+ limit?: InputMaybe<Scalars['Int']>;
315
+ offset?: InputMaybe<Scalars['Int']>;
316
+ };
317
+
318
+ export type ProjectsResponse = Response & {
319
+ __typename?: 'ProjectsResponse';
320
+ data: Array<Project>;
321
+ message?: Maybe<Scalars['String']>;
322
+ status: Scalars['Boolean'];
323
+ };
324
+
325
+ export enum PushNotificationType {
326
+ Comment = 'COMMENT',
327
+ Mention = 'MENTION',
328
+ Reply = 'REPLY',
329
+ Upvote = 'UPVOTE'
330
+ }
331
+
332
+ export type Query = {
333
+ __typename?: 'Query';
334
+ comments?: Maybe<CommentsResponse>;
335
+ me?: Maybe<UserResponse>;
336
+ mostActiveUsers?: Maybe<UsersResponse>;
337
+ notifications?: Maybe<NotificationsResponse>;
338
+ pageViews?: Maybe<PageViewResponse>;
339
+ project?: Maybe<ProjectResponse>;
340
+ projects?: Maybe<ProjectsResponse>;
341
+ tags?: Maybe<TagsResponse>;
342
+ user?: Maybe<UserResponse>;
343
+ users?: Maybe<UsersResponse>;
344
+ usersWhoComment?: Maybe<UsersResponse>;
345
+ };
346
+
347
+
348
+ export type QueryCommentsArgs = {
349
+ input: CommentsInput;
350
+ };
351
+
352
+
353
+ export type QueryNotificationsArgs = {
354
+ input: NotificationInput;
355
+ };
356
+
357
+
358
+ export type QueryProjectArgs = {
359
+ input: ProjectInput;
360
+ };
361
+
362
+
363
+ export type QueryProjectsArgs = {
364
+ input: ProjectsInput;
365
+ };
366
+
367
+
368
+ export type QueryUserArgs = {
369
+ input: UserInput;
370
+ };
371
+
372
+
373
+ export type QueryUsersArgs = {
374
+ input: UsersInput;
375
+ };
376
+
377
+
378
+ export type QueryUsersWhoCommentArgs = {
379
+ input: ProjectInput;
380
+ };
381
+
382
+ export type ResetPasswordResponse = Response & {
383
+ __typename?: 'ResetPasswordResponse';
384
+ data?: Maybe<Account>;
385
+ message?: Maybe<Scalars['String']>;
386
+ status: Scalars['Boolean'];
387
+ };
388
+
389
+ export type Response = {
390
+ message?: Maybe<Scalars['String']>;
391
+ status: Scalars['Boolean'];
392
+ };
393
+
394
+ export type SimpleAuthResponse = Response & {
395
+ __typename?: 'SimpleAuthResponse';
396
+ message?: Maybe<Scalars['String']>;
397
+ status: Scalars['Boolean'];
398
+ };
399
+
400
+ export type Subscription = {
401
+ __typename?: 'Subscription';
402
+ commentCreated?: Maybe<CommentSubscriptionResponse>;
403
+ mentionCreated?: Maybe<CommentSubscriptionResponse>;
404
+ replyCreated?: Maybe<CommentSubscriptionResponse>;
405
+ upvoteCreated?: Maybe<UpvoteSubscriptionResponse>;
406
+ };
407
+
408
+ export type Tag = {
409
+ __typename?: 'Tag';
410
+ id: Scalars['Int'];
411
+ projects?: Maybe<Array<Maybe<Project>>>;
412
+ value: Scalars['String'];
413
+ };
414
+
415
+ export type TagInput = {
416
+ id?: InputMaybe<Scalars['Int']>;
417
+ value: Scalars['String'];
418
+ };
419
+
420
+ export type TagsResponse = Response & {
421
+ __typename?: 'TagsResponse';
422
+ data?: Maybe<Array<Tag>>;
423
+ message?: Maybe<Scalars['String']>;
424
+ status: Scalars['Boolean'];
425
+ };
426
+
427
+ export type ToggleVoteInput = {
428
+ id: Scalars['String'];
429
+ };
430
+
431
+ export type UpdateCommentInput = {
432
+ commentId: Scalars['Int'];
433
+ message?: InputMaybe<Scalars['String']>;
434
+ };
435
+
436
+ export type UpdateProfileInput = {
437
+ banner?: InputMaybe<Scalars['String']>;
438
+ bio?: InputMaybe<Scalars['String']>;
439
+ country?: InputMaybe<Scalars['String']>;
440
+ file?: InputMaybe<Scalars['Upload']>;
441
+ firstname?: InputMaybe<Scalars['String']>;
442
+ id: Scalars['String'];
443
+ jobTitle?: InputMaybe<Scalars['String']>;
444
+ lastname?: InputMaybe<Scalars['String']>;
445
+ showProfession?: InputMaybe<Scalars['Boolean']>;
446
+ username?: InputMaybe<Scalars['String']>;
447
+ website?: InputMaybe<Scalars['String']>;
448
+ };
449
+
450
+ export type UpdateProjectInput = {
451
+ content?: InputMaybe<Scalars['JSON']>;
452
+ description?: InputMaybe<Scalars['String']>;
453
+ file?: InputMaybe<Scalars['Upload']>;
454
+ projectId: Scalars['String'];
455
+ tags?: InputMaybe<Array<InputMaybe<TagInput>>>;
456
+ title?: InputMaybe<Scalars['String']>;
457
+ };
458
+
459
+ export type UploadInput = {
460
+ file?: InputMaybe<Scalars['Upload']>;
461
+ };
462
+
463
+ export type UploadResponse = Response & {
464
+ __typename?: 'UploadResponse';
465
+ message?: Maybe<Scalars['String']>;
466
+ status: Scalars['Boolean'];
467
+ url?: Maybe<Scalars['String']>;
468
+ };
469
+
470
+ export type UpvoteSubscriptionResponse = {
471
+ __typename?: 'UpvoteSubscriptionResponse';
472
+ data?: Maybe<Notification>;
473
+ message?: Maybe<Scalars['String']>;
474
+ type?: Maybe<PushNotificationType>;
475
+ };
476
+
477
+ export type User = {
478
+ __typename?: 'User';
479
+ avatar?: Maybe<Scalars['String']>;
480
+ banner?: Maybe<Scalars['String']>;
481
+ bio?: Maybe<Scalars['String']>;
482
+ country?: Maybe<Scalars['String']>;
483
+ createdAt?: Maybe<Scalars['DateTime']>;
484
+ email: Scalars['String'];
485
+ firstname?: Maybe<Scalars['String']>;
486
+ id: Scalars['String'];
487
+ jobTitle?: Maybe<Scalars['String']>;
488
+ lastname?: Maybe<Scalars['String']>;
489
+ projects?: Maybe<Array<Maybe<Project>>>;
490
+ showProfession?: Maybe<Scalars['Boolean']>;
491
+ updatedAt?: Maybe<Scalars['DateTime']>;
492
+ username: Scalars['String'];
493
+ verifiedAt?: Maybe<Scalars['DateTime']>;
494
+ votes?: Maybe<Array<Maybe<Vote>>>;
495
+ website?: Maybe<Scalars['String']>;
496
+ };
497
+
498
+ export type UserForgotPasswordInput = {
499
+ email: Scalars['String'];
500
+ };
501
+
502
+ export type UserInput = {
503
+ username: Scalars['String'];
504
+ };
505
+
506
+ export type UserLoginInput = {
507
+ email: Scalars['String'];
508
+ password: Scalars['String'];
509
+ };
510
+
511
+ export type UserRegisterInput = {
512
+ email: Scalars['String'];
513
+ password: Scalars['String'];
514
+ username: Scalars['String'];
515
+ };
516
+
517
+ export type UserResetPasswordInput = {
518
+ password: Scalars['String'];
519
+ token: Scalars['String'];
520
+ };
521
+
522
+ export type UserResponse = Response & {
523
+ __typename?: 'UserResponse';
524
+ data?: Maybe<User>;
525
+ message?: Maybe<Scalars['String']>;
526
+ status: Scalars['Boolean'];
527
+ };
528
+
529
+ export type UserVerifyInput = {
530
+ token: Scalars['String'];
531
+ };
532
+
533
+ export type UsersInput = {
534
+ limit?: InputMaybe<Scalars['Int']>;
535
+ offset?: InputMaybe<Scalars['Int']>;
536
+ };
537
+
538
+ export type UsersResponse = Response & {
539
+ __typename?: 'UsersResponse';
540
+ data?: Maybe<Array<Maybe<User>>>;
541
+ message?: Maybe<Scalars['String']>;
542
+ status: Scalars['Boolean'];
543
+ };
544
+
545
+ export type ViewNotificationInput = {
546
+ notificationId: Scalars['Int'];
547
+ };
548
+
549
+ export type Vote = {
550
+ __typename?: 'Vote';
551
+ author?: Maybe<User>;
552
+ createdAt?: Maybe<Scalars['DateTime']>;
553
+ id: Scalars['Int'];
554
+ project?: Maybe<Project>;
555
+ updatedAt?: Maybe<Scalars['DateTime']>;
556
+ };
557
+
558
+ export type VoteResponse = Response & {
559
+ __typename?: 'VoteResponse';
560
+ data?: Maybe<Vote>;
561
+ message?: Maybe<Scalars['String']>;
562
+ status: Scalars['Boolean'];
563
+ };
hooks/index.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ export * from "./useMonaco";
2
+ export * from "./useTree";
hooks/useMonaco.ts ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useRef, useState } from "react";
2
+ import { IKeyboardEvent } from "monaco-editor";
3
+ import { useDebounce } from "use-debounce";
4
+ import { OnChange, OnMount } from "@monaco-editor/react";
5
+ import parserHtml from "prettier/parser-html";
6
+ import parserCss from "prettier/parser-postcss";
7
+ import parserBabel from "prettier/parser-babel";
8
+ import prettier from "prettier";
9
+ import { useAppSelector } from "../store/hook";
10
+ import { theme_state } from "../store/features/themeSlice";
11
+
12
+ export const useMonaco = () => {
13
+ const { theme } = useAppSelector(theme_state);
14
+ const codeEditor = useRef<any>();
15
+
16
+ const [input, setInput] = useState<string | undefined>("");
17
+ const [code] = useDebounce(input, 1000);
18
+
19
+ const onChange: OnChange = (value) => {
20
+ setInput(value);
21
+ };
22
+
23
+ const onMount: OnMount = async (monacoEditor, monaco) => {
24
+ codeEditor.current = monacoEditor;
25
+
26
+ monaco.editor.defineTheme("myTheme", {
27
+ base: "vs-dark",
28
+ inherit: true,
29
+ rules: [{ background: theme.background, token: "" }],
30
+ colors: {
31
+ "editor.background": theme.foreground,
32
+ },
33
+ });
34
+
35
+ monaco.editor.setTheme("myTheme");
36
+
37
+ const { default: traverse } = await import("@babel/traverse");
38
+ const { parse } = await import("@babel/parser");
39
+ const { default: MonacoJSXHighlighter } = await import(
40
+ "monaco-jsx-highlighter"
41
+ );
42
+
43
+ //jsx syntax highlight
44
+ const babelParse = (code: any) =>
45
+ parse(code, { sourceType: "module", plugins: ["jsx"] });
46
+
47
+ const monacoJSXHighlighter = new MonacoJSXHighlighter(
48
+ //@ts-ignore
49
+ monaco,
50
+ babelParse,
51
+ traverse,
52
+ monacoEditor
53
+ );
54
+
55
+ monacoJSXHighlighter.highLightOnDidChangeModelContent(
56
+ 0,
57
+ () => {},
58
+ () => {},
59
+ undefined,
60
+ () => {}
61
+ );
62
+
63
+ //format code
64
+ function formatOnSave() {
65
+ const unformattedCode = codeEditor.current.getModel().getValue();
66
+ const lang = codeEditor.current.getModel()._languageIdentifier.language;
67
+
68
+ let config;
69
+
70
+ switch (lang) {
71
+ case "html":
72
+ config = { parser: "html", plugin: [parserHtml] };
73
+ break;
74
+
75
+ case "css":
76
+ config = { parser: "css", plugin: [parserCss] };
77
+ break;
78
+
79
+ case "javascript":
80
+ config = { parser: "babel", plugin: [parserBabel] };
81
+ break;
82
+
83
+ default:
84
+ break;
85
+ }
86
+
87
+ const formattedCode = prettier.format(unformattedCode, {
88
+ parser: config && config.parser,
89
+ plugins: config && config.plugin,
90
+ useTabs: false,
91
+ semi: true,
92
+ });
93
+
94
+ codeEditor.current.setValue(formattedCode);
95
+ }
96
+
97
+ //save command
98
+ let handleOnKeyDown = codeEditor.current.onKeyDown(
99
+ (event: IKeyboardEvent) => {
100
+ if (
101
+ (window.navigator.platform.match("Mac")
102
+ ? event.metaKey
103
+ : event.ctrlKey) &&
104
+ event.code === "KeyS"
105
+ ) {
106
+ event.preventDefault();
107
+ formatOnSave();
108
+ }
109
+ }
110
+ );
111
+
112
+ //cleaning up
113
+ return () => handleOnKeyDown.dispose();
114
+ };
115
+
116
+ return { onMount, onChange, code };
117
+ };
hooks/useOutsideRef.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react";
2
+
3
+ const UseOutsideRef = (ref: any) => {
4
+ const [isOutsideRef, setIsOutsideRef] = useState(false);
5
+
6
+ useEffect(() => {
7
+ function handleClickOutside(event: any) {
8
+ if (ref.current && !ref.current.contains(event.target)) {
9
+ setIsOutsideRef(true);
10
+
11
+ setTimeout(() => {
12
+ setIsOutsideRef(false);
13
+ }, 200);
14
+ }
15
+ }
16
+
17
+ document.addEventListener("mousedown", handleClickOutside);
18
+ return () => {
19
+ document.removeEventListener("mousedown", handleClickOutside);
20
+ };
21
+ }, [ref]);
22
+
23
+ return { isOutsideRef };
24
+ };
25
+
26
+ export default UseOutsideRef;