Skip to content

Commit

Permalink
implement badge while converting and when done
Browse files Browse the repository at this point in the history
  • Loading branch information
Souhail Benlhachemi authored and Souhail Benlhachemi committed Sep 6, 2023
1 parent f942bc7 commit 0123c7b
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 47 deletions.
3 changes: 2 additions & 1 deletion app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,5 @@
body {
@apply bg-background text-foreground;
}
}
}

2 changes: 1 addition & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function Home() {
{/* Title + Desc */}
<div className="space-y-6">
<h1 className="text-3xl md:text-5xl font-medium text-center">
Cloud File Converter
Free Unlimited File Converter
</h1>
<p className="text-gray-400 text-md md:text-lg text-center md:px-24 xl:px-44 2xl:px-52">
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Ab,
Expand Down
161 changes: 117 additions & 44 deletions components/dropzone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import fileToIcon from '@/utils/file-to-icon';
import { useState, useEffect } from 'react';
import { useToast } from '@/components/ui/use-toast';
import compressFileName from '@/utils/compress-file-name';
import axios from 'axios';
import { Skeleton } from '@/components/ui/skeleton';
import convertImg from '@/utils/convert-img';
import { ImSpinner3 } from 'react-icons/im';
import { MdDone } from 'react-icons/md';
import { Badge } from '@/components/ui/badge';
import { HiOutlineDownload } from 'react-icons/hi';
import {
Select,
SelectContent,
Expand All @@ -19,31 +24,53 @@ import {
SelectValue,
} from './ui/select';
import { Button } from './ui/button';
import loadFfmpeg from '@/utils/load-ffmpeg';
import type { Action } from '@/types';

const extensions = {
image: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'ico', 'jfif'],
video: ['mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv'],
audio: ['mp3', 'wav', 'ogg', 'aac', 'wma', 'flac'],
};

export default function Dropzone() {
// variables & hooks
const { toast } = useToast();
const [is_hover, setIsHover] = useState<Boolean>(false);
const [is_hover, setIsHover] = useState<boolean>(false);
const [actions, setActions] = useState<Action[]>([]);
const [ready, setIsReady] = useState<Boolean>(false);
const [is_ready, setIsReady] = useState<boolean>(false);
const [files, setFiles] = useState<Array<any>>([]);
const [is_loaded, setIsLoaded] = useState<boolean>(false);
const [is_converting, setIsConverting] = useState<boolean>(false);
const [is_done, setIsDone] = useState<boolean>(false);
const accepted_files = {
'image/*': [],
};

// functions
const convert = async (): Promise<any> => {
axios({
method: 'POST',
url: '/api/convert',
data: {
actions,
},
headers: {
'Content-Type': 'multipart/form-data',
},
});
let tmp_actions = actions.map((elt) => ({
...elt,
is_converting: true,
}));
setActions(tmp_actions);
setIsConverting(true);
for (let action of tmp_actions) {
const { file, to } = action;
await convertImg(file, to);
tmp_actions = tmp_actions.map((elt) =>
elt === action
? {
...elt,
is_converted: true,
is_converting: false,
}
: elt,
);
setActions(tmp_actions);
}
setIsDone(true);
setIsConverting(false);
};
const handleUpload = (data: Array<any>): void => {
setFiles(data);
Expand All @@ -56,7 +83,9 @@ export default function Dropzone() {
from: file.name.slice(((file.name.lastIndexOf('.') - 1) >>> 0) + 2),
to: null,
file_type: file.type,
buffer: file,
file,
is_converted: false,
is_converting: false,
});
});
setActions(tmp);
Expand Down Expand Up @@ -92,6 +121,9 @@ export default function Dropzone() {
useEffect(() => {
checkIsReady();
}, [actions]);
useEffect(() => {
loadFfmpeg().then(() => setIsLoaded(true));
}, []);

// returns
if (actions.length) {
Expand All @@ -100,8 +132,11 @@ export default function Dropzone() {
{actions.map((action: Action, i: any) => (
<div
key={i}
className="w-full cursor-pointer rounded-xl border h-20 px-10 flex items-center justify-between"
className="w-full relative cursor-pointer rounded-xl border h-20 px-10 flex items-center justify-between"
>
{!is_loaded && (
<Skeleton className="h-full w-full -ml-10 cursor-progress absolute rounded-xl" />
)}
<div className="flex gap-4 items-center">
<span className="text-2xl text-orange-600">
{fileToIcon(action.file_type)}
Expand All @@ -116,39 +151,77 @@ export default function Dropzone() {
</div>
</div>

<div className="text-gray-400 text-md flex items-center gap-4">
<span>Convert to</span>
<Select
onValueChange={(value) => updateAction(action.file_name, value)}
>
<SelectTrigger className="w-32 text-center text-gray-600 bg-gray-50 text-md font-medium">
<SelectValue placeholder="..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="png">PNG</SelectItem>
<SelectItem value="mp4">MP4</SelectItem>
<SelectItem value="mp3">MP3</SelectItem>
</SelectContent>
</Select>
</div>
{action.is_converted ? (
<Badge variant="default" className="flex gap-2 bg-green-500">
<span>Done</span>
<MdDone />
</Badge>
) : action.is_converting ? (
<Badge variant="default" className="flex gap-2">
<span>Converting</span>
<span className="animate-spin">
<ImSpinner3 />
</span>
</Badge>
) : (
<div className="text-gray-400 text-md flex items-center gap-4">
<span>Convert to</span>
<Select
onValueChange={(value) =>
updateAction(action.file_name, value)
}
>
<SelectTrigger className="w-32 outline-none focus:outline-none focus:ring-0 text-center text-gray-600 bg-gray-50 text-md font-medium">
<SelectValue placeholder="..." />
</SelectTrigger>
<SelectContent>
{extensions.image.map((elt) => (
<SelectItem value={elt}>{elt}</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}

<span
onClick={() => deleteAction(action)}
className="cursor-pointer text-2xl text-gray-400"
>
<MdClose />
</span>
{action.is_converted ? (
<Button variant="outline">Download</Button>
) : (
<span
onClick={() => deleteAction(action)}
className="cursor-pointer hover:bg-gray-50 rounded-full h-10 w-10 flex items-center justify-center text-2xl text-gray-400"
>
<MdClose />
</span>
)}
</div>
))}
<div className="flex w-full justify-end">
<Button
size="lg"
disabled={!ready}
className="rounded-xl font-semibold py-4 text-md"
onClick={convert}
>
Convert Now
</Button>
{is_done ? (
<Button
size="lg"
disabled={!is_ready || is_converting}
className="rounded-xl font-semibold relative py-4 text-md flex gap-2 items-center w-fit"
onClick={convert}
>
{actions.length > 1 ? 'Download All' : 'Download'}
<HiOutlineDownload />
</Button>
) : (
<Button
size="lg"
disabled={!is_ready || is_converting}
className="rounded-xl font-semibold relative py-4 text-md flex items-center w-44"
onClick={convert}
>
{is_converting ? (
<span className="animate-spin text-lg">
<ImSpinner3 />
</span>
) : (
<span>Convert Now</span>
)}
</Button>
)}
</div>
</div>
);
Expand Down
36 changes: 36 additions & 0 deletions components/ui/badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/utils"

const badgeVariants = cva(
"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",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)

export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}

export { Badge, badgeVariants }
15 changes: 15 additions & 0 deletions components/ui/skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { cn } from "@/utils"

function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}

export { Skeleton }
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"lint": "next lint"
},
"dependencies": {
"@ffmpeg/ffmpeg": "^0.12.6",
"@ffmpeg/util": "^0.12.1",
"@radix-ui/react-select": "^1.2.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.4",
Expand Down
4 changes: 3 additions & 1 deletion types.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export type Action = {
buffer?: Buffer;
file: Buffer;
file_name: String;
file_size: number;
from: String;
to: String | null;
file_type?: String;
is_converting?: boolean;
is_converted?: boolean;
};
10 changes: 10 additions & 0 deletions utils/convert-img.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default async function convertImg(
image_file: Buffer,
to: any,
): Promise<any> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(true);
}, 4000);
});
}
13 changes: 13 additions & 0 deletions utils/load-ffmpeg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// imports
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { toBlobURL } from '@ffmpeg/util';

export default async function loadFfmpeg(): Promise<Boolean> {
const ffmpeg = new FFmpeg();
const baseURL = 'https://unpkg.com/@ffmpeg/[email protected]/dist/umd';
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
});
return true;
}
17 changes: 17 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,23 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.48.0.tgz#642633964e217905436033a2bd08bf322849b7fb"
integrity sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==

"@ffmpeg/ffmpeg@^0.12.6":
version "0.12.6"
resolved "https://registry.yarnpkg.com/@ffmpeg/ffmpeg/-/ffmpeg-0.12.6.tgz#34dd5959b9446837240a32acb6979474e8935500"
integrity sha512-4CuXDaqrCga5qBwVtiDDR45y65OGPYZd7VzwGCGz3QLdrQH7xaLYEjU19XL4DTCL0WnTSH8752b8Atyb1SiiLw==
dependencies:
"@ffmpeg/types" "^0.12.0"

"@ffmpeg/types@^0.12.0":
version "0.12.1"
resolved "https://registry.yarnpkg.com/@ffmpeg/types/-/types-0.12.1.tgz#659e4eb51f3e01e145a6fe99e5b8e12a373aa59a"
integrity sha512-n+v9yvxaBPi1Z/1wy2PvqMHvED3qV7LXBWnBmMBAAzybbs4KWp6wrhCTMxITORMHXpPZEEupsBH1iEoLuzYSUw==

"@ffmpeg/util@^0.12.1":
version "0.12.1"
resolved "https://registry.yarnpkg.com/@ffmpeg/util/-/util-0.12.1.tgz#98afa20d7b4c0821eebdb205ddcfa5d07b0a4f53"
integrity sha512-10jjfAKWaDyb8+nAkijcsi9wgz/y26LOc1NKJradNMyCIl6usQcBbhkjX5qhALrSBcOy6TOeksunTYa+a03qNQ==

"@floating-ui/core@^1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.4.1.tgz#0d633f4b76052668afb932492ac452f7ebe97f17"
Expand Down

0 comments on commit 0123c7b

Please sign in to comment.