Merge pull request #53 from davidmonterocrespo24/website
feat: add GitHub star banner to encourage users to star the project a…pull/74/head
commit
400437278f
|
|
@ -0,0 +1,87 @@
|
||||||
|
.gh-star-banner {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 28px;
|
||||||
|
right: 28px;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 14px;
|
||||||
|
background: #1e2228;
|
||||||
|
border: 1px solid #3c4049;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
width: 320px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.55);
|
||||||
|
animation: gh-banner-in 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gh-banner-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px) scale(0.97);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-star-banner__icon {
|
||||||
|
color: #e3b341;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-star-banner__body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-star-banner__body strong {
|
||||||
|
display: block;
|
||||||
|
color: #e1e4e8;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-star-banner__body p {
|
||||||
|
color: #8b949e;
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.45;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-star-banner__btn {
|
||||||
|
display: inline-block;
|
||||||
|
background: #e3b341;
|
||||||
|
color: #1b1f23;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-star-banner__btn:hover {
|
||||||
|
background: #f0c860;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-star-banner__close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #6e7681;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 1px;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-star-banner__close:hover {
|
||||||
|
color: #e1e4e8;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import React from 'react';
|
||||||
|
import './GitHubStarBanner.css';
|
||||||
|
|
||||||
|
const GITHUB_URL = 'https://github.com/davidmonterocrespo24/velxio';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GitHubStarBanner: React.FC<Props> = ({ onClose }) => (
|
||||||
|
<div className="gh-star-banner" role="dialog" aria-label="Star Velxio on GitHub">
|
||||||
|
<div className="gh-star-banner__icon">
|
||||||
|
{/* GitHub logo */}
|
||||||
|
<svg width="30" height="30" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||||
|
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="gh-star-banner__body">
|
||||||
|
<strong>Enjoying Velxio?</strong>
|
||||||
|
<p>
|
||||||
|
A ⭐ on GitHub helps more developers discover the project — it only takes 2 seconds!
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={GITHUB_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="gh-star-banner__btn"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
⭐ Star on GitHub
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="gh-star-banner__close" onClick={onClose} aria-label="Dismiss">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
@ -20,6 +20,7 @@ import { Oscilloscope } from '../components/simulator/Oscilloscope';
|
||||||
import { AppHeader } from '../components/layout/AppHeader';
|
import { AppHeader } from '../components/layout/AppHeader';
|
||||||
import { SaveProjectModal } from '../components/layout/SaveProjectModal';
|
import { SaveProjectModal } from '../components/layout/SaveProjectModal';
|
||||||
import { LoginPromptModal } from '../components/layout/LoginPromptModal';
|
import { LoginPromptModal } from '../components/layout/LoginPromptModal';
|
||||||
|
import { GitHubStarBanner } from '../components/layout/GitHubStarBanner';
|
||||||
import { useSimulatorStore } from '../store/useSimulatorStore';
|
import { useSimulatorStore } from '../store/useSimulatorStore';
|
||||||
import { useOscilloscopeStore } from '../store/useOscilloscopeStore';
|
import { useOscilloscopeStore } from '../store/useOscilloscopeStore';
|
||||||
import { useAuthStore } from '../store/useAuthStore';
|
import { useAuthStore } from '../store/useAuthStore';
|
||||||
|
|
@ -68,6 +69,46 @@ export const EditorPage: React.FC = () => {
|
||||||
const [bottomPanelHeight, setBottomPanelHeight] = useState(BOTTOM_PANEL_DEFAULT);
|
const [bottomPanelHeight, setBottomPanelHeight] = useState(BOTTOM_PANEL_DEFAULT);
|
||||||
const [saveModalOpen, setSaveModalOpen] = useState(false);
|
const [saveModalOpen, setSaveModalOpen] = useState(false);
|
||||||
const [loginPromptOpen, setLoginPromptOpen] = useState(false);
|
const [loginPromptOpen, setLoginPromptOpen] = useState(false);
|
||||||
|
const [showStarBanner, setShowStarBanner] = useState(false);
|
||||||
|
|
||||||
|
// ── GitHub star prompt (show once: 2nd visit OR after 3 min) ──────────────
|
||||||
|
useEffect(() => {
|
||||||
|
const STAR_KEY = 'velxio_star_prompted';
|
||||||
|
const VISITS_KEY = 'velxio_editor_visits';
|
||||||
|
const FIRST_VISIT_KEY = 'velxio_editor_first_visit';
|
||||||
|
const THREE_MIN = 3 * 60 * 1000;
|
||||||
|
|
||||||
|
if (localStorage.getItem(STAR_KEY)) return;
|
||||||
|
|
||||||
|
// Increment visit counter
|
||||||
|
const visits = parseInt(localStorage.getItem(VISITS_KEY) ?? '0', 10) + 1;
|
||||||
|
localStorage.setItem(VISITS_KEY, String(visits));
|
||||||
|
|
||||||
|
// Record timestamp of first visit
|
||||||
|
if (!localStorage.getItem(FIRST_VISIT_KEY)) {
|
||||||
|
localStorage.setItem(FIRST_VISIT_KEY, String(Date.now()));
|
||||||
|
}
|
||||||
|
const firstVisit = parseInt(localStorage.getItem(FIRST_VISIT_KEY)!, 10);
|
||||||
|
|
||||||
|
// Show immediately on second+ visit
|
||||||
|
if (visits >= 2) {
|
||||||
|
setShowStarBanner(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise schedule after the 3-minute mark
|
||||||
|
const elapsed = Date.now() - firstVisit;
|
||||||
|
const delay = Math.max(0, THREE_MIN - elapsed);
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (!localStorage.getItem(STAR_KEY)) setShowStarBanner(true);
|
||||||
|
}, delay);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDismissStarBanner = () => {
|
||||||
|
localStorage.setItem('velxio_star_prompted', '1');
|
||||||
|
setShowStarBanner(false);
|
||||||
|
};
|
||||||
const [explorerOpen, setExplorerOpen] = useState(true);
|
const [explorerOpen, setExplorerOpen] = useState(true);
|
||||||
const [explorerWidth, setExplorerWidth] = useState(EXPLORER_DEFAULT);
|
const [explorerWidth, setExplorerWidth] = useState(EXPLORER_DEFAULT);
|
||||||
const [isMobile, setIsMobile] = useState(() => window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`).matches);
|
const [isMobile, setIsMobile] = useState(() => window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`).matches);
|
||||||
|
|
@ -348,6 +389,7 @@ export const EditorPage: React.FC = () => {
|
||||||
|
|
||||||
{saveModalOpen && <SaveProjectModal onClose={() => setSaveModalOpen(false)} />}
|
{saveModalOpen && <SaveProjectModal onClose={() => setSaveModalOpen(false)} />}
|
||||||
{loginPromptOpen && <LoginPromptModal onClose={() => setLoginPromptOpen(false)} />}
|
{loginPromptOpen && <LoginPromptModal onClose={() => setLoginPromptOpen(false)} />}
|
||||||
|
{showStarBanner && <GitHubStarBanner onClose={handleDismissStarBanner} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -450,11 +450,13 @@ export const LandingPage: React.FC = () => {
|
||||||
<section className="landing-hero">
|
<section className="landing-hero">
|
||||||
<div className="hero-left">
|
<div className="hero-left">
|
||||||
<h1 className="hero-title">
|
<h1 className="hero-title">
|
||||||
Emulate Hardware.<br />
|
Simulate Arduino,<br />
|
||||||
<span className="hero-accent">In your browser.</span>
|
ESP32 & Raspberry Pi.<br />
|
||||||
|
<span className="hero-accent">And 16 more boards in your browser..</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="hero-subtitle">
|
<p className="hero-subtitle">
|
||||||
Write, compile, and simulate 17+ boards across 5 CPU architectures — no hardware, no cloud, no limits. Real emulation running entirely on your machine.
|
Write code, compile, and run on 19 real boards — Arduino Uno, ESP32, ESP32-C3,
|
||||||
|
Raspberry Pi Pico, Raspberry Pi 3, and more. No hardware, no cloud, no limits.
|
||||||
</p>
|
</p>
|
||||||
<div className="hero-ctas">
|
<div className="hero-ctas">
|
||||||
<Link to="/editor" className="cta-primary">
|
<Link to="/editor" className="cta-primary">
|
||||||
|
|
@ -479,7 +481,7 @@ export const LandingPage: React.FC = () => {
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<span className="section-label">Supported Hardware</span>
|
<span className="section-label">Supported Hardware</span>
|
||||||
<h2 className="section-title">Every architecture.<br />One tool.</h2>
|
<h2 className="section-title">Every architecture.<br />One tool.</h2>
|
||||||
<p className="section-sub">17 boards across 5 CPU architectures — AVR, ARM, RISC-V, Xtensa, and Linux. All running locally, no cloud needed.</p>
|
<p className="section-sub">19 boards across 5 CPU architectures — AVR8, ARM Cortex-M0+, RISC-V, Xtensa, and Linux. All running locally, no cloud needed.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── AVR8 · avr8js ────────────────────────────────────────── */}
|
{/* ── AVR8 · avr8js ────────────────────────────────────────── */}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue