diff --git a/README.md b/README.md index 49ea9e2..dcd352a 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,18 @@ Contributions are welcome! Here are some ways you can contribute: - Enhance SVG designs - Add animations or effects +## 🙌 Contributors + +Here is a list of everyone who has contributed to the Octocanvas! +- [thelonewolf39](https://github.com/thelonewolf39) +- [MV Karan](https://github.com/mvkaran) +- [Jessica Deen](https://github.com/jldeen) +- [Damian Brady](https://github.com/Damovisa) +- [Mike Perrotti](https://github.com/mperrotti) +- [Zack Koppert](https://github.com/zkoppert) +- [Katie Langerman](https://github.com/langermank) + + ## License This project is licensed under the terms of the MIT open source license. Please refer to the [LICENSE](./LICENSE) file for the full terms. diff --git a/astro.config.mjs b/astro.config.mjs index edb424c..dba8b69 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -22,4 +22,9 @@ export default defineConfig({ tailwind(), preact({ compat: true }) ], + + // Disable dev toolbar to fix axobject-query error + devToolbar: { + enabled: false + }, }); diff --git a/public/backgrounds/images.png b/public/backgrounds/images.png new file mode 100644 index 0000000..1a6bb49 Binary files /dev/null and b/public/backgrounds/images.png differ diff --git a/public/backgrounds/wallpaper_footer_4KUHD_16_9.webp b/public/backgrounds/wallpaper_footer_4KUHD_16_9.webp new file mode 100644 index 0000000..303f0cf Binary files /dev/null and b/public/backgrounds/wallpaper_footer_4KUHD_16_9.webp differ diff --git a/src/components/GitHubWallpaperApp.module.css b/src/components/GitHubWallpaperApp.module.css index 913cd93..f66eb23 100644 --- a/src/components/GitHubWallpaperApp.module.css +++ b/src/components/GitHubWallpaperApp.module.css @@ -95,6 +95,12 @@ border-top: none; /* Connect visually with tabs */ } +/* File upload label styling */ +.FileUploadLabel { + display: block; + cursor: pointer; +} + @media (min-width: 640px) { .PreviewArea { padding: var(--space-2xl); diff --git a/src/components/GitHubWallpaperApp.tsx b/src/components/GitHubWallpaperApp.tsx index d2f119d..54d6ec3 100644 --- a/src/components/GitHubWallpaperApp.tsx +++ b/src/components/GitHubWallpaperApp.tsx @@ -59,11 +59,14 @@ const getCleanUsername = (username: string) => // Background theme configurations for wallpaper const BACKGROUND_THEMES = { - "github-universe-green": { label: "GitHub Universe Green" }, - "github-universe-blue": { label: "GitHub Universe Blue" }, - "universe-octocanvas": { label: "Universe Octocanvas" }, - "github-dark": { label: "GitHub Dark" }, -}; + "github-universe-green": { label: "GitHub Universe Green", type: "gradient" }, + "github-universe-blue": { label: "GitHub Universe Blue", type: "gradient" }, + "universe-octocanvas": { label: "Universe Octocanvas", type: "gradient" }, + "github-dark": { label: "GitHub Dark", type: "gradient" }, + "bg-images": { label: "Background Image 1", type: "image", imagePath: "/backgrounds/images.png" }, + "bg-wallpaper": { label: "Background Image 2", type: "image", imagePath: "/backgrounds/wallpaper_footer_4KUHD_16_9.webp" }, + "custom": { label: "Custom Upload", type: "image" }, +} as const; // Avatar filter options const AVATAR_FILTERS = { @@ -86,6 +89,7 @@ export default function GitHubWallpaperApp() { >("github-universe-green"); const [wallpaperAvatarFilter, setWallpaperAvatarFilter] = useState("grayscale"); + const [customBackgroundUrl, setCustomBackgroundUrl] = useState(""); // Devemon Card form controls const [devemonAvatarFilter, setDevemonAvatarFilter] = useState< @@ -225,6 +229,24 @@ export default function GitHubWallpaperApp() { }; }; + /** + * Handle custom background image upload + */ + const handleBackgroundUpload = (e: JSX.TargetedEvent) => { + const input = e.target as HTMLInputElement; + const file = input.files?.[0]; + + if (file && file.type.startsWith('image/')) { + const reader = new FileReader(); + reader.onload = (event) => { + const dataUrl = event.target?.result as string; + setCustomBackgroundUrl(dataUrl); + setWallpaperTheme("custom"); + }; + reader.readAsDataURL(file); + } + }; + /** * Fetch GitHub user data from public API * No authentication required for public profiles @@ -405,6 +427,37 @@ export default function GitHubWallpaperApp() { )} + + {wallpaperTheme === "custom" && ( +
+ + + +

+ {customBackgroundUrl + ? "Custom background uploaded ✓" + : "Upload a custom image for your wallpaper background"} +

+
+ )} )} @@ -551,6 +604,7 @@ export default function GitHubWallpaperApp() { user={userData} selectedTheme={wallpaperTheme} avatarFilter={wallpaperAvatarFilter} + customBackgroundUrl={customBackgroundUrl} /> diff --git a/src/components/WallpaperGenerator.tsx b/src/components/WallpaperGenerator.tsx index f0f92e9..e60c4df 100644 --- a/src/components/WallpaperGenerator.tsx +++ b/src/components/WallpaperGenerator.tsx @@ -18,6 +18,7 @@ interface WallpaperGeneratorProps { user: GitHubUser; selectedTheme: keyof typeof BACKGROUND_THEMES; avatarFilter: keyof typeof AVATAR_FILTERS; + customBackgroundUrl?: string; } export interface WallpaperGeneratorRef { @@ -41,6 +42,7 @@ type SizeKey = keyof typeof SIZES; const BACKGROUND_THEMES = { "github-universe-green": { label: "GitHub Universe Green", + type: "gradient", gradient: { // Figma: linear-gradient(90deg, #BFFFD1 0%, #5FED83 100%) stops: [ @@ -55,6 +57,7 @@ const BACKGROUND_THEMES = { }, "github-universe-blue": { label: "GitHub Universe Blue", + type: "gradient", gradient: { // Figma: linear-gradient(90deg, #DEFEFA 8.15%, #3094FF 74.77%, #0527FC 119.18%) // SVG adaptation: Start earlier with cyan, hold blue longer, push deep blue to edge @@ -70,6 +73,7 @@ const BACKGROUND_THEMES = { }, "universe-octocanvas": { label: "Universe Octocanvas", + type: "gradient", gradient: { // Solid color as a single-stop gradient stops: [ @@ -85,6 +89,7 @@ const BACKGROUND_THEMES = { }, "github-dark": { label: "GitHub Dark", + type: "gradient", gradient: { stops: [ { offset: "0%", color: "#909692" }, @@ -97,7 +102,21 @@ const BACKGROUND_THEMES = { end: "#0D1117", }, }, -}; + "bg-images": { + label: "Background Image 1", + type: "image", + imagePath: "/backgrounds/images.png", + }, + "bg-wallpaper": { + label: "Background Image 2", + type: "image", + imagePath: "/backgrounds/wallpaper_footer_4KUHD_16_9.webp", + }, + "custom": { + label: "Custom Upload", + type: "image", + }, +} as const; type BackgroundThemeKey = keyof typeof BACKGROUND_THEMES; @@ -112,13 +131,21 @@ type AvatarFilterKey = keyof typeof AVATAR_FILTERS; const WallpaperGenerator = forwardRef< WallpaperGeneratorRef, WallpaperGeneratorProps ->(({ user, selectedTheme, avatarFilter }, ref) => { +>(({ user, selectedTheme, avatarFilter, customBackgroundUrl = "" }, ref) => { const svgRef = useRef(null); const [avatarBase64, setAvatarBase64] = useState(""); const avatarBase64Ref = useRef(""); // Ref to hold current avatar value for imperative handle const [isMobile, setIsMobile] = useState(false); const [downloadingSize, setDownloadingSize] = useState(null); + // Store custom background URL in ref for download + const customBackgroundRef = useRef(""); + + // Update ref when customBackgroundUrl changes + useEffect(() => { + customBackgroundRef.current = customBackgroundUrl; + }, [customBackgroundUrl]); + // Shared social media post text const SHARE_POST_TEXT = "Just created my custom GitHub Universe wallpaper using Octocanvas from #GitHubUniverse 🎨"; @@ -390,9 +417,20 @@ const WallpaperGenerator = forwardRef< // Get selected theme colors const theme = BACKGROUND_THEMES[selectedTheme]; - // Set text colors based on theme (white for GitHub Dark, black for others) - const textColor = selectedTheme === "github-dark" ? "#FFFFFF" : "#000000"; - const handleColor = selectedTheme === "github-dark" ? "#FFFFFF" : "#4F4F4F"; + // Check if theme uses an image background + const isImageBackground = theme.type === "image"; + let backgroundImageUrl = ""; + if (isImageBackground) { + if (selectedTheme === "custom") { + backgroundImageUrl = customBackgroundUrl; + } else if ("imagePath" in theme) { + backgroundImageUrl = theme.imagePath; + } + } + + // Set text colors based on theme (white for GitHub Dark and image backgrounds, black for others) + const textColor = selectedTheme === "github-dark" || isImageBackground ? "#FFFFFF" : "#000000"; + const handleColor = selectedTheme === "github-dark" || isImageBackground ? "#FFFFFF" : "#4F4F4F"; // Set avatar border color based on theme const avatarBorderColor = @@ -514,17 +552,20 @@ const WallpaperGenerator = forwardRef< const cardAvatarY = cardY + cardHeight * 0.25; return ` - + - + ${!isImageBackground + ? ` ${theme.gradient.stops - .map( - (stop: any) => - `` - ) - .join("\n ")} - + .map( + (stop: any) => + `` + ) + .join("\n ")} + ` + : "" + } @@ -532,7 +573,7 @@ const WallpaperGenerator = forwardRef< }" /> - + ${!isStatic ? ` ` : "" } - - - + + + ${isImageBackground + ? `` + : `` + } ${generateDecorativeElements(width, height, scale)} @@ -701,25 +745,26 @@ const WallpaperGenerator = forwardRef< // Default theme rendering (existing themes) return ` - + - + ${!isImageBackground + ? ` ${theme.gradient.stops - .map( - (stop: any) => - `` - ) - .join("\n ")} + .map( + (stop: any) => + `` + ) + .join("\n ")} - - + ` + : ""} @@ -727,7 +772,7 @@ const WallpaperGenerator = forwardRef< }" /> - + ${!isStatic ? ` ` : "" } - - - - + + + ${isImageBackground + ? ` + + ` + : ` + `} => { const size = SIZES[sizeKey]; - // Ensure avatar is loaded as base64 (use ref to get current value) + // Ensure avatar is loaded as base64 const currentAvatarBase64 = avatarBase64Ref.current; if (!currentAvatarBase64) { throw new Error("Avatar not loaded yet"); } - const svgString = generateSVG(size.width, size.height, true); // true = static version + // Get current custom background URL from ref + const currentCustomBg = customBackgroundRef.current; - // Create canvas for PNG conversion - const canvas = document.createElement("canvas"); - canvas.width = size.width; - canvas.height = size.height; - const ctx = canvas.getContext("2d", { willReadFrequently: false }); - - if (!ctx) { - throw new Error("Failed to get canvas context"); + // Check if we have a background image + const theme = BACKGROUND_THEMES[selectedTheme]; + const isImageBackground = theme.type === "image"; + let backgroundImageUrl = ""; + + if (isImageBackground) { + if (selectedTheme === "custom" && currentCustomBg) { + backgroundImageUrl = currentCustomBg; + } else if ("imagePath" in theme) { + backgroundImageUrl = theme.imagePath; + } } - // Encode SVG string properly for data URL - const encodedSvg = encodeURIComponent(svgString) - .replace(/'/g, "%27") - .replace(/"/g, "%22"); - - const dataUrl = `data:image/svg+xml,${encodedSvg}`; + // Create a container div to render everything + const container = document.createElement("div"); + container.style.position = "absolute"; + container.style.left = "-9999px"; + container.style.top = "0"; + container.style.width = `${size.width}px`; + container.style.height = `${size.height}px`; + container.style.overflow = "hidden"; + + // If we have a background image, set it as CSS background + if (backgroundImageUrl) { + container.style.backgroundImage = `url('${backgroundImageUrl}')`; + container.style.backgroundSize = "cover"; + container.style.backgroundPosition = "center"; + container.style.backgroundRepeat = "no-repeat"; + } - // Create image and wait for it to load - const img = new Image(); + // Generate SVG content (without background) + const svgString = generateSVG(size.width, size.height, true); + container.innerHTML = svgString; - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error("Image load timeout after 10 seconds")); - }, 10000); // 10 second timeout + document.body.appendChild(container); - img.onload = () => { - clearTimeout(timeout); - resolve(); - }; + try { + // If we have a background image, add the dark overlay as a child div + if (backgroundImageUrl) { + const overlay = document.createElement("div"); + overlay.style.position = "absolute"; + overlay.style.top = "0"; + overlay.style.left = "0"; + overlay.style.width = "100%"; + overlay.style.height = "100%"; + overlay.style.backgroundColor = "rgba(0, 0, 0, 0.3)"; + overlay.style.pointerEvents = "none"; + container.insertBefore(overlay, container.firstChild); + } - img.onerror = (e) => { - clearTimeout(timeout); - console.error("Failed to load SVG image:", e); - reject(new Error("Failed to load SVG image")); - }; + // Remove the background image element from SVG if present + if (isImageBackground) { + const svgElement = container.querySelector('svg'); + if (svgElement) { + const bgImages = svgElement.querySelectorAll('image'); + bgImages.forEach((img) => { + const href = img.getAttribute('href') || img.getAttribute('xlink:href'); + if (href === backgroundImageUrl || (currentCustomBg && href === currentCustomBg)) { + img.remove(); + } + }); + // Remove overlay rect from SVG (we're using CSS overlay now) + const overlayRects = svgElement.querySelectorAll('rect[opacity="0.3"]'); + overlayRects.forEach(rect => rect.remove()); + } + } - img.src = dataUrl; - }); - - // Draw the SVG image onto canvas - ctx.drawImage(img, 0, 0); - - // Convert canvas to PNG blob - const blob = await new Promise((resolve, reject) => { - canvas.toBlob( - (b) => { - if (b) resolve(b); - else reject(new Error("Failed to create blob from canvas")); - }, - "image/png", - 1.0 - ); - }); + // Wait for images to load + await new Promise(resolve => setTimeout(resolve, 500)); + + // Use html2canvas to render the entire container + const html2canvas = (await import("html2canvas")).default; + const canvas = await html2canvas(container, { + backgroundColor: null, + scale: 1, + width: size.width, + height: size.height, + useCORS: true, + allowTaint: true, + logging: false, + }); + + // Convert canvas to PNG blob + const blob = await new Promise((resolve, reject) => { + canvas.toBlob( + (b) => { + if (b) resolve(b); + else reject(new Error("Failed to create blob from canvas")); + }, + "image/png", + 1.0 + ); + }); - return blob; + return blob; + } finally { + // Clean up + document.body.removeChild(container); + } }; /** diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 59aa945..1a8730c 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -40,6 +40,9 @@ export interface ButtonProps { /** Show icon on right */ icon?: JSX.Element; + + /** Render as different element (for use with labels) */ + as?: "button" | "span"; } export function Button({ @@ -52,6 +55,7 @@ export function Button({ fullWidth = false, className = "", icon, + as = "button", }: ButtonProps) { // Combine CSS module classes const buttonClasses = [ @@ -64,6 +68,24 @@ export function Button({ .filter(Boolean) .join(" "); + const content = ( + <> + {/* Button text with GitHub Universe styling */} + {children} + + {/* Icon if provided */} + {icon && {icon as any}} + + ); + + if (as === "span") { + return ( + + {content} + + ); + } + return ( ); } diff --git a/src/components/ui/Icon.tsx b/src/components/ui/Icon.tsx index 27d077f..2ef0e5f 100644 --- a/src/components/ui/Icon.tsx +++ b/src/components/ui/Icon.tsx @@ -313,6 +313,22 @@ export function Icon({ ); + case 'upload': + return ( + + + + + ); + default: // Fallback for unknown icons return ( diff --git a/src/pages/index.astro b/src/pages/index.astro index 829953d..6364d3f 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -3,14 +3,14 @@ import GitHubWallpaperApp from "../components/GitHubWallpaperApp"; import "../styles/global.css"; -// Base sub folder from environment variable -const base = import.meta.env.BASE_URL || "/octocanvas"; +// Base sub folder from environment variable (must match astro.config.mjs) +const base = import.meta.env.BASE_URL || "/"; --- - + OCTOCANVAS - GitHub Universe Collectibles @@ -120,4 +120,4 @@ const base = import.meta.env.BASE_URL || "/octocanvas"; - + \ No newline at end of file