Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 49 additions & 4 deletions components/TextBox.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { Button, Tabs, Dropdown, Menu, Space } from "antd";
import { Button, Tabs, Dropdown, Menu, Space, Tooltip, message } from "antd";
const { TabPane } = Tabs;
import { DownOutlined, PlayCircleOutlined,FullscreenOutlined,FullscreenExitOutlined} from "@ant-design/icons";
import {
DownOutlined,
PlayCircleOutlined,
FullscreenOutlined,
FullscreenExitOutlined,
ShareAltOutlined
} from "@ant-design/icons"; // Combined all icons here

import { useIsMobile } from "./useIsMobile";
import React, { useState, useEffect } from 'react';
import preinstalled_programs from "../utils/preinstalled_programs";
import { encodeSnippet } from "../utils/snippet";
import dynamic from 'next/dynamic'

const Editor = dynamic(import('./Editor'), {
ssr: false
})

function TextBox({ disabled, sourceCode, setSourceCode, exampleName, setExampleName, activeTab, handleUserTabChange, myHeight }) {
function TextBox({ disabled, sourceCode, setSourceCode, exampleName, setExampleName, activeTab, handleUserTabChange, myHeight,sourceUrl,setSourceUrl }) {
const isMobile = useIsMobile();
const [isFullScreen, setIsFullScreen] = useState(false);

Expand All @@ -27,6 +36,30 @@ function TextBox({ disabled, sourceCode, setSourceCode, exampleName, setExampleN
document.exitFullscreen();
}
};
const handleShare = () => {
// Step 1: Guard against sharing local/manual code without an external source
if (!sourceUrl) {
message.warning("Only code loaded from an external source (Gist/Snippet) can be shared.");
return;
}

// Step 2: Debug log before encoding to ensure the source URL is correct
console.log("[Share] Encoding:", sourceUrl);

// Step 3: Encode the full raw URL and build a clean base link
const hash = encodeSnippet(sourceUrl);
const shareLink = `${window.location.origin}?snippet=${hash}`;

// Step 4: Write to clipboard with success/error feedback
navigator.clipboard.writeText(shareLink)
.then(() => {
message.success("Shareable link copied to clipboard!");
})
.catch((err) => {
console.error("[Share] Clipboard Error:", err);
message.error("Failed to copy link.");
});
};

var menu_items = [];
for (let category in preinstalled_programs) {
Expand All @@ -38,6 +71,7 @@ function TextBox({ disabled, sourceCode, setSourceCode, exampleName, setExampleN
onClick: () => {
setSourceCode(preinstalled_programs[category][example]);
setExampleName(example);
if (setSourceUrl) setSourceUrl(""); // Clear the external URL
}
});
}
Expand All @@ -53,13 +87,24 @@ function TextBox({ disabled, sourceCode, setSourceCode, exampleName, setExampleN
const extraOperations = {
right: (
<Space>
{/* Tooltip provides UX context for why the button might be disabled */}
<Tooltip title={sourceUrl ? "Share this snippet" : "Load an external source to enable sharing"}>
<Button
icon={<ShareAltOutlined />}
onClick={handleShare}
disabled={!sourceUrl} // Ensures sharing only works for external sources
>
{!isMobile && "Share"}
</Button>
</Tooltip>

<Button
onClick={handleFullScreen}
icon={isFullScreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
>
{/* Now both text options only show on desktop, keeping mobile clean */}
{!isMobile && (isFullScreen ? " Exit Fullscreen" : " Fullscreen")}
</Button>

<Button
disabled={disabled}
onClick={() => handleUserTabChange(activeTab)}
Expand Down
81 changes: 56 additions & 25 deletions pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import LoadLFortran from "../components/LoadLFortran";
import preinstalled_programs from "../utils/preinstalled_programs";
import { useIsMobile } from "../components/useIsMobile";

import { useState, useEffect } from "react";
import { Col, Row, Spin } from "antd";
import { notification } from "antd";
import { LoadingOutlined } from "@ant-design/icons";
import { useState, useEffect, useRef } from "react";
import { Col, Row, Spin, notification } from "antd"; // Combined into one line
import { LoadingOutlined, ShareAltOutlined } from "@ant-design/icons"; // Added Share icon
import AnsiUp from "ansi_up";

// ONLY add this for Issue #23
import { decodeSnippet } from "../utils/snippet";

var ansi_up = new AnsiUp();

const antIcon = (
Expand Down Expand Up @@ -42,58 +44,86 @@ var lfortran_funcs = {
export default function Home() {
const [moduleReady, setModuleReady] = useState(false);
const [sourceCode, setSourceCode] = useState("");
const [sourceUrl, setSourceUrl] = useState("");
const [exampleName, setExampleName] = useState("main");
const [activeTab, setActiveTab] = useState("STDOUT");
const [output, setOutput] = useState("");
const [dataFetch, setDataFetch] = useState(false);
const isMobile = useIsMobile();

const initialized = useRef(false);
const myHeight = ((!isMobile) ? "calc(100vh - 170px)" : "calc(50vh - 85px)");

// Consolidated Effect: Handles mounting logic exactly once
useEffect(() => {
setSourceCode("");
fetchData();
if (!initialized.current) {
initialized.current = true;
setSourceCode(""); // Clear initial state
fetchData(); // Trigger the fetch
}
}, []);

// Handles the automatic run/tab change once data is ready
useEffect(() => {
if(moduleReady && dataFetch) {
handleUserTabChange("STDOUT");
}
}, [moduleReady, dataFetch]);

async function fetchData() {
const url = window.location.search;
const gist = "https://gist.githubusercontent.com/";
const urlParams = new URLSearchParams(url);
const urlParams = new URLSearchParams(window.location.search);
let downloadUrl = "";

if (urlParams.get("code")) {
// Case 1: Shared Snippet (Issue #23)
if (urlParams.get("snippet")) {
downloadUrl = decodeSnippet(urlParams.get("snippet"));
}
// Case 2: Direct URL Parameter (Direct Raw Link)
else if (urlParams.get("url")) {
downloadUrl = urlParams.get("url");
}
// Case 4: Direct Code
else if (urlParams.get("code")) {
setSourceCode(decodeURIComponent(urlParams.get("code")));
setSourceUrl("");
setDataFetch(true);
} else if (urlParams.get("gist")) {
const gistUrl = gist + urlParams.get("gist") + "/raw/";
fetch(gistUrl, {cache: "no-store"})
.then((response) => response.text())
return;
}

// Execution: Fetch only if a downloadUrl was successfully determined
if (downloadUrl) {
fetch(downloadUrl, { cache: "no-store" })
.then((response) => {
if (!response.ok) throw new Error("Fetch failed");
return response.text();
})
.then((data) => {
setSourceCode(data);
setSourceUrl(downloadUrl); // Enable Share button
setDataFetch(true);
openNotification(
"Source Code loaded from gist.",
"bottomRight"
);
openNotification("Source Code loaded successfully.", "bottomRight");
})
.catch((error) => {
console.error("Error fetching data:", error);
openNotification("error fetching .", "bottomRight");
console.error("Fetch error:", error);
// Only one notification will now appear due to the Ref guard
openNotification("Error: Please provide a direct download link.", "bottomRight");
setSourceCode(preinstalled_programs.basic.mandelbrot);
setSourceUrl("");
setDataFetch(true);
});
} else {
}
else {
// Default behavior if no valid download parameters are present
setSourceCode(preinstalled_programs.basic.mandelbrot);
setSourceUrl("");
setDataFetch(true);
if(urlParams.size>0){

// Only notify for invalid/unsupported parameters if the URL isn't empty
const hasParams = urlParams.keys().next().done === false;
if (hasParams && !urlParams.get("code") && !urlParams.get("gist") && !urlParams.get("url") && !urlParams.get("snippet")) {
openNotification("The URL contains an invalid parameter.", "bottomRight");
}
}
}

async function handleUserTabChange(key) {
if (key == "STDOUT") {
if(sourceCode.trim() === ""){
Expand Down Expand Up @@ -144,7 +174,6 @@ export default function Home() {
} else if (key == "PY") {
setOutput("Support for PY is not yet enabled");
} else {
console.log("Unknown key:", key);
setOutput("Unknown key: " + key);
}
setActiveTab(key);
Expand All @@ -166,6 +195,8 @@ export default function Home() {
disabled={!moduleReady}
sourceCode={sourceCode}
setSourceCode={setSourceCode}
sourceUrl={sourceUrl} // New prop for sharing
setSourceUrl={setSourceUrl} // Allow clearing if code changes manually
exampleName={exampleName}
setExampleName={setExampleName}
activeTab={activeTab}
Expand Down
55 changes: 55 additions & 0 deletions utils/snippet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Utility for Issue #23: Shareable Snippet Links
* Encodes and decodes external URLs into URL-safe Base64 strings.
*/

/**
* Encodes a URL into a URL-safe Base64 string
* @param {string} url - The raw Gist or GitHub raw URL
* @returns {string} - The encoded snippet hash
*/
export function encodeSnippet(url) {
if (!url) return "";

try {
// 1. Standard Base64 encoding
const base64 = window.btoa(url);

// 2. Make it URL-safe:
// Replace '+' with '-', '/' with '_', and remove trailing '='
return base64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
} catch (e) {
console.error("[Snippet] Encoding failed:", e);
return "";
}
}

/**
* Decodes a URL-safe Base64 string back into the original URL
* @param {string} hash - The encoded string from the URL path
* @returns {string} - The decoded download URL
*/
export function decodeSnippet(hash) {
if (!hash) return "";

try {
// 1. Reverse the URL-safe replacements
let base64 = hash
.replace(/-/g, '+')
.replace(/_/g, '/');

// 2. Restore padding '=' if necessary
while (base64.length % 4) {
base64 += '=';
}

// 3. Standard Base64 decoding
return window.atob(base64);
} catch (e) {
console.error("[Snippet] Decoding failed:", e);
return "";
}
}