├── public
├── robots.txt
├── images
│ └── logo.webp
├── icons
│ ├── favicon.ico
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── apple-touch-icon.png
│ ├── icon512_maskable.png
│ ├── icon512_rounded.png
│ ├── android-chrome-192x192.png
│ └── android-chrome-512x512.png
└── site.webmanifest
├── src
├── vite-env.d.ts
├── lib
│ ├── utils.ts
│ └── url.ts
├── main.tsx
├── components
│ ├── loader.tsx
│ ├── ui
│ │ ├── separator.tsx
│ │ ├── input.tsx
│ │ ├── resizable.tsx
│ │ ├── button.tsx
│ │ └── drawer.tsx
│ ├── editor
│ │ ├── editor.tsx
│ │ ├── stats.tsx
│ │ └── terminal.tsx
│ ├── nav-buttons.tsx
│ └── settings.tsx
├── types.ts
├── globals.css
├── App.tsx
└── store
│ └── useStore.ts
├── bun.lockb
├── postcss.config.js
├── tsconfig.json
├── .gitignore
├── prettier.config.js
├── tsconfig.node.json
├── components.json
├── .github
└── workflows
│ ├── lint.yml
│ ├── format.yml
│ └── codeql.yml
├── .eslintrc.cjs
├── tsconfig.app.json
├── vite.config.ts
├── LICENSE
├── package.json
├── tailwind.config.js
├── README.md
└── index.html
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vwh/python-playground/HEAD/bun.lockb
--------------------------------------------------------------------------------
/public/images/logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vwh/python-playground/HEAD/public/images/logo.webp
--------------------------------------------------------------------------------
/public/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vwh/python-playground/HEAD/public/icons/favicon.ico
--------------------------------------------------------------------------------
/public/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vwh/python-playground/HEAD/public/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/public/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vwh/python-playground/HEAD/public/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vwh/python-playground/HEAD/public/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/icons/icon512_maskable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vwh/python-playground/HEAD/public/icons/icon512_maskable.png
--------------------------------------------------------------------------------
/public/icons/icon512_rounded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vwh/python-playground/HEAD/public/icons/icon512_rounded.png
--------------------------------------------------------------------------------
/public/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vwh/python-playground/HEAD/public/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vwh/python-playground/HEAD/public/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/url.ts:
--------------------------------------------------------------------------------
1 | export const getDecodedParam = (param: string | null): string | null => {
2 | if (param) {
3 | try {
4 | return atob(param);
5 | } catch (e) {
6 | console.error("Failed to decode parameter:", e);
7 | return null;
8 | }
9 | }
10 | return null;
11 | };
12 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import "@/globals.css";
2 |
3 | import React from "react";
4 | import ReactDOM from "react-dom/client";
5 |
6 | import App from "@/App.tsx";
7 |
8 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
9 |
10 |
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | {
5 | "path": "./tsconfig.app.json"
6 | },
7 | {
8 | "path": "./tsconfig.node.json"
9 | }
10 | ],
11 | "compilerOptions": {
12 | "baseUrl": ".",
13 | "paths": {
14 | "@/*": ["./src/*"]
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */
2 | const config = {
3 | plugins: ["prettier-plugin-tailwindcss"],
4 | arrowParens: "always",
5 | printWidth: 80,
6 | singleQuote: false,
7 | jsxSingleQuote: false,
8 | semi: true,
9 | trailingComma: "none",
10 | tabWidth: 2,
11 | };
12 |
13 | export default config;
14 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
5 | "skipLibCheck": true,
6 | "module": "ESNext",
7 | "moduleResolution": "bundler",
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "noEmit": true
11 | },
12 | "include": ["vite.config.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/loader.tsx:
--------------------------------------------------------------------------------
1 | import { LoaderCircleIcon } from "lucide-react";
2 |
3 | interface LoaderProps {
4 | text: string;
5 | }
6 |
7 | export default function Loader({ text }: LoaderProps) {
8 | return (
9 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface PyodideInterface {
2 | runPythonAsync: (code: string) => Promise;
3 | loadPackage: (name: string) => Promise;
4 | globals: {
5 | set: (key: string, value: any) => void;
6 | };
7 | pyimport: (name: string) => Promise;
8 | }
9 |
10 | declare global {
11 | interface Window {
12 | loadPyodide: () => Promise;
13 | micropip: {
14 | install: (
15 | packages: string | string[],
16 | keep_going?: boolean
17 | ) => Promise;
18 | };
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | lint:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout repository
17 | uses: actions/checkout@v3
18 |
19 | - name: Set up Node.js
20 | uses: actions/setup-node@v3
21 | with:
22 | node-version: "18"
23 |
24 | - name: Install dependencies
25 | run: npm install
26 |
27 | - name: Run ESLint
28 | run: npm run lint
29 |
--------------------------------------------------------------------------------
/.github/workflows/format.yml:
--------------------------------------------------------------------------------
1 | name: Check Format
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | format:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout repository
17 | uses: actions/checkout@v3
18 |
19 | - name: Set up Node.js
20 | uses: actions/setup-node@v3
21 | with:
22 | node-version: "16"
23 |
24 | - name: Install dependencies
25 | run: npm install
26 |
27 | - name: Run Prettier check
28 | run: npm run format:check
29 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import("eslint").Linter.Config} */
2 | module.exports = {
3 | root: true,
4 | env: { browser: true, es2020: true },
5 | extends: [
6 | "eslint:recommended",
7 | "plugin:@typescript-eslint/recommended",
8 | "plugin:react-hooks/recommended"
9 | ],
10 | ignorePatterns: ["dist", ".eslintrc.cjs"],
11 | parser: "@typescript-eslint/parser",
12 | plugins: ["react-refresh"],
13 | rules: {
14 | "react-hooks/exhaustive-deps": "off",
15 | "eslint:@typescript-eslint/no-explicit-any": "off",
16 | "@typescript-eslint/no-explicit-any": "off",
17 | "react-refresh/only-export-components": [
18 | "warn",
19 | { allowConstantExport: true }
20 | ]
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "theme_color": "#1e1e1e",
3 | "background_color": "#141110",
4 | "icons": [
5 | {
6 | "purpose": "maskable",
7 | "sizes": "512x512",
8 | "src": "./icons/icon512_maskable.png",
9 | "type": "image/png"
10 | },
11 | {
12 | "purpose": "any",
13 | "sizes": "512x512",
14 | "src": "./icons/icon512_rounded.png",
15 | "type": "image/png"
16 | }
17 | ],
18 | "orientation": "any",
19 | "display": "standalone",
20 | "dir": "left",
21 | "lang": "en-US",
22 | "start_url": "/python-playground/",
23 | "scope": "/python-playground/",
24 | "description": "Explore and experiment with Python code",
25 | "id": "vwh-python-playground",
26 | "name": "Python Playground",
27 | "short_name": "Python"
28 | }
29 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
5 | "target": "ES2020",
6 | "useDefineForClassFields": true,
7 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
8 | "module": "ESNext",
9 | "skipLibCheck": true,
10 |
11 | /* Bundler mode */
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "moduleDetection": "force",
17 | "noEmit": true,
18 | "jsx": "react-jsx",
19 |
20 | /* Linting */
21 | "strict": true,
22 | "noUnusedLocals": true,
23 | "noUnusedParameters": true,
24 | "noFallthroughCasesInSwitch": true,
25 |
26 | "baseUrl": ".",
27 | "paths": {
28 | "@/*": ["./src/*"]
29 | }
30 | },
31 |
32 | "include": ["src"]
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as SeparatorPrimitive from "@radix-ui/react-separator";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const Separator = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(
10 | (
11 | { className, orientation = "horizontal", decorative = true, ...props },
12 | ref
13 | ) => (
14 |
25 | )
26 | );
27 | Separator.displayName = SeparatorPrimitive.Root.displayName;
28 |
29 | export { Separator };
30 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | type InputProps = React.InputHTMLAttributes;
6 |
7 | const Input = React.forwardRef(
8 | ({ className, type, ...props }, ref) => {
9 | return (
10 |
19 | );
20 | }
21 | );
22 | Input.displayName = "Input";
23 |
24 | export { Input };
25 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import react from "@vitejs/plugin-react";
3 | import { defineConfig } from "vite";
4 |
5 | import compression from "vite-plugin-compression";
6 | import dynamicImport from "vite-plugin-dynamic-import";
7 | import { VitePWA } from "vite-plugin-pwa";
8 |
9 | // https://vitejs.dev/config/
10 | export default defineConfig({
11 | base: "/python-playground/",
12 | plugins: [
13 | compression(),
14 | react(),
15 | dynamicImport(),
16 | VitePWA({ registerType: "autoUpdate" })
17 | ],
18 | build: {
19 | rollupOptions: {
20 | output: {
21 | manualChunks(id) {
22 | if (id.includes("node_modules")) {
23 | return id
24 | .toString()
25 | .split("node_modules/")[1]
26 | .split("/")[0]
27 | .toString();
28 | }
29 | }
30 | }
31 | }
32 | },
33 | resolve: {
34 | alias: {
35 | "@": path.resolve(__dirname, "./src")
36 | }
37 | }
38 | });
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 VWH
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/editor/editor.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useMemo } from "react";
2 | import { useStore } from "@/store/useStore";
3 |
4 | import MonacoEditor, { type OnMount } from "@monaco-editor/react";
5 | import Loader from "@/components/loader";
6 |
7 | export default function Editor() {
8 | const { code, setCode } = useStore();
9 |
10 | const handleCodeOnChange = useCallback(
11 | (value: string | undefined) => {
12 | if (value) {
13 | setCode(value);
14 | }
15 | },
16 | [setCode]
17 | );
18 |
19 | const editorOptions = useMemo(
20 | () => ({
21 | minimap: { enabled: false },
22 | scrollBeyondLastLine: false,
23 | fontSize: 14,
24 | fontFamily: "'Fira Code', monospace",
25 | fontLigatures: true,
26 | cursorSmoothCaretAnimation: "on",
27 | smoothScrolling: true,
28 | padding: { top: 16, bottom: 16 },
29 | renderLineHighlight: "all",
30 | matchBrackets: "always",
31 | autoClosingBrackets: "always",
32 | autoClosingQuotes: "always",
33 | formatOnPaste: true,
34 | formatOnType: true
35 | }),
36 | []
37 | );
38 |
39 | const handleEditorDidMount: OnMount = useCallback((editor) => {
40 | editor.focus();
41 | }, []);
42 |
43 | return (
44 |
45 | }
51 | // @ts-expect-error ts(2322)
52 | options={editorOptions}
53 | onMount={handleEditorDidMount}
54 | className="h-full w-full"
55 | />
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/editor/stats.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 | import { useStore } from "@/store/useStore";
3 |
4 | import { FileTextIcon, TypeIcon, HashIcon } from "lucide-react";
5 |
6 | interface CodeStats {
7 | lines: number;
8 | words: number;
9 | characters: number;
10 | }
11 |
12 | export default function Stats() {
13 | const { code } = useStore();
14 |
15 | const stats: CodeStats = useMemo(
16 | () => ({
17 | lines: code.split("\n").length,
18 | words: code.trim().split(/\s+/).length,
19 | characters: code.length
20 | }),
21 | [code]
22 | );
23 |
24 | return (
25 |
26 |
27 | }
29 | value={stats.lines}
30 | label="Lines"
31 | />
32 | }
34 | value={stats.words}
35 | label="Words"
36 | />
37 | }
39 | value={stats.characters}
40 | label="Chars"
41 | />
42 |
43 |
44 | );
45 | }
46 |
47 | interface StatItemProps {
48 | icon: React.ReactNode;
49 | value: number;
50 | label: string;
51 | }
52 |
53 | function StatItem({ icon, value, label }: StatItemProps) {
54 | return (
55 |
56 |
57 | {icon}
58 | {value}
59 |
60 |
{label}
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | import * as ResizablePrimitive from "react-resizable-panels";
2 | import { GripVerticalIcon } from "lucide-react";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const ResizablePanelGroup = ({
7 | className,
8 | ...props
9 | }: React.ComponentProps) => (
10 |
17 | );
18 |
19 | const ResizablePanel = ResizablePrimitive.Panel;
20 |
21 | const ResizableHandle = ({
22 | withHandle,
23 | className,
24 | ...props
25 | }: React.ComponentProps & {
26 | withHandle?: boolean;
27 | }) => (
28 | div]:rotate-90",
31 | className
32 | )}
33 | {...props}
34 | >
35 | {withHandle && (
36 |
37 |
38 |
39 | )}
40 |
41 | );
42 |
43 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
44 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline"
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10"
27 | }
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default"
32 | }
33 | }
34 | );
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean;
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button";
45 | return (
46 |
51 | );
52 | }
53 | );
54 | Button.displayName = "Button";
55 |
56 | export { Button };
57 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "homepage": "https://vwh.github.io/python-playground/",
3 | "name": "python-playground",
4 | "private": true,
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "preview": "vite preview",
10 | "deploy": "gh-pages -d dist",
11 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
12 | "format": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
13 | "format:check": "prettier --check 'src/**/*.{js,jsx,ts,tsx}'"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/vwh/python-playground.git"
18 | },
19 | "author": "vwh",
20 | "license": "MIT",
21 | "dependencies": {
22 | "@monaco-editor/react": "^4.6.0",
23 | "@radix-ui/react-dialog": "^1.1.4",
24 | "@radix-ui/react-separator": "^1.1.1",
25 | "@radix-ui/react-slot": "^1.1.1",
26 | "class-variance-authority": "^0.7.1",
27 | "clsx": "^2.1.1",
28 | "error-stack-parser": "^2.1.4",
29 | "lucide-react": "^0.424.0",
30 | "react": "^18.3.1",
31 | "react-dom": "^18.3.1",
32 | "react-resizable-panels": "^2.1.7",
33 | "tailwind-merge": "^2.6.0",
34 | "tailwindcss-animate": "^1.0.7",
35 | "vaul": "^0.9.9",
36 | "zustand": "^4.5.5"
37 | },
38 | "devDependencies": {
39 | "@types/node": "^22.10.5",
40 | "@types/react": "^18.3.18",
41 | "@types/react-dom": "^18.3.5",
42 | "@typescript-eslint/eslint-plugin": "^8.19.0",
43 | "@typescript-eslint/parser": "^8.19.0",
44 | "@vitejs/plugin-react": "^4.3.4",
45 | "autoprefixer": "^10.4.20",
46 | "eslint": "^8.57.1",
47 | "eslint-plugin-react-hooks": "^4.6.2",
48 | "eslint-plugin-react-refresh": "^0.4.16",
49 | "gh-pages": "^6.3.0",
50 | "postcss": "^8.4.49",
51 | "prettier": "^3.4.2",
52 | "prettier-plugin-tailwindcss": "^0.6.9",
53 | "tailwindcss": "^3.4.17",
54 | "typescript": "^5.7.2",
55 | "vite": "^5.4.11",
56 | "vite-plugin-compression": "^0.5.1",
57 | "vite-plugin-dynamic-import": "^1.6.0",
58 | "vite-plugin-pwa": "^0.20.5"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 222.2 84% 4.9%;
8 | --foreground: 210 40% 98%;
9 | --card: 222.2 84% 4.9%;
10 | --card-foreground: 210 40% 98%;
11 | --popover: 222.2 84% 4.9%;
12 | --popover-foreground: 210 40% 98%;
13 | --primary: 210 40% 98%;
14 | --primary-foreground: 222.2 47.4% 11.2%;
15 | --secondary: 217.2 32.6% 17.5%;
16 | --secondary-foreground: 210 40% 98%;
17 | --muted: 217.2 32.6% 17.5%;
18 | --muted-foreground: 215 20.2% 65.1%;
19 | --accent: 217.2 32.6% 17.5%;
20 | --accent-foreground: 210 40% 98%;
21 | --destructive: 0 62.8% 30.6%;
22 | --destructive-foreground: 210 40% 98%;
23 | --border: 217.2 32.6% 17.5%;
24 | --input: 217.2 32.6% 17.5%;
25 | --ring: 212.7 26.8% 83.9;
26 | --radius: 0.5rem;
27 | }
28 | }
29 |
30 | @layer base {
31 | * {
32 | @apply border-border;
33 | }
34 | body {
35 | @apply bg-background text-foreground;
36 | font-feature-settings:
37 | "rlig" 1,
38 | "calt" 1;
39 | }
40 |
41 | /* For Webkit browsers like Chrome and Safari */
42 | ::-webkit-scrollbar {
43 | width: 8px;
44 | height: 8px;
45 | }
46 |
47 | ::-webkit-scrollbar-track {
48 | background: var(--scrollbar-track);
49 | border-radius: 1px;
50 | }
51 |
52 | ::-webkit-scrollbar-thumb {
53 | background: var(--scrollbar-thumb);
54 | border-radius: 1px;
55 | transition: background 0.2s ease-in-out;
56 | }
57 |
58 | ::-webkit-scrollbar-thumb:hover {
59 | background: var(--scrollbar-thumb-hover);
60 | }
61 |
62 | :root {
63 | --scrollbar-track: hsl(var(--secondary));
64 | --scrollbar-thumb: hsl(var(--muted-foreground) / 0.5);
65 | --scrollbar-thumb-hover: hsl(var(--muted-foreground) / 0.7);
66 | }
67 | }
68 |
69 | .markdown-body pre {
70 | direction: ltr;
71 | @apply rounded border bg-background !important;
72 | }
73 |
74 | .markdown-body {
75 | background: none !important;
76 | }
77 |
78 | .monaco-editor .minimap,
79 | .iPadShowKeyboard {
80 | display: none;
81 | }
82 |
83 | .monaco-editor-background {
84 | background-color: hsl(var(--background)) !important;
85 | }
86 |
87 | .margin-view-overlays {
88 | background-color: hsl(var(--background)) !important;
89 | }
90 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useStore } from "@/store/useStore";
3 |
4 | import { getDecodedParam } from "@/lib/url";
5 |
6 | import Editor from "@/components/editor/editor";
7 | import ButtonsNav from "@/components/nav-buttons";
8 | import Stats from "@/components/editor/stats";
9 | import Terminal from "@/components/editor/terminal";
10 | import {
11 | ResizableHandle,
12 | ResizablePanel,
13 | ResizablePanelGroup
14 | } from "@/components/ui/resizable";
15 |
16 | export default function App() {
17 | const { direction, setCode, initializePyodide, isPyodideLoading } =
18 | useStore();
19 |
20 | // Initialize Pyodide and ( set the code from URL params if present )
21 | useEffect(() => {
22 | const initializeApp = async () => {
23 | await initializePyodide();
24 |
25 | const urlParams = new URLSearchParams(window.location.search);
26 | const encodedParam = urlParams.get("v");
27 | if (encodedParam) {
28 | const decodedCode = getDecodedParam(encodedParam);
29 | if (decodedCode) {
30 | setCode(decodedCode);
31 | }
32 | }
33 | };
34 |
35 | initializeApp();
36 | }, [initializePyodide, setCode]);
37 |
38 | if (isPyodideLoading) {
39 | return ;
40 | }
41 |
42 | return (
43 |
44 |
45 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | );
60 | }
61 |
62 | function LoadingScreen() {
63 | return (
64 |
65 |
66 |
71 |
72 | Loading WebAssembly
73 |
74 |
75 |
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import tailwindcssAnimate from "tailwindcss-animate";
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | export default {
5 | darkMode: ["class"],
6 | content: [
7 | "./pages/**/*.{ts,tsx}",
8 | "./components/**/*.{ts,tsx}",
9 | "./app/**/*.{ts,tsx}",
10 | "./src/**/*.{ts,tsx}"
11 | ],
12 | prefix: "",
13 | theme: {
14 | container: {
15 | center: true,
16 | padding: "2rem",
17 | screens: {
18 | "2xl": "1400px"
19 | }
20 | },
21 | extend: {
22 | borderWidth: {
23 | DEFAULT: "2px"
24 | },
25 | colors: {
26 | border: "hsl(var(--border))",
27 | input: "hsl(var(--input))",
28 | ring: "hsl(var(--ring))",
29 | background: "hsl(var(--background))",
30 | foreground: "hsl(var(--foreground))",
31 | primary: {
32 | DEFAULT: "hsl(var(--primary))",
33 | foreground: "hsl(var(--primary-foreground))"
34 | },
35 | secondary: {
36 | DEFAULT: "hsl(var(--secondary))",
37 | foreground: "hsl(var(--secondary-foreground))"
38 | },
39 | destructive: {
40 | DEFAULT: "hsl(var(--destructive))",
41 | foreground: "hsl(var(--destructive-foreground))"
42 | },
43 | muted: {
44 | DEFAULT: "hsl(var(--muted))",
45 | foreground: "hsl(var(--muted-foreground))"
46 | },
47 | accent: {
48 | DEFAULT: "hsl(var(--accent))",
49 | foreground: "hsl(var(--accent-foreground))"
50 | },
51 | popover: {
52 | DEFAULT: "hsl(var(--popover))",
53 | foreground: "hsl(var(--popover-foreground))"
54 | },
55 | card: {
56 | DEFAULT: "hsl(var(--card))",
57 | foreground: "hsl(var(--card-foreground))"
58 | }
59 | },
60 | borderRadius: {
61 | lg: "var(--radius)",
62 | md: "calc(var(--radius) - 2px)",
63 | sm: "calc(var(--radius) - 4px)"
64 | },
65 | keyframes: {
66 | "accordion-down": {
67 | from: { height: "0" },
68 | to: { height: "var(--radix-accordion-content-height)" }
69 | },
70 | "accordion-up": {
71 | from: { height: "var(--radix-accordion-content-height)" },
72 | to: { height: "0" }
73 | },
74 | ripple: {
75 | "0%, 100%": {
76 | transform: "translate(-50%, -50%) scale(1)"
77 | },
78 | "50%": {
79 | transform: "translate(-50%, -50%) scale(0.9)"
80 | }
81 | }
82 | },
83 | animation: {
84 | "accordion-down": "accordion-down 0.2s ease-out",
85 | "accordion-up": "accordion-up 0.2s ease-out",
86 | ripple: "ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite"
87 | }
88 | }
89 | },
90 | plugins: [tailwindcssAnimate]
91 | };
92 |
--------------------------------------------------------------------------------
/src/components/nav-buttons.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, memo } from "react";
2 | import { useStore } from "@/store/useStore";
3 |
4 | import { Button } from "./ui/button";
5 | import Settings from "./settings";
6 |
7 | import {
8 | ReplaceIcon,
9 | PlayIcon,
10 | Trash2Icon,
11 | LoaderCircleIcon
12 | } from "lucide-react";
13 |
14 | const ButtonsNav = () => {
15 | const {
16 | setDirection,
17 | direction,
18 | clearOutput,
19 | setError,
20 | runCode,
21 | code,
22 | isCodeExecuting
23 | } = useStore();
24 |
25 | const handleDirectionChange = useCallback(() => {
26 | setDirection(direction === "vertical" ? "horizontal" : "vertical");
27 | }, [direction, setDirection]);
28 |
29 | const handleTerminalClear = useCallback(() => {
30 | clearOutput("Running Python 3.12.7");
31 | setError(null);
32 | }, [clearOutput, setError]);
33 |
34 | const handleRunCode = useCallback(() => {
35 | runCode(code);
36 | }, [runCode, code]);
37 |
38 | return (
39 |
40 |
41 |
42 |
48 | ) : (
49 |
50 | )
51 | }
52 | label="Run"
53 | title="Execute Python Code"
54 | />
55 |
}
59 | label="Clear Terminal"
60 | title="Clear Terminal"
61 | />
62 |
}
65 | label={
66 | direction.substring(0, 1).toUpperCase() + direction.substring(1)
67 | }
68 | title="Change direction"
69 | />
70 |
71 |
72 |
73 |
74 |
75 |
76 | );
77 | };
78 |
79 | const NavButton = memo(
80 | ({
81 | onClick,
82 | disabled,
83 | icon,
84 | label,
85 | title
86 | }: {
87 | onClick: () => void;
88 | disabled?: boolean;
89 | icon: React.ReactNode;
90 | label: string;
91 | title: string;
92 | }) => (
93 |
100 | {icon}
101 | {label}
102 |
103 | )
104 | );
105 |
106 | NavButton.displayName = "NavButton";
107 |
108 | export default memo(ButtonsNav);
109 |
--------------------------------------------------------------------------------
/src/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Drawer as DrawerPrimitive } from "vaul";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const Drawer = ({
7 | shouldScaleBackground = true,
8 | ...props
9 | }: React.ComponentProps) => (
10 |
14 | );
15 | Drawer.displayName = "Drawer";
16 |
17 | const DrawerTrigger = DrawerPrimitive.Trigger;
18 |
19 | const DrawerPortal = DrawerPrimitive.Portal;
20 |
21 | const DrawerClose = DrawerPrimitive.Close;
22 |
23 | const DrawerOverlay = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ));
33 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
34 |
35 | const DrawerContent = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, children, ...props }, ref) => (
39 |
40 |
41 |
49 |
50 | {children}
51 |
52 |
53 | ));
54 | DrawerContent.displayName = "DrawerContent";
55 |
56 | const DrawerHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
64 | );
65 | DrawerHeader.displayName = "DrawerHeader";
66 |
67 | const DrawerFooter = ({
68 | className,
69 | ...props
70 | }: React.HTMLAttributes) => (
71 |
75 | );
76 | DrawerFooter.displayName = "DrawerFooter";
77 |
78 | const DrawerTitle = React.forwardRef<
79 | React.ElementRef,
80 | React.ComponentPropsWithoutRef
81 | >(({ className, ...props }, ref) => (
82 |
90 | ));
91 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
92 |
93 | const DrawerDescription = React.forwardRef<
94 | React.ElementRef,
95 | React.ComponentPropsWithoutRef
96 | >(({ className, ...props }, ref) => (
97 |
102 | ));
103 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
104 |
105 | export {
106 | Drawer,
107 | DrawerPortal,
108 | DrawerOverlay,
109 | DrawerTrigger,
110 | DrawerClose,
111 | DrawerContent,
112 | DrawerHeader,
113 | DrawerFooter,
114 | DrawerTitle,
115 | DrawerDescription
116 | };
117 |
--------------------------------------------------------------------------------
/src/components/editor/terminal.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useRef, useCallback, useMemo } from "react";
2 | import { useStore } from "@/store/useStore";
3 |
4 | import Loader from "@/components/loader";
5 |
6 | interface CommandHandlers {
7 | [key: string]: (input: string) => Promise;
8 | }
9 |
10 | export default function Terminal() {
11 | const {
12 | output,
13 | error,
14 | setOutput,
15 | isPyodideLoading,
16 | runCode,
17 | pipInstall,
18 | setError,
19 | clearOutput
20 | } = useStore();
21 | const [terminalCode, setTerminalCode] = useState("");
22 | const outputRef = useRef(null);
23 |
24 | const scrollToBottom = useCallback(() => {
25 | if (outputRef.current) {
26 | outputRef.current.scrollTop = outputRef.current.scrollHeight;
27 | }
28 | }, []);
29 |
30 | // Scroll to bottom on output change
31 | useEffect(() => {
32 | scrollToBottom();
33 | }, [output, scrollToBottom]);
34 |
35 | const getCwd = useCallback(async () => {
36 | await runCode("import os; print(os.getcwd())");
37 | }, [runCode]);
38 |
39 | const clearTerminal = useCallback(async () => {
40 | clearOutput("Running Python 3.12.7");
41 | setError(null);
42 | }, [clearOutput, setError]);
43 |
44 | const commandHandlers: CommandHandlers = useMemo(
45 | () => ({
46 | "pip install": async (input: string) => await pipInstall(input),
47 | "echo ": async (input: string) =>
48 | await setOutput(input.split(" ").slice(1).join(" ")),
49 | pwd: getCwd,
50 | cwd: getCwd,
51 | clear: clearTerminal,
52 | cls: clearTerminal
53 | }),
54 | [pipInstall, setOutput, getCwd, clearTerminal]
55 | );
56 |
57 | const handleReplSubmit = useCallback(
58 | async (e: React.FormEvent) => {
59 | e.preventDefault();
60 | const formData = new FormData(e.currentTarget);
61 | const replInput = formData.get("terminalCode") as string;
62 |
63 | if (!replInput.trim()) return;
64 |
65 | setOutput(replInput);
66 |
67 | const handler = Object.entries(commandHandlers).find(([key]) =>
68 | replInput.startsWith(key)
69 | );
70 |
71 | if (handler) {
72 | await handler[1](replInput);
73 | } else {
74 | await runCode(replInput);
75 | }
76 |
77 | setTerminalCode("");
78 | },
79 | [runCode, setOutput, commandHandlers]
80 | );
81 |
82 | if (isPyodideLoading) {
83 | return (
84 |
85 |
86 |
87 | );
88 | }
89 |
90 | return (
91 |
95 | {(error || output) && (
96 |
99 | {error || output}
100 |
101 | )}
102 |
115 |
116 | );
117 | }
118 |
--------------------------------------------------------------------------------
/src/store/useStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import type { PyodideInterface } from "@/types";
3 |
4 | type Direction = "horizontal" | "vertical";
5 |
6 | interface State {
7 | code: string;
8 | output: string;
9 | error: string | null;
10 | direction: Direction;
11 | pyodide: PyodideInterface | null;
12 | isPyodideLoading: boolean;
13 | isCodeExecuting: boolean;
14 | isLibLoading: boolean;
15 | }
16 |
17 | interface Actions {
18 | setCode: (code: string) => void;
19 | setOutput: (newOutput: string) => void;
20 | clearOutput: (defaultValue?: string) => void;
21 | setError: (error: string | null) => void;
22 | setDirection: (direction: Direction) => void;
23 | initializePyodide: () => Promise;
24 | pipInstall: (packageName: string) => Promise;
25 | runCode: (code: string) => Promise;
26 | }
27 |
28 | const initialState: State = {
29 | code: `import sys\n\nprint("Python", sys.version)\n\n# https://github.com/vwh/python-playground`,
30 | output: "Running Python 3.12.7",
31 | error: null,
32 | direction: "vertical",
33 | pyodide: null,
34 | isPyodideLoading: true,
35 | isCodeExecuting: false,
36 | isLibLoading: false
37 | };
38 |
39 | export const useStore = create((set, get) => ({
40 | ...initialState,
41 |
42 | setCode: (code) => set({ code }),
43 | setOutput: (newOutput) =>
44 | set((state) => ({ output: `${state.output}\n${newOutput}` })),
45 | clearOutput: (defaultValue = "") => set({ output: defaultValue }),
46 | setError: (error) => set({ error }),
47 | setDirection: (direction) => set({ direction }),
48 |
49 | initializePyodide: async () => {
50 | if (!window.loadPyodide) {
51 | set({ error: "Pyodide script not loaded.", isPyodideLoading: false });
52 | return;
53 | }
54 |
55 | try {
56 | const pyodideInstance = await window.loadPyodide();
57 | await pyodideInstance.loadPackage("micropip");
58 | const micropip = await pyodideInstance.pyimport("micropip");
59 | window.micropip = micropip;
60 | set({ pyodide: pyodideInstance, isPyodideLoading: false });
61 | } catch (error) {
62 | console.error("Failed to load Pyodide:", error);
63 | set({
64 | error: "Failed to load Pyodide. Please refresh the page and try again.",
65 | isPyodideLoading: false
66 | });
67 | }
68 | },
69 |
70 | pipInstall: async (packageName: string) => {
71 | const { setOutput, setError } = get();
72 | const lib = packageName.replace("pip install ", "").trim();
73 |
74 | if (!window.micropip || !lib) return;
75 | set({ isLibLoading: true });
76 |
77 | try {
78 | await window.micropip.install(lib, true);
79 | setOutput(`pip install ${lib} successfully installed`);
80 | setError(null);
81 | } catch (e) {
82 | setError(`Failed to install ${lib}: ${(e as Error).message}`);
83 | } finally {
84 | set({ isLibLoading: false });
85 | }
86 | },
87 |
88 | runCode: async (code: string) => {
89 | const { pyodide, setError, setOutput } = get();
90 | if (!pyodide) return;
91 |
92 | set({ isCodeExecuting: true });
93 | try {
94 | setError(null);
95 | const printOutput: string[] = [];
96 |
97 | pyodide.globals.set("print", (...args: any[]) => {
98 | const result = args.join(" ");
99 | printOutput.push(result);
100 | setOutput(printOutput.join("\n"));
101 | });
102 |
103 | await pyodide.runPythonAsync(code);
104 | } catch (error) {
105 | console.error(error);
106 | setError(
107 | error instanceof Error ? error.message : "An unknown error occurred"
108 | );
109 | } finally {
110 | set({ isCodeExecuting: false });
111 | }
112 | }
113 | }));
114 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | # Python Playground
8 |
9 |
10 | Browser-based Python playground built using WebAssembly (Pyodide) and ReactJS. Run and test Python code directly in your browser with an interactive and user-friendly interface.
11 |
12 |
13 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | ## Features
37 |
38 |
39 |
40 | Python in Browser
41 | WebAssembly Powered
42 | Library Support
43 |
44 |
45 |
46 | • Run Python code instantly
47 | • No server-side processing
48 | • Immediate feedback and results
49 |
50 |
51 | • Pyodide integration
52 | • Near-native performance
53 | • Seamless Python execution
54 |
55 |
56 | • Install packages via micropip
57 | • Access to wide range of libraries
58 | • Expand functionality on-the-fly
59 |
60 |
61 |
62 | Terminal Commands
63 | Intuitive Interface
64 | Fast and Responsive
65 |
66 |
67 |
68 | • Support for pwd, clear, etc.
69 | • Familiar command-line experience
70 | • Enhanced debugging capabilities
71 |
72 |
73 | • Clean and modern UI design
74 | • User-friendly code editor
75 | • Intuitive layout and controls
76 |
77 |
78 | • Quick code execution
79 | • Responsive editor interface
80 | • Smooth user experience
81 |
82 |
83 |
84 |
85 | ## Contributing
86 |
87 | Contributions are welcome! Feel free to open a pull request with your improvements or fixes.
88 |
89 | ## License
90 |
91 | Under the MIT License. See [License](https://github.com/vwh/python-playground/blob/main/LICENSE) for more information.
92 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 | Python Playground
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
22 |
28 |
34 |
40 |
46 |
47 |
51 |
55 |
56 |
60 |
64 |
68 |
69 |
70 |
74 |
75 |
76 |
77 |
78 |
82 |
83 |
97 |
98 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
JavaScript Required
110 |
111 | We're sorry, but this application requires JavaScript to function
112 | correctly. Please enable JavaScript in your browser and reload the
113 | page.
114 |
115 |
116 |
117 |
118 |
119 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: ["main"]
17 | pull_request:
18 | branches: ["main"]
19 | schedule:
20 | - cron: "29 20 * * 2"
21 |
22 | jobs:
23 | analyze:
24 | name: Analyze (${{ matrix.language }})
25 | # Runner size impacts CodeQL analysis time. To learn more, please see:
26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql
27 | # - https://gh.io/supported-runners-and-hardware-resources
28 | # - https://gh.io/using-larger-runners (GitHub.com only)
29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements.
30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
31 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
32 | permissions:
33 | # required for all workflows
34 | security-events: write
35 |
36 | # required to fetch internal or private CodeQL packs
37 | packages: read
38 |
39 | # only required for workflows in private repositories
40 | actions: read
41 | contents: read
42 |
43 | strategy:
44 | fail-fast: false
45 | matrix:
46 | include:
47 | - language: javascript-typescript
48 | build-mode: none
49 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
50 | # Use `c-cpp` to analyze code written in C, C++ or both
51 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
52 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
53 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
54 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
55 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
56 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
57 | steps:
58 | - name: Checkout repository
59 | uses: actions/checkout@v4
60 |
61 | # Initializes the CodeQL tools for scanning.
62 | - name: Initialize CodeQL
63 | uses: github/codeql-action/init@v3
64 | with:
65 | languages: ${{ matrix.language }}
66 | build-mode: ${{ matrix.build-mode }}
67 | # If you wish to specify custom queries, you can do so here or in a config file.
68 | # By default, queries listed here will override any specified in a config file.
69 | # Prefix the list here with "+" to use these queries and those in the config file.
70 |
71 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
72 | # queries: security-extended,security-and-quality
73 |
74 | # If the analyze step fails for one of the languages you are analyzing with
75 | # "We were unable to automatically build your code", modify the matrix above
76 | # to set the build mode to "manual" for that language. Then modify this step
77 | # to build your code.
78 | # ℹ️ Command-line programs to run using the OS shell.
79 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
80 | - if: matrix.build-mode == 'manual'
81 | shell: bash
82 | run: |
83 | echo 'If you are using a "manual" build mode for one or more of the' \
84 | 'languages you are analyzing, replace this with the commands to build' \
85 | 'your code, for example:'
86 | echo ' make bootstrap'
87 | echo ' make release'
88 | exit 1
89 |
90 | - name: Perform CodeQL Analysis
91 | uses: github/codeql-action/analyze@v3
92 | with:
93 | category: "/language:${{matrix.language}}"
94 |
--------------------------------------------------------------------------------
/src/components/settings.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from "react";
2 | import { useStore } from "@/store/useStore";
3 |
4 | import { Input } from "./ui/input";
5 | import { Button } from "./ui/button";
6 | import {
7 | Drawer,
8 | DrawerClose,
9 | DrawerContent,
10 | DrawerDescription,
11 | DrawerFooter,
12 | DrawerHeader,
13 | DrawerTitle,
14 | DrawerTrigger
15 | } from "./ui/drawer";
16 |
17 | import {
18 | DownloadIcon,
19 | UploadIcon,
20 | SettingsIcon,
21 | LoaderIcon
22 | } from "lucide-react";
23 |
24 | interface SettingsSectionProps {
25 | title: string;
26 | children: React.ReactNode;
27 | }
28 |
29 | function SettingsSection({ title, children }: SettingsSectionProps) {
30 | return (
31 |
32 |
{title}
33 |
{children}
34 |
35 | );
36 | }
37 |
38 | export default function Settings() {
39 | const { code, pipInstall, isLibLoading } = useStore();
40 |
41 | const handleDownloadCode = useCallback(() => {
42 | const blob = new Blob([code], { type: "text/plain" });
43 | const url = URL.createObjectURL(blob);
44 | const link = document.createElement("a");
45 | link.download = "code.py";
46 | link.href = url;
47 | link.click();
48 | URL.revokeObjectURL(url);
49 | }, [code]);
50 |
51 | const handlePipInstall = useCallback(
52 | async (e: React.FormEvent) => {
53 | e.preventDefault();
54 | const packageName = new FormData(e.currentTarget).get("lib") as string;
55 | await pipInstall(packageName);
56 | },
57 | [pipInstall]
58 | );
59 |
60 | const handleCodeShare = useCallback(() => {
61 | const urlParams = new URLSearchParams(window.location.search);
62 | urlParams.set("v", btoa(code));
63 | const newUrl = `${window.location.origin}${window.location.pathname}?${urlParams.toString()}${window.location.hash}`;
64 | window.history.replaceState({}, document.title, newUrl);
65 | navigator.clipboard.writeText(newUrl);
66 | }, [code]);
67 |
68 | return (
69 |
70 |
71 |
76 |
77 | Settings
78 |
79 |
80 |
81 |
82 |
83 | Settings
84 |
85 | Personalize your site experience here.
86 |
87 |
88 |
89 |
90 |
111 |
112 |
113 |
119 |
120 | Download Code
121 |
122 |
128 |
129 | Share Code
130 |
131 |
132 |
133 |
134 |
135 |
136 | Close
137 |
138 |
139 |
140 |
141 |
142 |
143 | );
144 | }
145 |
--------------------------------------------------------------------------------