Skip to content

Commit

Permalink
Feature/21/add-project-form (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
AmirAgassi authored Nov 27, 2024
2 parents 65ca9de + 4ece8a4 commit 3b55af8
Show file tree
Hide file tree
Showing 15 changed files with 934 additions and 499 deletions.
5 changes: 3 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "nokap",
"name": "spur",
"private": true,
"version": "0.0.0",
"type": "module",
Expand All @@ -16,10 +16,11 @@
"dependencies": {
"@headlessui/react": "^2.2.0",
"clsx": "^2.1.1",
"framer-motion": "^11.11.17",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"react-router-dom": "^6.27.0"
"react-router-dom": "^7.0.1"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
Expand Down
790 changes: 419 additions & 371 deletions frontend/pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion frontend/src/components/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const Dropdown: React.FC<DropdownProps> = ({
return (
<div className="max-w-[400px] w-full">
{label && (
<label className="block text-2xl font-bold mb-2">
<label className="block text-sm font-medium text-gray-900 mb-2">
{label}
</label>
)}
Expand Down
94 changes: 74 additions & 20 deletions frontend/src/components/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { useState, useRef } from 'react';
import { FiUpload } from 'react-icons/fi';
import { FiUpload, FiX } from 'react-icons/fi';

interface FileUploadProps {
onFilesChange?: (files: File[]) => void;
children?: React.ReactNode;
className?: string;
maxSizeMB?: number;
}

const FileUpload: React.FC<FileUploadProps> = ({
onFilesChange,
children,
className = ''
className = '',
maxSizeMB = 50
}) => {
const [isDragging, setIsDragging] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
const fileInputRef = useRef<HTMLInputElement>(null);

// handle drag events
Expand Down Expand Up @@ -52,21 +55,52 @@ const FileUpload: React.FC<FileUploadProps> = ({
};

const handleFiles = (files: File[]) => {
// check file types
const validFiles = files.filter(file =>
['application/pdf', 'image/png', 'image/jpeg'].includes(file.type)
);

if (validFiles.length !== files.length) {
alert('only pdf, png and jpeg files are allowed');
alert('Only PDF, PNG and JPEG files are allowed');
return;
}

if (validFiles.length > 0 && onFilesChange) {
onFilesChange(validFiles);
// check file sizes
const oversizedFiles = validFiles.filter(
file => file.size > maxSizeMB * 1024 * 1024
);

if (oversizedFiles.length > 0) {
alert(`Files must be smaller than ${maxSizeMB}MB`);
return;
}

// update state and call onChange
const newFiles = [...uploadedFiles, ...validFiles];
setUploadedFiles(newFiles);

if (onFilesChange) {
onFilesChange(newFiles);
}
};

const removeFile = (fileToRemove: File) => {
const newFiles = uploadedFiles.filter(file => file !== fileToRemove);
setUploadedFiles(newFiles);

if (onFilesChange) {
onFilesChange(newFiles);
}
};

const formatFileSize = (bytes: number) => {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
};

return (
<div className={`w-full max-w-2xl mx-auto p-4 ${className}`}>
<div className={`w-full ${className}`}>
<div
className={`border-2 border-dashed rounded-lg p-6 ${
isDragging ? 'border-blue-500 bg-blue-50' : 'border-gray-300'
Expand All @@ -77,23 +111,18 @@ const FileUpload: React.FC<FileUploadProps> = ({
onDrop={handleDrop}
>
{children || (
<div>
<div className="text-center">
<div className="flex items-center justify-center gap-3">
<FiUpload className="text-gray-400 text-2xl" />
<h3 className="text-lg font-medium">Drag and drop here</h3>
</div>
<div className="text-center mt-2">
<p className="text-gray-500">or</p>
<button
onClick={() => fileInputRef.current?.click()}
className="mt-2 px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700"
>
Select File
</button>
<p className="mt-2 text-sm text-gray-500">
Accepted file types: PDF, PNG, JPEG
</p>
<h3 className="text-sm font-medium">Drag and drop here</h3>
</div>
<p className="text-sm text-gray-500 mt-2">or</p>
<button
onClick={() => fileInputRef.current?.click()}
className="mt-2 px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 text-sm"
>
Select File
</button>
</div>
)}
<input
Expand All @@ -105,6 +134,31 @@ const FileUpload: React.FC<FileUploadProps> = ({
multiple
/>
</div>

{/* File list */}
{uploadedFiles.length > 0 && (
<div className="mt-4 space-y-2">
{uploadedFiles.map((file, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-gray-50 rounded-md"
>
<div className="flex items-center gap-3">
<span className="text-sm font-medium">{file.name}</span>
<span className="text-sm text-gray-500">
{formatFileSize(file.size)}
</span>
</div>
<button
onClick={() => removeFile(file)}
className="text-gray-400 hover:text-gray-600"
>
<FiX />
</button>
</div>
))}
</div>
)}
</div>
);
};
Expand Down
52 changes: 52 additions & 0 deletions frontend/src/components/NotificationBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ReactNode } from 'react';
import { motion, AnimatePresence } from 'framer-motion';

interface NotificationBannerProps {
message: ReactNode;
variant?: 'info' | 'warning' | 'error';
position?: 'inline' | 'bottom';
}

const NotificationBanner: React.FC<NotificationBannerProps> = ({
message,
variant = 'info',
position = 'inline'
}) => {
const variantStyles = {
info: 'bg-gray-50 border-gray-200 text-gray-600',
warning: 'bg-gray-50 border-gray-200 text-gray-600',
error: 'bg-red-50 border-red-200 text-red-600'
};

if (position === 'bottom') {
return (
<motion.div
className="absolute bottom-0 left-0 right-0 mx-auto px-4 mb-4 flex justify-center"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.3,
ease: "easeOut"
}}
>
<motion.div
className={`
inline-block px-6 py-4 rounded-full border text-center
${variantStyles[variant]}
`}
transition={{ duration: 0.2 }}
>
<p className="text-sm">{message}</p>
</motion.div>
</motion.div>
);
}

return (
<div className={`w-full border rounded-lg p-4 ${variantStyles[variant]}`}>
<p className="text-sm text-center">{message}</p>
</div>
);
};

export { NotificationBanner };
35 changes: 19 additions & 16 deletions frontend/src/components/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ interface TextInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
description?: string;
value?: string;
required?: boolean;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
onChange?: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
multiline?: boolean;
rows?: number;
}

const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
({ label, error, description, className = '', value, required, onChange, ...props }, ref) => {
({ label, error, description, className = '', value, required, onChange, multiline = false, rows = 4, ...props }, ref) => {
const inputProps = onChange
? { value, onChange }
: { defaultValue: value };
Expand All @@ -26,6 +28,8 @@ const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
${className}
`;

const InputComponent = multiline ? 'textarea' : 'input';

return (
<div className="w-full">
<Field>
Expand All @@ -42,27 +46,26 @@ const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
</div>
)}

<Input
ref={ref}
<InputComponent
ref={ref as any}
className={sharedClassNames}
invalid={!!error}
required={required}
rows={multiline ? rows : undefined}
{...inputProps}
{...props}
/>

{description && (
<div className="mt-1 text-sm text-gray-500">
{description}
</div>
)}

{error && (
<div className="mt-1 text-sm text-red-500">
{error}
</div>
)}
</Field>
{error && (
<p className="mt-1 text-sm text-red-500">
{error}
</p>
)}
{description && (
<p className="mt-1 text-sm text-gray-500">
{description}
</p>
)}
</div>
);
}
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export { Dropdown } from './Dropdown';
export { FileUpload } from './FileUpload';
export { InfoCard } from './InfoCard';
export { Button } from './Button';
export { NotificationBanner } from './NotificationBanner';
export { FormContainer } from './FormContainer';
export { ScrollLink } from './ScrollLink';
export { AnchorLinks } from './AnchorLinks';
export { TextArea } from './TextArea';
Expand All @@ -12,8 +14,7 @@ export {
Grid,
Footer,
Header,
FormContainer,
DashboardTemplate,
AdminDashboard,
UserDashboard
} from './layout';
} from './layout';
13 changes: 13 additions & 0 deletions frontend/src/components/layout/components/DashboardSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { motion } from 'framer-motion';

export const DashboardSkeleton = () => {
return (
<div className="px-6 animate-pulse">
<div className="h-8 w-48 bg-gray-200 rounded mb-6" />
<div className="space-y-4">
<div className="h-4 w-full max-w-md mx-auto bg-gray-200 rounded" />
<div className="h-4 w-full max-w-sm mx-auto bg-gray-200 rounded" />
</div>
</div>
);
};
18 changes: 13 additions & 5 deletions frontend/src/components/layout/pages/UserDashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { ReactNode } from "react";
import { FiFolder, FiBook, FiStar, FiUser } from "react-icons/fi";
import { DashboardTemplate } from "../templates/DashboardTemplate";
import { useNavigate } from 'react-router-dom';

const userMenuItems = [
{ label: "My Projects", path: "/projects", icon: <FiFolder /> },
Expand All @@ -19,15 +20,22 @@ interface UserDashboardProps {
}

export const UserDashboard: React.FC<UserDashboardProps> = ({ children }) => {
const navigate = useNavigate();

const userActions = (
<>
<button className="px-4 py-2 bg-gray-800 text-white text-sm font-medium rounded-md hover:bg-gray-700">
<button
onClick={() => navigate('/submit-project')}
className="px-4 py-2 bg-gray-800 text-white text-sm font-medium rounded-md hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Submit a project
</button>
<button className="p-2 text-gray-600 hover:text-gray-900 rounded-full">
<span className="sr-only">User menu</span>
<div className="w-8 h-8 bg-gray-200 rounded-full" />
</button>
<div className="relative">
<button className="p-2 text-gray-600 hover:text-gray-900 rounded-full">
<span className="sr-only">User menu</span>
<div className="w-8 h-8 bg-gray-200 rounded-full" />
</button>
</div>
</>
);

Expand Down
Loading

0 comments on commit 3b55af8

Please sign in to comment.