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
54 changes: 34 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,25 @@
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com)
[![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)](https://github.com/ellerbrock/open-source-badges/)

**Web3 Student Lab** is an open-source educational platform that helps students learn blockchain,
smart contracts, open-source collaboration, and hackathon project development in one place.
**Web3 Student Lab** is an open-source educational platform that helps students learn blockchain, smart contracts, open-source collaboration, and hackathon project development in one place.

The platform provides **interactive tools, coding environments, and guided learning paths** designed
for beginners and university students.
The platform provides **interactive tools, coding environments, and guided learning paths** designed for beginners and university students.

## 🟢 Live Deployment

The application is fully deployed and accessible online:

- **Frontend Application**: [https://web3-student-lab.vercel.app/](https://web3-student-lab.vercel.app/)
- **Backend Infrastructure**: Hosted securely on Render using PostgreSQL, Redis, and integrated with the Stellar/Soroban Testnet.
- **Smart Contracts**: My contract is deployed!

## 🚀 Core Modules

1. **Blockchain Learning Simulator**: Visually learn how blockchains work (create transactions, mine
blocks, view hashes, and see how blocks connect).
2. **Smart Contract Playground**: Write, run, and test smart contracts directly in your browser.
Focuses on Soroban contracts written in Rust.
3. **Web3 Learning Roadmap**: A guided path spanning programming fundamentals, cryptography,
blockchain architecture, smart contracts, and full Web3 applications.
4. **Hackathon Project Idea Generator**: Overcome coder's block by generating ideas based on
technology and sector preferences.
5. **Open Source Contribution Trainer**: Get hands-on with Git, simulated GitHub issues, and PR
exercises to confidently contribute to open source.
1. **Blockchain Learning Simulator**: Visually learn how blockchains work (create transactions, mine blocks, view hashes, and see how blocks connect).
2. **Smart Contract Playground**: Write, run, and test smart contracts directly in your browser. Focuses on Soroban contracts written in Rust.
3. **Web3 Learning Roadmap**: A guided path spanning programming fundamentals, cryptography, blockchain architecture, smart contracts, and full Web3 applications.
4. **Hackathon Project Idea Generator**: Overcome coder's block by generating ideas based on technology and sector preferences.
5. **Open Source Contribution Trainer**: Get hands-on with Git, simulated GitHub issues, and PR exercises to confidently contribute to open source.

## 🛠 Technology Stack

Expand All @@ -38,10 +32,8 @@ The application is fully deployed and accessible online:
- Tailwind CSS
- Monaco Editor


**Backend**


- Node.js / Express
- PostgreSQL

Expand Down Expand Up @@ -70,16 +62,38 @@ web3-student-lab/
└── docs/ # Documentation and learning materials
```

# [Frontend] Build WebAssembly Rust Compiler for Browser Sandbox
## Feature Overview
Run the Rust compiler in WebAssembly directly in the student's browser to compile playground code without hitting backend sandboxes.

This is an essential, MVP-critical feature designed to take Web3 Student Lab's curriculum layer to a dynamic, production-ready level.

## 🛠️ Implementation Requirements

- Fetch and compile lightweight Rust/Wasm toolchains.
- Execute code compilation client-side inside a web worker thread.

## 🔧 Technical Specifications

- Rust compiled to WASM, Next.js, Web Workers.

## ✅ Acceptance Criteria

- Editor compiles Rust code into WebAssembly in-browser in <3 seconds.

---

## 🎓 Difficulty Level
Advanced - Requires understanding of frontend development.

## 🤝 Contributing

We love our contributors! This project is being built for students, by students and open-source
enthusiasts.
We love our contributors! This project is being built for students, by students and open-source enthusiasts.

To start contributing:

1. Read our [Contribution Guidelines](CONTRIBUTING.md).
2. Check out our existing [Issues](https://github.com/your-repo/issues) or look for the
`good first issue` label.
2. Check out our existing [Issues](https://github.com/your-repo/issues) or look for the `good first issue` label.
3. Fork the repository and submit a Pull Request!

## 📜 License
Expand Down
100 changes: 78 additions & 22 deletions frontend/src/app/playground/page.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
'use client';

import { VirtualizedFileTree, type FileTreeNode } from '@/components/explorer/VirtualizedFileTree';
import dynamic from 'next/dynamic';
const CodeEditor = dynamic(() => import('@/components/playground/CodeEditor').then((mod) => mod.CodeEditor), {
ssr: false,
});
import { OfflineIndicator } from '@/components/storage/OfflineIndicator';
import {
CompileOutputTerminal,
type CompileLogEntry,
CompileOutputTerminal,
type CompileLogEntry,
} from '@/components/terminal/CompileOutputTerminal';
import { TerminalPanel } from '@/components/terminal/TerminalPanel';
import { WithSkeleton } from '@/components/ui/WithSkeleton';
import { EditorSkeleton } from '@/components/ui/skeletons/EditorSkeleton';
import { useTutorial } from '@/contexts/TutorialContext';
import { useState, useEffect, useMemo, useCallback } from 'react';
import { CollaborationProvider } from '@/lib/collaboration/YjsProvider';
import type { CompileWorkerResponse } from '@/lib/compiler/compileTypes';
import { FilePresenceManager } from '@/lib/explorer/FilePresence';
import { DatabaseManager } from '@/lib/storage/DatabaseManager';
import { SyncManager } from '@/lib/storage/SyncManager';
import { FilePresenceManager } from '@/lib/explorer/FilePresence';
import { Settings, X } from 'lucide-react';
import dynamic from 'next/dynamic';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
const CodeEditor = dynamic(() => import('@/components/playground/CodeEditor').then((mod) => mod.CodeEditor), {
ssr: false,
});

const INITIAL_TREE: FileTreeNode[] = [
{
Expand Down Expand Up @@ -95,6 +96,19 @@ function moveFileNode(
export default function PlaygroundPage() {
const [compileLogs, setCompileLogs] = useState<CompileLogEntry[]>([]);
const [isCompiling, setIsCompiling] = useState(false);
const [sourceCode, setSourceCode] = useState<string>(`#![no_std]

use soroban_sdk::{contract, contractimpl, Env, Symbol};

#[contract]
pub struct HelloContract;

#[contractimpl]
impl HelloContract {
pub fn hello(_env: Env) -> Symbol {
Symbol::new(&_env, "hello")
}
}`);
const [settingsOpen, setSettingsOpen] = useState(false);
const [editorSettings, setEditorSettings] = useState({
fontSize: 14,
Expand All @@ -112,6 +126,44 @@ export default function PlaygroundPage() {
const [isOnline, setIsOnline] = useState(true);
const [pendingCount, setPendingCount] = useState(0);
const [activeTab, setActiveTab] = useState<'editor' | 'output'>('editor');
const workerRef = useRef<Worker | null>(null);

useEffect(() => {
const compileWorker = new Worker(new URL('../../lib/compiler/compile.worker.ts', import.meta.url), {
type: 'module',
});

const handleWorkerMessage = (event: MessageEvent<CompileWorkerResponse>) => {
const data = event.data;
if (data.type === 'log') {
setCompileLogs((prev) => [...prev, data.entry]);
return;
}
if (data.type === 'complete') {
setIsCompiling(false);
if (!data.success && data.errors.length === 0) {
setCompileLogs((prev) => [
...prev,
{
id: crypto.randomUUID?.() ?? `${Date.now()}-compile-failed`,
level: 'error',
timestamp: new Date().toLocaleTimeString(),
message: 'Browser compile failed with unknown diagnostics.',
},
]);
}
}
};

compileWorker.addEventListener('message', handleWorkerMessage);
workerRef.current = compileWorker;

return () => {
compileWorker.removeEventListener('message', handleWorkerMessage);
compileWorker.terminate();
workerRef.current = null;
};
}, []);

useEffect(() => {
const timer = setTimeout(() => setIsInitializing(false), 1500);
Expand Down Expand Up @@ -188,7 +240,7 @@ export default function PlaygroundPage() {
}, [activeFilePath, databaseManager]);

const handleCompile = useCallback(() => {
setIsCompiling(true);
if (isCompiling) return;
const stamp = () => new Date().toLocaleTimeString();
setCompileLogs([
{
Expand All @@ -204,26 +256,28 @@ export default function PlaygroundPage() {
message: 'Checking Rust target wasm32-unknown-unknown...',
},
]);
setTimeout(() => {
setIsCompiling(true);

if (!workerRef.current) {
setCompileLogs((prev) => [
...prev,
{
id: crypto.randomUUID?.() ?? `${Date.now()}-compile-success`,
level: 'success',
timestamp: stamp(),
message: 'Compilation successful. WASM size: 4.2KB',
},
{
id: crypto.randomUUID?.() ?? `${Date.now()}-compile-ready`,
level: 'success',
id: crypto.randomUUID?.() ?? `${Date.now()}-compile-error`,
level: 'error',
timestamp: stamp(),
message:
'Contract ready for simulation. Exports: register_hash, verify, history_for_owner, process_payment, refund_payment.',
message: 'Unable to start browser compiler worker.',
},
]);
setIsCompiling(false);
}, 1500);
}, [activeFilePath]);
return;
}

workerRef.current.postMessage({
type: 'compile',
source: sourceCode,
filePath: activeFilePath,
});
}, [activeFilePath, isCompiling, sourceCode]);

useEffect(() => {
const handleShortcutCompile = () => {
Expand Down Expand Up @@ -355,6 +409,8 @@ export default function PlaygroundPage() {
roomName="main-lab-session"
collaborationProvider={provider}
settings={editorSettings}
value={sourceCode}
onCodeChange={setSourceCode}
/>
</WithSkeleton>
</div>
Expand Down
38 changes: 28 additions & 10 deletions frontend/src/components/playground/CodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
'use client';

import dynamic from 'next/dynamic';
import type { OnMount } from '@monaco-editor/react';
import type { editor } from 'monaco-editor';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ChevronRight, FileText } from 'lucide-react';
import type { CollaborationProvider } from '@/lib/collaboration/YjsProvider';
import { extendRustLanguage } from '@/lib/editor/SorobanLanguage';
import { registerSorobanCompletion } from '@/lib/editor/SorobanCompletion';
import { registerSorobanHover } from '@/lib/editor/SorobanHover';
import { createSorobanLinter } from '@/lib/editor/SorobanLinter';
import { extendRustLanguage } from '@/lib/editor/SorobanLanguage';
import type { SorobanLinterInstance } from '@/lib/editor/SorobanLinter';
import { createSorobanLinter } from '@/lib/editor/SorobanLinter';
import { THEME_COLORS } from '@/lib/theme/themeColors';
import type { OnMount } from '@monaco-editor/react';
import { ChevronRight, FileText } from 'lucide-react';
import type { editor } from 'monaco-editor';
import dynamic from 'next/dynamic';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';

const Editor = dynamic(() => import('@monaco-editor/react'), {
ssr: false,
Expand All @@ -30,6 +30,8 @@ interface CodeEditorProps {
mobileMode?: boolean;
collaborationProvider?: CollaborationProvider;
settings?: MonacoEditorSettings;
value?: string;
onCodeChange?: (value: string) => void;
}

export interface MonacoEditorSettings {
Expand Down Expand Up @@ -115,9 +117,11 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
mobileMode = false,
collaborationProvider,
settings = { fontSize: 14, tabSize: 2, vimBindings: false },
value,
onCodeChange,
}) => {
const [editorInstance, setEditorInstance] = useState<editor.IStandaloneCodeEditor | null>(null);
const [code, setCode] = useState(DEFAULT_CODE);
const [code, setCode] = useState(value ?? DEFAULT_CODE);
const [monacoError, setMonacoError] = useState(false);
const linterRef = useRef<SorobanLinterInstance | null>(null);
const compileActionRef = useRef<{ dispose: () => void } | null>(null);
Expand All @@ -131,8 +135,16 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
}, [collaborationProvider, roomName]);

const handleCodeChange = useCallback((value: string | undefined) => {
setCode(value ?? '');
}, []);
const next = value ?? '';
setCode(next);
onCodeChange?.(next);
}, [onCodeChange]);

useEffect(() => {
if (value !== undefined && value !== code) {
setCode(value);
}
}, [value, code]);

const handleMonacoError = useCallback(() => {
setMonacoError(true);
Expand Down Expand Up @@ -235,6 +247,12 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
};
}, []);

useEffect(() => {
if (value !== undefined && value !== code) {
setCode(value);
}
}, [value, code]);

if (monacoError) {
return (
<div
Expand Down
Loading