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:
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user