├── .eslintrc.json ├── app ├── favicon.ico ├── setup │ └── page.tsx ├── layout.tsx ├── globals.css └── page.tsx ├── postcss.config.js ├── lib └── utils.ts ├── next.config.js ├── types.d.ts ├── components ├── ui │ ├── skeleton.tsx │ ├── toaster.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── tabs.tsx │ ├── card.tsx │ ├── use-toast.ts │ ├── select.tsx │ ├── sheet.tsx │ ├── toast.tsx │ └── dropdown-menu.tsx ├── theme-provider.tsx ├── mode-toggle.tsx ├── navbar.tsx ├── code.tsx └── dropzone.tsx ├── util ├── bytes-to-size.ts ├── load-ffmpeg.ts ├── file-to-icon.tsx ├── compress-file-name.ts └── convert.ts ├── components.json ├── .gitignore ├── tailwind.config.ts ├── public ├── vercel.svg ├── logo.svg └── next.svg ├── tsconfig.json ├── package.json ├── README.md └── tailwind.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neon-20/FileMagnet-File-Converter/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ['api.producthunt.com'], 5 | }, 6 | } 7 | 8 | module.exports = nextConfig 9 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | export type Actions = { 2 | file:any; 3 | file_name:any; 4 | file_size: number; 5 | from: string; 6 | to: String | null; 7 | file_type: string; 8 | is_converting?: boolean; 9 | is_converted?: boolean; 10 | is_error?: boolean; 11 | url?: any; 12 | output?: any; 13 | } -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /util/bytes-to-size.ts: -------------------------------------------------------------------------------- 1 | export default function bytesToSize(bytes: number): String { 2 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; 3 | 4 | if (bytes === 0) return '0 Byte'; 5 | 6 | const i = Math.floor(Math.log(bytes) / Math.log(1024)); 7 | const size = (bytes / Math.pow(1024, i)).toFixed(2); 8 | 9 | return `${size} ${sizes[i]}`; 10 | } -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ThemeProvider as NextThemesProvider } from "next-themes" 5 | import { type ThemeProviderProps } from "next-themes/dist/types" 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children} 9 | } 10 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /util/load-ffmpeg.ts: -------------------------------------------------------------------------------- 1 | import { FFmpeg } from "@ffmpeg/ffmpeg"; 2 | import { toBlobURL } from '@ffmpeg/util'; 3 | 4 | export default async function loadFfmpeg(): Promise{ 5 | const ffmpeg = new FFmpeg(); 6 | const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.2/dist/umd'; 7 | await ffmpeg.load({ 8 | coreURL:await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'), 9 | wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'), 10 | 11 | }); 12 | return ffmpeg; 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | dist 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /app/setup/page.tsx: -------------------------------------------------------------------------------- 1 | import Code from "@/components/code"; 2 | 3 | const SetupPage = () => { 4 | return ( 5 |
6 |
7 |

8 | Setup this project easily 🤓 9 |

10 |
11 | 12 |
13 |
14 |
15 | ); 16 | } 17 | export default SetupPage; -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /util/file-to-icon.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BsFillImageFill, 3 | BsFileEarmarkTextFill, 4 | BsFillCameraVideoFill, 5 | } from 'react-icons/bs'; 6 | import { FaFileAudio } from 'react-icons/fa'; 7 | import { AiFillFile } from 'react-icons/ai'; 8 | import { PiSpeakerSimpleHighFill } from 'react-icons/pi'; 9 | 10 | export default function fileToIcon(file_type: any): any { 11 | if(file_type.includes('video')) return ; 12 | if(file_type.includes('audio')) return ; 13 | if(file_type.includes('text')) return ; 14 | if(file_type.includes('image')) return ; 15 | else return ; 16 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "outDir": "./dist", 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ], 26 | "paths": { 27 | "@/*": [ 28 | "./*" 29 | ] 30 | }, 31 | "forceConsistentCasingInFileNames": true 32 | }, 33 | "include": [ 34 | "next-env.d.ts", 35 | "**/*.ts", 36 | "**/*.tsx", 37 | ".next/types/**/*.ts" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | } from "@/components/ui/toast" 11 | import { useToast } from "@/components/ui/use-toast" 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast() 15 | 16 | return ( 17 | 18 | {toasts.map(function ({ id, title, description, action, ...props }) { 19 | return ( 20 | 21 |
22 | {title && {title}} 23 | {description && ( 24 | {description} 25 | )} 26 |
27 | {action} 28 | 29 |
30 | ) 31 | })} 32 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /util/compress-file-name.ts: -------------------------------------------------------------------------------- 1 | export default function compressFileName(fileName: any): string { 2 | const maxSubstrLength = 18; 3 | 4 | if (fileName.length > maxSubstrLength) { 5 | const fileNameWithoutExtension = fileName.split('.').slice(0, -1).join('.'); 6 | 7 | // Extract the extension from the fileName 8 | const fileExtension = fileName.split('.').pop(); 9 | 10 | // Calculate the length of characters to keep in the middle 11 | const charsToKeep = 12 | maxSubstrLength - (fileNameWithoutExtension.length + fileExtension.length + 3); 13 | 14 | // Create the compressed fileName 15 | const compressedFileName = 16 | fileNameWithoutExtension.substring( 17 | 0, 18 | maxSubstrLength - fileExtension.length - 3, 19 | ) + 20 | '...' + 21 | fileNameWithoutExtension.slice(-charsToKeep) + 22 | '.' + 23 | fileExtension; 24 | 25 | return compressedFileName; 26 | } else { 27 | // If the fileName is shorter than the maximum length, return it as is 28 | return fileName.trim(); 29 | } 30 | } -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import type { Metadata } from 'next' 3 | import { Inter } from 'next/font/google' 4 | import { Toaster } from "@/components/ui/toaster"; 5 | import NavBar from '@/components/navbar' 6 | import { ThemeProvider } from "@/components/theme-provider" 7 | import {ModeToggle} from "@/components/mode-toggle" 8 | 9 | const inter = Inter({ subsets: ['latin'] }) 10 | 11 | export const metadata: Metadata = { 12 | title: 'FileMagnet: A MultiPurpose Free File-Converter for the Web', 13 | description: `Users can come in this application and convert any file 14 | into their desired file format.`, 15 | } 16 | 17 | export default function RootLayout({ 18 | children, 19 | }: { 20 | children: React.ReactNode 21 | }) { 22 | return ( 23 | 24 | 25 | 31 | 32 | 33 |
35 | {children} 36 |
37 |
38 | 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "file-magnet", 3 | "author": "Pranav Rajveer", 4 | "version": "0.1.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@ffmpeg/ffmpeg": "^0.12.6", 14 | "@ffmpeg/util": "^0.12.1", 15 | "@radix-ui/react-dialog": "1.0.4", 16 | "@radix-ui/react-dropdown-menu": "^2.0.6", 17 | "@radix-ui/react-select": "^2.0.0", 18 | "@radix-ui/react-slot": "^1.0.2", 19 | "@radix-ui/react-tabs": "^1.0.4", 20 | "@radix-ui/react-toast": "^1.1.5", 21 | "class-variance-authority": "^0.7.0", 22 | "clsx": "^2.0.0", 23 | "lucide-react": "^0.279.0", 24 | "next": "^13.4.12", 25 | "next-themes": "^0.2.1", 26 | "react": "latest", 27 | "react-dom": "latest", 28 | "react-dropzone": "^14.2.3", 29 | "react-icons": "^4.11.0", 30 | "tailwind-merge": "^1.14.0", 31 | "tailwindcss-animate": "^1.0.7" 32 | }, 33 | "devDependencies": { 34 | "@types/node": "latest", 35 | "@types/react": "latest", 36 | "@types/react-dom": "latest", 37 | "autoprefixer": "latest", 38 | "eslint": "latest", 39 | "eslint-config-next": "latest", 40 | "postcss": "latest", 41 | "tailwindcss": "latest", 42 | "typescript": "latest" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /components/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { Moon, Sun } from "lucide-react" 5 | import { useTheme } from "next-themes" 6 | 7 | import { Button } from "@/components/ui/button" 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from "@/components/ui/dropdown-menu" 14 | 15 | export function ModeToggle() { 16 | const { setTheme } = useTheme() 17 | 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | setTheme("light")}> 29 | Light 30 | 31 | setTheme("dark")}> 32 | Dark 33 | 34 | setTheme("system")}> 35 | System 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body, 7 | :root{ 8 | height: 100%; 9 | } 10 | 11 | @layer base { 12 | :root { 13 | --background: 0 0% 100%; 14 | --foreground: 240 10% 3.9%; 15 | --card: 0 0% 100%; 16 | --card-foreground: 240 10% 3.9%; 17 | --popover: 0 0% 100%; 18 | --popover-foreground: 240 10% 3.9%; 19 | --primary: 240 5.9% 10%; 20 | --primary-foreground: 0 0% 98%; 21 | --secondary: 240 4.8% 95.9%; 22 | --secondary-foreground: 240 5.9% 10%; 23 | --muted: 240 4.8% 95.9%; 24 | --muted-foreground: 240 3.8% 46.1%; 25 | --accent: 240 4.8% 95.9%; 26 | --accent-foreground: 240 5.9% 10%; 27 | --destructive: 0 84.2% 60.2%; 28 | --destructive-foreground: 0 0% 98%; 29 | --border: 240 5.9% 90%; 30 | --input: 240 5.9% 90%; 31 | --ring: 240 5.9% 10%; 32 | --radius: 1rem; 33 | } 34 | 35 | .dark { 36 | --background: 240 10% 3.9%; 37 | --foreground: 0 0% 98%; 38 | --card: 240 10% 3.9%; 39 | --card-foreground: 0 0% 98%; 40 | --popover: 240 10% 3.9%; 41 | --popover-foreground: 0 0% 98%; 42 | --primary: 0 0% 98%; 43 | --primary-foreground: 240 5.9% 10%; 44 | --secondary: 240 3.7% 15.9%; 45 | --secondary-foreground: 0 0% 98%; 46 | --muted: 240 3.7% 15.9%; 47 | --muted-foreground: 240 5% 64.9%; 48 | --accent: 240 3.7% 15.9%; 49 | --accent-foreground: 0 0% 98%; 50 | --destructive: 0 62.8% 30.6%; 51 | --destructive-foreground: 0 0% 98%; 52 | --border: 240 3.7% 15.9%; 53 | --input: 240 3.7% 15.9%; 54 | --ring: 240 4.9% 83.9%; 55 | } 56 | } 57 | 58 | 59 | @layer base { 60 | * { 61 | @apply border-border; 62 | } 63 | body { 64 | @apply bg-background text-foreground; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /util/convert.ts: -------------------------------------------------------------------------------- 1 | import { Actions } from '@/types'; 2 | import { FFmpeg } from '@ffmpeg/ffmpeg'; 3 | import { fetchFile } from '@ffmpeg/util'; 4 | 5 | function getFileExtension(file_name: string) { 6 | const regex = /(?:\.([^.]+))?$/; // Matches the last dot and everything after it 7 | const match = regex.exec(file_name); 8 | if (match && match[1]) { 9 | return match[1]; 10 | } 11 | return ''; // No file extension found 12 | } 13 | 14 | function removeFileExtension(file_name: string) { 15 | const lastDotIndex = file_name.lastIndexOf('.'); 16 | if (lastDotIndex !== -1) { 17 | return file_name.slice(0, lastDotIndex); 18 | } 19 | return file_name; // No file extension found 20 | } 21 | 22 | export default async function convertFile( 23 | ffmpeg: FFmpeg, 24 | action: Actions, 25 | ): Promise { 26 | const { file, to, file_name, file_type } = action; 27 | const input = getFileExtension(file_name); 28 | const output = removeFileExtension(file_name) + '.' + to; 29 | ffmpeg.writeFile(input, await fetchFile(file)); 30 | 31 | // FFMEG COMMANDS 32 | let ffmpeg_cmd: any = []; 33 | // 3gp video 34 | if (to === '3gp') 35 | ffmpeg_cmd = [ 36 | '-i', 37 | input, 38 | '-r', 39 | '20', 40 | '-s', 41 | '352x288', 42 | '-vb', 43 | '400k', 44 | '-acodec', 45 | 'aac', 46 | '-strict', 47 | 'experimental', 48 | '-ac', 49 | '1', 50 | '-ar', 51 | '8000', 52 | '-ab', 53 | '24k', 54 | output, 55 | ]; 56 | else ffmpeg_cmd = ['-i', input, output]; 57 | 58 | // execute cmd 59 | await ffmpeg.exec(ffmpeg_cmd); 60 | 61 | const data = (await ffmpeg.readFile(output)) as any; 62 | const blob = new Blob([data], { type: file_type.split('/')[0] }); 63 | const url = URL.createObjectURL(blob); 64 | return { url, output }; 65 | } -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | // import DropZone from "@/components/dropzone" 2 | 3 | // import Code from "@/components/code"; 4 | import DropZone from "@/components/dropzone"; 5 | import { ModeToggle } from "@/components/mode-toggle"; 6 | import Image from "next/image"; 7 | import Link from "next/link"; 8 | // import Image from "next/image"; 9 | 10 | const Home = () => { 11 | return ( 12 |
13 |
14 |

FileMagnet🚀

15 |

16 | Convert files with ease.. 17 |
18 | {/* */} 19 |
20 | 21 | FileMagnet - FileMagnet is a free file converter, which is backendless 🎯 | Product Hunt 26 | 27 |
28 |
29 |

30 |
31 | {/* Upload box */} 32 | 33 |
34 | ); 35 | } 36 | 37 | export default Home; 38 | 39 | // "FileMagnet - FileMagnet is a free, file converter which is backendless 🎯 | Product Hunt" 40 | // "https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=433044&theme=light" -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Video Demonstration 🎥 3 | https://github.com/Neon-20/FileMagnet-File-Converter/assets/55043383/ce112c67-fdf7-4dd6-bf43-f5d064fe9499 4 | 5 | 6 | # FileMagnet🚀 7 | 8 | FileMagnet is your one-stop solution for converting various file formats, including images, audio, and videos. With FileMagnet, you can easily convert your files to a different format hassle-free.🤝 9 | 10 | ## Getting Started⭐️ 11 | 12 | Follow these simple steps to run FileMagnet locally and start using this versatile application: 13 | 14 | 1. **Clone the Repository** 15 | - Clone the FileMagnet repository to your local machine using Git: 16 | ```shell 17 | git clone https://github.com/your-username/FileMagnet-File-Converter.git 18 | ``` 19 | 20 | 2. **Install Dependencies** 21 | - Navigate to the project directory and install the required dependencies: 22 | ```shell 23 | cd filemagnet 24 | npm install 25 | ``` 26 | 27 | 3. **Run the Application** 28 | - Start the FileMagnet application locally: 29 | ```shell 30 | npm start 31 | ``` 32 | 33 | That's it! You're ready to enjoy FileMagnet and start converting your files to the formats you need. 34 | 35 | ## Features 36 | 37 | FileMagnet offers a range of features, including: 38 | 39 | - **Image Conversion**: Easily convert images between various formats, such as JPEG, PNG, and GIF. 40 | - **Audio Conversion**: Convert audio files to different formats like MP3, WAV, and AAC. 41 | - **Video Conversion**: Seamlessly transform video files into formats like MP4, AVI, and more. 42 | 43 | ## Contributing 44 | 45 | We welcome contributions from the open-source community. Feel free to submit issues, feature requests, or pull requests on our [GitHub repository](https://github.com/your-username/FileMagnet-File-Converter.git). 46 | 47 | ## License 48 | 49 | FileMagnet is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details. 50 | 51 | Thank you for choosing FileMagnet for your file format conversion needs. We hope you find this application helpful and efficient! 52 | 53 | Happy converting! 🚀 54 | -------------------------------------------------------------------------------- /components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TabsPrimitive from "@radix-ui/react-tabs" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Tabs = TabsPrimitive.Root 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )) 23 | TabsList.displayName = TabsPrimitive.List.displayName 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )) 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )) 53 | TabsContent.displayName = TabsPrimitive.Content.displayName 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent } 56 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | './pages/**/*.{ts,tsx}', 6 | './components/**/*.{ts,tsx}', 7 | './app/**/*.{ts,tsx}', 8 | './src/**/*.{ts,tsx}', 9 | ], 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: "2rem", 14 | screens: { 15 | "2xl": "1400px", 16 | }, 17 | }, 18 | extend: { 19 | colors: { 20 | border: "hsl(var(--border))", 21 | input: "hsl(var(--input))", 22 | ring: "hsl(var(--ring))", 23 | background: "hsl(var(--background))", 24 | foreground: "hsl(var(--foreground))", 25 | primary: { 26 | DEFAULT: "hsl(var(--primary))", 27 | foreground: "hsl(var(--primary-foreground))", 28 | }, 29 | secondary: { 30 | DEFAULT: "hsl(var(--secondary))", 31 | foreground: "hsl(var(--secondary-foreground))", 32 | }, 33 | destructive: { 34 | DEFAULT: "hsl(var(--destructive))", 35 | foreground: "hsl(var(--destructive-foreground))", 36 | }, 37 | muted: { 38 | DEFAULT: "hsl(var(--muted))", 39 | foreground: "hsl(var(--muted-foreground))", 40 | }, 41 | accent: { 42 | DEFAULT: "hsl(var(--accent))", 43 | foreground: "hsl(var(--accent-foreground))", 44 | }, 45 | popover: { 46 | DEFAULT: "hsl(var(--popover))", 47 | foreground: "hsl(var(--popover-foreground))", 48 | }, 49 | card: { 50 | DEFAULT: "hsl(var(--card))", 51 | foreground: "hsl(var(--card-foreground))", 52 | }, 53 | }, 54 | borderRadius: { 55 | lg: "var(--radius)", 56 | md: "calc(var(--radius) - 2px)", 57 | sm: "calc(var(--radius) - 4px)", 58 | }, 59 | keyframes: { 60 | "accordion-down": { 61 | from: { height: 0 }, 62 | to: { height: "var(--radix-accordion-content-height)" }, 63 | }, 64 | "accordion-up": { 65 | from: { height: "var(--radix-accordion-content-height)" }, 66 | to: { height: 0 }, 67 | }, 68 | }, 69 | animation: { 70 | "accordion-down": "accordion-down 0.2s ease-out", 71 | "accordion-up": "accordion-up 0.2s ease-out", 72 | }, 73 | }, 74 | }, 75 | plugins: [require("tailwindcss-animate")], 76 | } -------------------------------------------------------------------------------- /components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | import {BsGithub} from 'react-icons/bs'; 5 | import {GrMenu} from 'react-icons/gr'; 6 | 7 | import { 8 | Sheet, 9 | SheetClose, 10 | SheetContent, 11 | SheetDescription, 12 | SheetTitle, 13 | SheetHeader, 14 | SheetTrigger, 15 | SheetFooter 16 | } from "@/components/ui/sheet" 17 | import { ModeToggle } from "@/components/mode-toggle"; 18 | 19 | const NavBar = () => { 20 | return ( 21 | 86 | ); 87 | } 88 | 89 | 90 | export default NavBar; -------------------------------------------------------------------------------- /components/code.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | export default function Code(){ 4 | return( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |

bash

14 |
15 |
16 |

Clone the repo

17 |

$ https://github.com/your-username/FileMagnet-File-Converter.git

18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |

bash

28 |
29 |
30 |

Install the packages

31 |

$ npm i

or

32 |

$ pnpm i

33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |

bash

43 |
44 |
45 |

Run the Application

46 |

$ npm run dev

or

47 |

$ pnpm dev

48 |
🎉 49 |
50 |

Boom, your Project is setup successfully and you are ready to launch 🎉

51 | 79 |
80 | 81 | ) 82 | } -------------------------------------------------------------------------------- /components/ui/use-toast.ts: -------------------------------------------------------------------------------- 1 | // Inspired by react-hot-toast library 2 | import * as React from "react" 3 | 4 | import type { 5 | ToastActionElement, 6 | ToastProps, 7 | } from "@/components/ui/toast" 8 | 9 | const TOAST_LIMIT = 1 10 | const TOAST_REMOVE_DELAY = 1000000 11 | 12 | type ToasterToast = ToastProps & { 13 | id: string 14 | title?: React.ReactNode 15 | description?: React.ReactNode 16 | action?: ToastActionElement 17 | } 18 | 19 | const actionTypes = { 20 | ADD_TOAST: "ADD_TOAST", 21 | UPDATE_TOAST: "UPDATE_TOAST", 22 | DISMISS_TOAST: "DISMISS_TOAST", 23 | REMOVE_TOAST: "REMOVE_TOAST", 24 | } as const 25 | 26 | let count = 0 27 | 28 | function genId() { 29 | count = (count + 1) % Number.MAX_VALUE 30 | return count.toString() 31 | } 32 | 33 | type ActionType = typeof actionTypes 34 | 35 | type Action = 36 | | { 37 | type: ActionType["ADD_TOAST"] 38 | toast: ToasterToast 39 | } 40 | | { 41 | type: ActionType["UPDATE_TOAST"] 42 | toast: Partial 43 | } 44 | | { 45 | type: ActionType["DISMISS_TOAST"] 46 | toastId?: ToasterToast["id"] 47 | } 48 | | { 49 | type: ActionType["REMOVE_TOAST"] 50 | toastId?: ToasterToast["id"] 51 | } 52 | 53 | interface State { 54 | toasts: ToasterToast[] 55 | } 56 | 57 | const toastTimeouts = new Map>() 58 | 59 | const addToRemoveQueue = (toastId: string) => { 60 | if (toastTimeouts.has(toastId)) { 61 | return 62 | } 63 | 64 | const timeout = setTimeout(() => { 65 | toastTimeouts.delete(toastId) 66 | dispatch({ 67 | type: "REMOVE_TOAST", 68 | toastId: toastId, 69 | }) 70 | }, TOAST_REMOVE_DELAY) 71 | 72 | toastTimeouts.set(toastId, timeout) 73 | } 74 | 75 | export const reducer = (state: State, action: Action): State => { 76 | switch (action.type) { 77 | case "ADD_TOAST": 78 | return { 79 | ...state, 80 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), 81 | } 82 | 83 | case "UPDATE_TOAST": 84 | return { 85 | ...state, 86 | toasts: state.toasts.map((t) => 87 | t.id === action.toast.id ? { ...t, ...action.toast } : t 88 | ), 89 | } 90 | 91 | case "DISMISS_TOAST": { 92 | const { toastId } = action 93 | 94 | // ! Side effects ! - This could be extracted into a dismissToast() action, 95 | // but I'll keep it here for simplicity 96 | if (toastId) { 97 | addToRemoveQueue(toastId) 98 | } else { 99 | state.toasts.forEach((toast) => { 100 | addToRemoveQueue(toast.id) 101 | }) 102 | } 103 | 104 | return { 105 | ...state, 106 | toasts: state.toasts.map((t) => 107 | t.id === toastId || toastId === undefined 108 | ? { 109 | ...t, 110 | open: false, 111 | } 112 | : t 113 | ), 114 | } 115 | } 116 | case "REMOVE_TOAST": 117 | if (action.toastId === undefined) { 118 | return { 119 | ...state, 120 | toasts: [], 121 | } 122 | } 123 | return { 124 | ...state, 125 | toasts: state.toasts.filter((t) => t.id !== action.toastId), 126 | } 127 | } 128 | } 129 | 130 | const listeners: Array<(state: State) => void> = [] 131 | 132 | let memoryState: State = { toasts: [] } 133 | 134 | function dispatch(action: Action) { 135 | memoryState = reducer(memoryState, action) 136 | listeners.forEach((listener) => { 137 | listener(memoryState) 138 | }) 139 | } 140 | 141 | type Toast = Omit 142 | 143 | function toast({ ...props }: Toast) { 144 | const id = genId() 145 | 146 | const update = (props: ToasterToast) => 147 | dispatch({ 148 | type: "UPDATE_TOAST", 149 | toast: { ...props, id }, 150 | }) 151 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) 152 | 153 | dispatch({ 154 | type: "ADD_TOAST", 155 | toast: { 156 | ...props, 157 | id, 158 | open: true, 159 | onOpenChange: (open) => { 160 | if (!open) dismiss() 161 | }, 162 | }, 163 | }) 164 | 165 | return { 166 | id: id, 167 | dismiss, 168 | update, 169 | } 170 | } 171 | 172 | function useToast() { 173 | const [state, setState] = React.useState(memoryState) 174 | 175 | React.useEffect(() => { 176 | listeners.push(setState) 177 | return () => { 178 | const index = listeners.indexOf(setState) 179 | if (index > -1) { 180 | listeners.splice(index, 1) 181 | } 182 | } 183 | }, [state]) 184 | 185 | return { 186 | ...state, 187 | toast, 188 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), 189 | } 190 | } 191 | 192 | export { useToast, toast } 193 | -------------------------------------------------------------------------------- /components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SelectPrimitive from "@radix-ui/react-select" 5 | import { Check, ChevronDown } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Select = SelectPrimitive.Root 10 | 11 | const SelectGroup = SelectPrimitive.Group 12 | 13 | const SelectValue = SelectPrimitive.Value 14 | 15 | const SelectTrigger = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, children, ...props }, ref) => ( 19 | 27 | {children} 28 | 29 | 30 | 31 | 32 | )) 33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 34 | 35 | const SelectContent = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, children, position = "popper", ...props }, ref) => ( 39 | 40 | 51 | 58 | {children} 59 | 60 | 61 | 62 | )) 63 | SelectContent.displayName = SelectPrimitive.Content.displayName 64 | 65 | const SelectLabel = React.forwardRef< 66 | React.ElementRef, 67 | React.ComponentPropsWithoutRef 68 | >(({ className, ...props }, ref) => ( 69 | 74 | )) 75 | SelectLabel.displayName = SelectPrimitive.Label.displayName 76 | 77 | const SelectItem = React.forwardRef< 78 | React.ElementRef, 79 | React.ComponentPropsWithoutRef 80 | >(({ className, children, ...props }, ref) => ( 81 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | {children} 96 | 97 | )) 98 | SelectItem.displayName = SelectPrimitive.Item.displayName 99 | 100 | const SelectSeparator = React.forwardRef< 101 | React.ElementRef, 102 | React.ComponentPropsWithoutRef 103 | >(({ className, ...props }, ref) => ( 104 | 109 | )) 110 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 111 | 112 | export { 113 | Select, 114 | SelectGroup, 115 | SelectValue, 116 | SelectTrigger, 117 | SelectContent, 118 | SelectLabel, 119 | SelectItem, 120 | SelectSeparator, 121 | } 122 | -------------------------------------------------------------------------------- /components/ui/sheet.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SheetPrimitive from "@radix-ui/react-dialog" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | import { X } from "lucide-react" 7 | 8 | import { cn } from "@/lib/utils" 9 | 10 | const Sheet = SheetPrimitive.Root 11 | 12 | const SheetTrigger = SheetPrimitive.Trigger 13 | 14 | const SheetClose = SheetPrimitive.Close 15 | 16 | const SheetPortal = ({ 17 | className, 18 | ...props 19 | }: SheetPrimitive.DialogPortalProps) => ( 20 | 21 | ) 22 | SheetPortal.displayName = SheetPrimitive.Portal.displayName 23 | 24 | const SheetOverlay = React.forwardRef< 25 | React.ElementRef, 26 | React.ComponentPropsWithoutRef 27 | >(({ className, ...props }, ref) => ( 28 | 36 | )) 37 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName 38 | 39 | const sheetVariants = cva( 40 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", 41 | { 42 | variants: { 43 | side: { 44 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", 45 | bottom: 46 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", 47 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", 48 | right: 49 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", 50 | }, 51 | }, 52 | defaultVariants: { 53 | side: "right", 54 | }, 55 | } 56 | ) 57 | 58 | interface SheetContentProps 59 | extends React.ComponentPropsWithoutRef, 60 | VariantProps {} 61 | 62 | const SheetContent = React.forwardRef< 63 | React.ElementRef, 64 | SheetContentProps 65 | >(({ side = "right", className, children, ...props }, ref) => ( 66 | 67 | 68 | 73 | {children} 74 | 75 | 76 | Close 77 | 78 | 79 | 80 | )) 81 | SheetContent.displayName = SheetPrimitive.Content.displayName 82 | 83 | const SheetHeader = ({ 84 | className, 85 | ...props 86 | }: React.HTMLAttributes) => ( 87 |
94 | ) 95 | SheetHeader.displayName = "SheetHeader" 96 | 97 | const SheetFooter = ({ 98 | className, 99 | ...props 100 | }: React.HTMLAttributes) => ( 101 |
108 | ) 109 | SheetFooter.displayName = "SheetFooter" 110 | 111 | const SheetTitle = React.forwardRef< 112 | React.ElementRef, 113 | React.ComponentPropsWithoutRef 114 | >(({ className, ...props }, ref) => ( 115 | 120 | )) 121 | SheetTitle.displayName = SheetPrimitive.Title.displayName 122 | 123 | const SheetDescription = React.forwardRef< 124 | React.ElementRef, 125 | React.ComponentPropsWithoutRef 126 | >(({ className, ...props }, ref) => ( 127 | 132 | )) 133 | SheetDescription.displayName = SheetPrimitive.Description.displayName 134 | 135 | export { 136 | Sheet, 137 | SheetTrigger, 138 | SheetClose, 139 | SheetContent, 140 | SheetHeader, 141 | SheetFooter, 142 | SheetTitle, 143 | SheetDescription, 144 | } 145 | -------------------------------------------------------------------------------- /components/ui/toast.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ToastPrimitives from "@radix-ui/react-toast" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | import { X } from "lucide-react" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ToastProvider = ToastPrimitives.Provider 9 | 10 | const ToastViewport = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )) 23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName 24 | 25 | const toastVariants = cva( 26 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", 27 | { 28 | variants: { 29 | variant: { 30 | default: "border bg-background text-foreground", 31 | destructive: 32 | "destructive group border-destructive bg-destructive text-destructive-foreground", 33 | }, 34 | }, 35 | defaultVariants: { 36 | variant: "default", 37 | }, 38 | } 39 | ) 40 | 41 | const Toast = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef & 44 | VariantProps 45 | >(({ className, variant, ...props }, ref) => { 46 | return ( 47 | 52 | ) 53 | }) 54 | Toast.displayName = ToastPrimitives.Root.displayName 55 | 56 | const ToastAction = React.forwardRef< 57 | React.ElementRef, 58 | React.ComponentPropsWithoutRef 59 | >(({ className, ...props }, ref) => ( 60 | 68 | )) 69 | ToastAction.displayName = ToastPrimitives.Action.displayName 70 | 71 | const ToastClose = React.forwardRef< 72 | React.ElementRef, 73 | React.ComponentPropsWithoutRef 74 | >(({ className, ...props }, ref) => ( 75 | 84 | 85 | 86 | )) 87 | ToastClose.displayName = ToastPrimitives.Close.displayName 88 | 89 | const ToastTitle = React.forwardRef< 90 | React.ElementRef, 91 | React.ComponentPropsWithoutRef 92 | >(({ className, ...props }, ref) => ( 93 | 98 | )) 99 | ToastTitle.displayName = ToastPrimitives.Title.displayName 100 | 101 | const ToastDescription = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )) 111 | ToastDescription.displayName = ToastPrimitives.Description.displayName 112 | 113 | type ToastProps = React.ComponentPropsWithoutRef 114 | 115 | type ToastActionElement = React.ReactElement 116 | 117 | export { 118 | type ToastProps, 119 | type ToastActionElement, 120 | ToastProvider, 121 | ToastViewport, 122 | Toast, 123 | ToastTitle, 124 | ToastDescription, 125 | ToastClose, 126 | ToastAction, 127 | } 128 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 | import { Check, ChevronRight, Circle } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )) 40 | DropdownMenuSubTrigger.displayName = 41 | DropdownMenuPrimitive.SubTrigger.displayName 42 | 43 | const DropdownMenuSubContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )) 56 | DropdownMenuSubContent.displayName = 57 | DropdownMenuPrimitive.SubContent.displayName 58 | 59 | const DropdownMenuContent = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, sideOffset = 4, ...props }, ref) => ( 63 | 64 | 73 | 74 | )) 75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 76 | 77 | const DropdownMenuItem = React.forwardRef< 78 | React.ElementRef, 79 | React.ComponentPropsWithoutRef & { 80 | inset?: boolean 81 | } 82 | >(({ className, inset, ...props }, ref) => ( 83 | 92 | )) 93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 94 | 95 | const DropdownMenuCheckboxItem = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, children, checked, ...props }, ref) => ( 99 | 108 | 109 | 110 | 111 | 112 | 113 | {children} 114 | 115 | )) 116 | DropdownMenuCheckboxItem.displayName = 117 | DropdownMenuPrimitive.CheckboxItem.displayName 118 | 119 | const DropdownMenuRadioItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )) 139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 140 | 141 | const DropdownMenuLabel = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef & { 144 | inset?: boolean 145 | } 146 | >(({ className, inset, ...props }, ref) => ( 147 | 156 | )) 157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 158 | 159 | const DropdownMenuSeparator = React.forwardRef< 160 | React.ElementRef, 161 | React.ComponentPropsWithoutRef 162 | >(({ className, ...props }, ref) => ( 163 | 168 | )) 169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 170 | 171 | const DropdownMenuShortcut = ({ 172 | className, 173 | ...props 174 | }: React.HTMLAttributes) => { 175 | return ( 176 | 180 | ) 181 | } 182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 183 | 184 | export { 185 | DropdownMenu, 186 | DropdownMenuTrigger, 187 | DropdownMenuContent, 188 | DropdownMenuItem, 189 | DropdownMenuCheckboxItem, 190 | DropdownMenuRadioItem, 191 | DropdownMenuLabel, 192 | DropdownMenuSeparator, 193 | DropdownMenuShortcut, 194 | DropdownMenuGroup, 195 | DropdownMenuPortal, 196 | DropdownMenuSub, 197 | DropdownMenuSubContent, 198 | DropdownMenuSubTrigger, 199 | DropdownMenuRadioGroup, 200 | } 201 | -------------------------------------------------------------------------------- /components/dropzone.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | // imports 4 | import { FiUploadCloud } from 'react-icons/fi'; 5 | import { LuFileSymlink } from 'react-icons/lu'; 6 | import { MdClose } from 'react-icons/md'; 7 | import ReactDropzone from 'react-dropzone'; 8 | import bytesToSize from '@/util/bytes-to-size'; 9 | import fileToIcon from '@/util/file-to-icon'; 10 | import { useState, useEffect, useRef } from 'react'; 11 | import { useToast } from '@/components/ui/use-toast'; 12 | import compressFileName from '@/util/compress-file-name'; 13 | import { Skeleton } from '@/components/ui/skeleton'; 14 | import convertFile from '@/util/convert'; 15 | import { ImSpinner3 } from 'react-icons/im'; 16 | import { MdDone } from 'react-icons/md'; 17 | import { Badge } from '@/components/ui/badge'; 18 | import { HiOutlineDownload } from 'react-icons/hi'; 19 | import { BiError } from 'react-icons/bi'; 20 | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; 21 | import { 22 | Select, 23 | SelectContent, 24 | SelectItem, 25 | SelectTrigger, 26 | SelectValue, 27 | } from './ui/select'; 28 | import { Button } from './ui/button'; 29 | import loadFfmpeg from '@/util/load-ffmpeg'; 30 | import type { Actions } from '@/types'; 31 | import { FFmpeg } from '@ffmpeg/ffmpeg'; 32 | 33 | const extensions = { 34 | image: [ 35 | 'jpg', 36 | 'jpeg', 37 | 'png', 38 | 'gif', 39 | 'bmp', 40 | 'webp', 41 | 'ico', 42 | 'tif', 43 | 'tiff', 44 | 'svg', 45 | 'raw', 46 | 'tga', 47 | ], 48 | video: [ 49 | 'mp4', 50 | 'm4v', 51 | 'mp4v', 52 | '3gp', 53 | '3g2', 54 | 'avi', 55 | 'mov', 56 | 'wmv', 57 | 'mkv', 58 | 'flv', 59 | 'ogv', 60 | 'webm', 61 | 'h264', 62 | '264', 63 | 'hevc', 64 | '265', 65 | ], 66 | audio: ['mp3', 'wav', 'ogg', 'aac', 'wma', 'flac', 'm4a'], 67 | }; 68 | 69 | export default function Dropzone() { 70 | const { toast } = useToast(); 71 | const [is_hover, setIsHover] = useState(false); 72 | const [actions, setActions] = useState([]); 73 | const [is_ready, setIsReady] = useState(false); 74 | const [files, setFiles] = useState>([]); 75 | const [is_loaded, setIsLoaded] = useState(false); 76 | const [is_converting, setIsConverting] = useState(false); 77 | const [is_done, setIsDone] = useState(false); 78 | const ffmpegRef = useRef(null); 79 | const accepted_files = { 80 | 'image/*': [ 81 | '.jpg', 82 | '.jpeg', 83 | '.png', 84 | '.gif', 85 | '.bmp', 86 | '.webp', 87 | '.ico', 88 | '.tif', 89 | '.tiff', 90 | '.raw', 91 | '.tga', 92 | ], 93 | 'audio/*': [], 94 | 'video/*': [], 95 | }; 96 | 97 | // functions 98 | const reset = () => { 99 | setIsDone(false); 100 | setActions([]); 101 | setFiles([]); 102 | setIsReady(false); 103 | setIsConverting(false); 104 | }; 105 | const downloadAll = (): void => { 106 | for (let action of actions) { 107 | !action.is_error && download(action); 108 | } 109 | }; 110 | const download = (action: Actions) => { 111 | const a = document.createElement('a'); 112 | a.style.display = 'none'; 113 | a.href = action.url; 114 | a.download = action.output; 115 | 116 | document.body.appendChild(a); 117 | a.click(); 118 | 119 | // Clean up after download 120 | URL.revokeObjectURL(action.url); 121 | document.body.removeChild(a); 122 | }; 123 | const convert = async (): Promise => { 124 | let tmp_actions = actions.map((elt) => ({ 125 | ...elt, 126 | is_converting: true, 127 | })); 128 | setActions(tmp_actions); 129 | setIsConverting(true); 130 | for (let action of tmp_actions) { 131 | try { 132 | const { url, output } = await convertFile(ffmpegRef.current, action); 133 | tmp_actions = tmp_actions.map((elt) => 134 | elt === action 135 | ? { 136 | ...elt, 137 | is_converted: true, 138 | is_converting: false, 139 | url, 140 | output, 141 | } 142 | : elt, 143 | ); 144 | setActions(tmp_actions); 145 | } catch (err) { 146 | tmp_actions = tmp_actions.map((elt) => 147 | elt === action 148 | ? { 149 | ...elt, 150 | is_converted: false, 151 | is_converting: false, 152 | is_error: true, 153 | } 154 | : elt, 155 | ); 156 | setActions(tmp_actions); 157 | } 158 | } 159 | setIsDone(true); 160 | setIsConverting(false); 161 | }; 162 | const handleUpload = (data: Array): void => { 163 | handleExitHover(); 164 | setFiles(data); 165 | const tmp: Actions[] = []; 166 | data.forEach((file: any) => { 167 | const formData = new FormData(); 168 | tmp.push({ 169 | file_name: file.name, 170 | file_size: file.size, 171 | from: file.name.slice(((file.name.lastIndexOf('.') - 1) >>> 0) + 2), 172 | to: null, 173 | file_type: file.type, 174 | file, 175 | is_converted: false, 176 | is_converting: false, 177 | is_error: false, 178 | }); 179 | }); 180 | setActions(tmp); 181 | }; 182 | const handleHover = (): void => setIsHover(true); 183 | const handleExitHover = (): void => setIsHover(false); 184 | 185 | const updateAction = (file_name: String, to: String) => { 186 | setActions( 187 | actions.map((action): Actions => { 188 | if (action.file_name === file_name) { 189 | console.log('FOUND'); 190 | return { 191 | ...action, 192 | to, 193 | }; 194 | } 195 | 196 | return action; 197 | }), 198 | ); 199 | }; 200 | const checkIsReady = (): void => { 201 | let tmp_is_ready = true; 202 | actions.forEach((action: Actions) => { 203 | if (!action.to) tmp_is_ready = false; 204 | }); 205 | setIsReady(tmp_is_ready); 206 | }; 207 | const deleteAction = (action: Actions): void => { 208 | setActions(actions.filter((elt) => elt !== action)); 209 | setFiles(files.filter((elt) => elt.name !== action.file_name)); 210 | }; 211 | useEffect(() => { 212 | if (!actions.length) { 213 | setIsDone(false); 214 | setFiles([]); 215 | setIsReady(false); 216 | setIsConverting(false); 217 | } else checkIsReady(); 218 | }, [actions]); 219 | useEffect(() => { 220 | load(); 221 | }, []); 222 | const load = async () => { 223 | const ffmpeg_response: FFmpeg = await loadFfmpeg(); 224 | ffmpegRef.current = ffmpeg_response; 225 | setIsLoaded(true); 226 | }; 227 | 228 | 229 | // returns 230 | if (actions.length) { 231 | return ( 232 |
233 | {actions.map((action: Actions, i: any) => ( 234 |
238 | {!is_loaded && ( 239 | 240 | )} 241 |
242 | 243 | {fileToIcon(action.file_type)} 244 | 245 |
246 | 247 | {compressFileName(action.file_name)} 248 | 249 | 250 | ({bytesToSize(action.file_size)}) 251 | 252 |
253 |
254 | 255 | {action.is_error ? ( 256 | 257 | Error Converting File 258 | 259 | 260 | ) : action.is_converted ? ( 261 | 262 | Done 263 | 264 | 265 | ) : action.is_converting ? ( 266 | 267 | Converting 268 | 269 | 270 | 271 | 272 | ) : ( 273 |
274 | Convert to 275 | 342 |
343 | )} 344 | 345 | {action.is_converted ? ( 346 | 349 | ) : ( 350 | deleteAction(action)} 352 | className="cursor-pointer hover:bg-gray-50 rounded-full h-10 w-10 flex items-center justify-center text-2xl text-gray-400" 353 | > 354 | 355 | 356 | )} 357 |
358 | ))} 359 |
360 | {is_done ? ( 361 |
362 | 370 | 378 |
379 | ) : ( 380 | 394 | )} 395 |
396 |
397 | ); 398 | } 399 | 400 | return ( 401 | { 407 | handleExitHover(); 408 | toast({ 409 | variant: 'destructive', 410 | title: 'Error uploading your file(s)', 411 | description: 'Allowed Files: Audio, Video and Images.', 412 | duration: 5000, 413 | }); 414 | }} 415 | onError={() => { 416 | handleExitHover(); 417 | toast({ 418 | variant: 'destructive', 419 | title: 'Error uploading your file(s)', 420 | description: 'Allowed Files: Audio, Video and Images.', 421 | duration: 5000, 422 | }); 423 | }} 424 | > 425 | {({ getRootProps, getInputProps }) => ( 426 |
430 | 431 |
432 | {is_hover ? ( 433 | <> 434 |
435 | 436 |
437 |

438 | Yes, right there 439 |

440 | 441 | ) : ( 442 | <> 443 |
444 | 445 |
446 |

447 | Click, or drop your files here 448 |

449 | 450 | )} 451 |
452 |
453 | )} 454 |
455 | ); 456 | } 457 | --------------------------------------------------------------------------------