Add Next.js website with GitHub Pages deployment

- Added complete Next.js single-page website for Vocalinux
- Configured static export with proper GitHub Pages deployment
- Added GitHub Actions workflow for automated deployment to gh-pages branch
- Configured custom domain (vocalinux.com) with CNAME file
- Added comprehensive build script with npm support
- Responsive design with dark/light theme toggle
- Modern UI components with Tailwind CSS and shadcn/ui
- Proper SEO metadata and accessibility features
This commit is contained in:
jatinkrmalik
2025-05-25 21:11:17 -05:00
parent 27471242c5
commit ec68dd90f0
26 changed files with 2592 additions and 5 deletions
+109
View File
@@ -0,0 +1,109 @@
"use client";
import React, { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Mic } from "lucide-react";
interface DictationOverlayProps {
isActive: boolean;
onAnimationComplete?: () => void;
}
export function DictationOverlay({
isActive,
onAnimationComplete
}: DictationOverlayProps) {
const [text, setText] = useState("");
const fullText = "Hello world";
// Handle text typing animation
useEffect(() => {
if (!isActive) {
setText("");
return;
}
let currentIndex = 0;
const interval = setInterval(() => {
if (currentIndex < fullText.length) {
setText(fullText.substring(0, currentIndex + 1));
currentIndex++;
} else {
clearInterval(interval);
// Wait a bit before triggering the fade out
setTimeout(() => {
if (onAnimationComplete) onAnimationComplete();
}, 800);
}
}, 100); // Speed of typing
return () => clearInterval(interval);
}, [isActive, onAnimationComplete]);
return <AnimatePresence>
{isActive && <div className="fixed inset-0 flex items-center justify-center z-50 bg-background/40 backdrop-blur-sm" data-unique-id="73f29596-a34d-4d7b-9c33-94c994beba56" data-file-name="components/dictation-overlay.tsx">
<div className="flex flex-col items-center" data-unique-id="a63e27a7-6607-4e2f-ac18-22f36aa5177c" data-file-name="components/dictation-overlay.tsx" data-dynamic-text="true">
{/* Microphone Icon */}
<motion.div initial={{
opacity: 0,
scale: 0.5
}} animate={{
opacity: 1,
scale: 1
}} exit={{
opacity: 0,
scale: 0.5
}} transition={{
duration: 0.5
}} className="relative mb-6" data-unique-id="a51981e6-39f5-44b0-b5b8-badaeff800ec" data-file-name="components/dictation-overlay.tsx" data-dynamic-text="true">
<motion.div animate={{
scale: [1, 1.2, 1]
}} transition={{
repeat: Infinity,
duration: 1.5
}} className="bg-primary/10 p-6 rounded-full" data-unique-id="475ec395-9c1e-4800-af6f-40ce7ee6e266" data-file-name="components/dictation-overlay.tsx">
<Mic className="h-12 w-12 text-primary" />
</motion.div>
{/* Ripple effect */}
<motion.div className="absolute inset-0 rounded-full border-2 border-primary" initial={{
opacity: 0.6,
scale: 1
}} animate={{
opacity: 0,
scale: 1.8
}} transition={{
repeat: Infinity,
duration: 1.5,
ease: "easeOut"
}} data-unique-id="f5b542bd-f5bb-4809-af37-2a72a44184e1" data-file-name="components/dictation-overlay.tsx" />
</motion.div>
{/* Text Box */}
<motion.div initial={{
opacity: 0,
y: 10
}} animate={{
opacity: 1,
y: 0
}} exit={{
opacity: 0,
y: 10
}} transition={{
delay: 0.3,
duration: 0.5
}} className="bg-white dark:bg-zinc-800 p-4 rounded-lg shadow-lg min-w-[300px] border border-border" data-unique-id="99680bc6-f0b8-4ef7-ac31-f6ea7035ecdf" data-file-name="components/dictation-overlay.tsx">
<div className="flex items-center gap-2 mb-2" data-unique-id="675b73f9-04de-4b87-be56-4fae6927b795" data-file-name="components/dictation-overlay.tsx">
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse" data-unique-id="d99993f3-871f-4c25-805d-b15e7ae14fa5" data-file-name="components/dictation-overlay.tsx"></div>
<span className="text-sm font-medium" data-unique-id="29e50d52-8aa2-46e1-b2a4-bd91ab9874f0" data-file-name="components/dictation-overlay.tsx"><span className="editable-text" data-unique-id="c833a664-84bf-4bc8-b23c-ff2f384bf175" data-file-name="components/dictation-overlay.tsx">Transcribing...</span></span>
</div>
<div className="min-h-[24px]" data-unique-id="81d6cdf1-a7ff-4d8c-9f15-1949329685bb" data-file-name="components/dictation-overlay.tsx" data-dynamic-text="true">
{text}
<motion.span animate={{
opacity: [1, 0]
}} transition={{
repeat: Infinity,
duration: 0.8
}} className="inline-block w-2 h-4 bg-primary ml-0.5" data-unique-id="d10f4ecc-fbc2-4201-96a1-3ffb9dd816db" data-file-name="components/dictation-overlay.tsx" />
</div>
</motion.div>
</div>
</div>}
</AnimatePresence>;
}
+11
View File
@@ -0,0 +1,11 @@
"use client";
import React from "react";
export const VocalinuxLogo: React.FC<React.SVGProps<SVGSVGElement>> = props => {
return <svg width="36" height="36" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg" {...props} data-unique-id="c262995b-7d9d-449a-8328-e57966ecfafa" data-file-name="components/icons.tsx">
<path d="M243.33 120.92c-58.29 1.8-107.88 51.39-107.88 110.12v100.24c0 20.87 5.16 39.95 14.02 57.07l-44.12 44.12c-3.27 3.27-3.27 8.58 0 11.86 1.64 1.64 3.79 2.46 5.93 2.46 2.14 0 4.29-.82 5.93-2.46l43.39-43.39a107.388 107.388 0 0074.73 30.13h.01c20.94 0 40.83-6.05 57.99-16.52 23.86-14.53 42.22-37.58 49.7-64.48.8-2.89-1.18-5.8-4.17-6.14-2.99-.33-5.57 1.79-5.75 4.8-2.91 48.11-43.05 85.82-91.5 85.82h-.76c-50.83-1.57-93.56-42.09-93.56-93.67V231.04c0-51.11 41.57-93.67 92.68-93.67 46.36 0 86.43 35.37 93.54 80.62.41 2.64 2.69 4.56 5.38 4.5 2.67-.06 4.84-2.1 5.13-4.75 7.65-71.11-46.43-98.62-99.69-96.82z" fill="currentColor" />
<path d="M299.33 256.96c0-2.36-1.91-4.27-4.27-4.27s-4.27 1.91-4.27 4.27v62.09c0 2.36 1.91 4.27 4.27 4.27s4.27-1.91 4.27-4.27v-62.09z" fill="currentColor" />
<path d="M361.42 164.01v-17.06c0-2.36-1.91-4.27-4.27-4.27s-4.27 1.91-4.27 4.27v17.06c0 2.36 1.91 4.27 4.27 4.27s4.27-1.91 4.27-4.27zM336.62 164.01v-17.06c0-2.36-1.91-4.27-4.27-4.27s-4.27 1.91-4.27 4.27v17.06c0 2.36 1.91 4.27 4.27 4.27s4.27-1.91 4.27-4.27zM311.82 164.01v-17.06c0-2.36-1.91-4.27-4.27-4.27s-4.27 1.91-4.27 4.27v17.06c0 2.36 1.91 4.27 4.27 4.27s4.27-1.91 4.27-4.27z" fill="currentColor" />
<path d="M404.61 164.01c0-28.19-11.08-38.24-11.08-38.24-5.54-5.54-12.75-8.31-20-8.31s-14.46 2.77-20 8.31l-7.65 7.65-7.65-7.65c-11.04-11.04-28.96-11.04-40 0l-7.65 7.65-7.65-7.65c-11.04-11.04-28.96-11.04-40 0-11.08 11.08-11.08 28.92 0 40l47.65 47.57v44.62c0 14.15 11.51 25.65 25.65 25.65 14.15 0 25.65-11.51 25.65-25.65v-44.62l47.65-47.57c5.54-5.54 8.31-12.75 8.31-20s-2.77-14.46-8.31-20c-1.67-1.67-1.67-4.38 0-6.04 10.88 4.95 20.73 13.84 20.73 24.27zm-80.82 84.97v19.07c0 9.44-7.68 17.11-17.11 17.11s-17.11-7.67-17.11-17.11v-19.07l17.11 17.11 17.11-17.11zm24.27-48.27l-41.38 41.38L265.3 200.7c-7.37-7.37-7.37-19.32 0-26.69 7.37-7.37 19.32-7.37 26.69 0l14.69 14.69 14.69-14.69c3.56-3.56 8.3-5.52 13.34-5.52s9.78 1.96 13.35 5.52a4.233 4.233 0 006.04 0c3.56-3.56 8.3-5.52 13.34-5.52s9.78 1.96 13.34 5.52c7.37 7.37 7.37 19.32 0 26.69z" fill="currentColor" />
</svg>;
};
+18
View File
@@ -0,0 +1,18 @@
import React from "react";
import { VocalinuxLogo } from "./icons";
import { cn } from "@/lib/utils";
interface LogoProps {
className?: string;
iconClassName?: string;
textClassName?: string;
}
export function Logo({
className,
iconClassName,
textClassName
}: LogoProps) {
return <div className={cn("flex items-center gap-2", className)} data-unique-id="a1b7626e-adb6-498a-97dc-21507a6c6e22" data-file-name="components/logo.tsx">
<VocalinuxLogo className={cn("h-8 w-8 text-primary", iconClassName)} />
<span className={cn("font-bold text-xl", textClassName)} data-unique-id="c9a54c54-3c8e-4667-a3c1-ea265d7df166" data-file-name="components/logo.tsx"><span className="editable-text" data-unique-id="73a6ea30-8bd2-4f6c-9fe3-048638d1848b" data-file-name="components/logo.tsx">Vocalinux</span></span>
</div>;
}
+23
View File
@@ -0,0 +1,23 @@
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import React from "react";
type Attribute = "class" | "data-theme" | "data-mode";
// Define the props type with correct attribute type
interface ThemeProviderProps {
children: React.ReactNode;
attribute?: Attribute | Attribute[];
defaultTheme?: string;
enableSystem?: boolean;
disableTransitionOnChange?: boolean;
storageKey?: string;
forcedTheme?: string;
themes?: string[];
}
export function ThemeProvider({
children,
...props
}: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
+29
View File
@@ -0,0 +1,29 @@
"use client";
import { useEffect, useState } from "react";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { motion } from "framer-motion";
export function ThemeToggle() {
const [mounted, setMounted] = useState(false);
const {
theme,
setTheme
} = useTheme();
// Wait for component to be mounted to avoid hydration mismatch
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return <div className="w-9 h-9 bg-muted rounded-md flex items-center justify-center" data-unique-id="4ef36b6f-5b83-4a67-a991-4f70aea4ec19" data-file-name="components/theme-toggle.tsx">
<span className="sr-only" data-unique-id="9dc304c5-60b7-4f04-9561-9cf2c2a58e09" data-file-name="components/theme-toggle.tsx"><span className="editable-text" data-unique-id="97b0ebb5-1aad-4b0e-b031-21053ba12dc0" data-file-name="components/theme-toggle.tsx">Loading theme toggle</span></span>
</div>;
}
return <motion.button whileTap={{
scale: 0.95
}} onClick={() => setTheme(theme === "dark" ? "light" : "dark")} className="w-8 h-8 sm:w-9 sm:h-9 bg-muted hover:bg-muted/80 rounded-md flex items-center justify-center transition-all" aria-label="Toggle theme" data-unique-id="5f422091-838a-431f-9e75-917f703ab13c" data-file-name="components/theme-toggle.tsx" data-dynamic-text="true">
{theme === "dark" ? <Sun className="h-4 w-4 sm:h-[1.2rem] sm:w-[1.2rem] text-yellow-400" /> : <Moon className="h-4 w-4 sm:h-[1.2rem] sm:w-[1.2rem] text-slate-700" />}
<span className="sr-only" data-unique-id="b525d238-cd43-45a3-9db2-cdc8027ff1eb" data-file-name="components/theme-toggle.tsx"><span className="editable-text" data-unique-id="4ba377eb-21c8-4b06-ac04-8faa338729a7" data-file-name="components/theme-toggle.tsx">Toggle theme</span></span>
</motion.button>;
}
+31
View File
@@ -0,0 +1,31 @@
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'outline' | 'ghost' | 'link';
size?: 'default' | 'sm' | 'lg';
className?: string;
asChild?: boolean;
children?: React.ReactNode;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({
className,
variant = "default",
size = "default",
asChild = false,
...props
}, ref) => {
const Comp = asChild ? "span" : "button";
return <Comp className={cn("inline-flex items-center justify-center rounded-lg font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", {
"bg-primary text-primary-foreground hover:bg-primary/90": variant === "default",
"border border-input bg-background hover:bg-accent hover:text-accent-foreground": variant === "outline",
"hover:bg-accent hover:text-accent-foreground": variant === "ghost",
"text-primary underline-offset-4 hover:underline": variant === "link",
"h-10 px-4 py-2": size === "default",
"h-8 rounded-md px-3 text-xs": size === "sm",
"h-12 rounded-md px-6": size === "lg"
}, className)} ref={ref} {...props} />;
});
Button.displayName = "Button";
export { Button };