mirror of
https://github.com/harivansh-afk/Saas-Teamspace.git
synced 2026-04-20 03:00:33 +00:00
changed a lot
This commit is contained in:
parent
ef9ccf22d3
commit
28901128ff
20 changed files with 1794 additions and 526 deletions
84
components/file-upload.tsx
Normal file
84
components/file-upload.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import * as React from "react"
|
||||
import { useDropzone } from "react-dropzone"
|
||||
import { Cloud, File, X } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface FileUploadProps {
|
||||
onUpload: (files: File[]) => void
|
||||
value?: File[]
|
||||
onChange?: (files: File[]) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function FileUpload({ onUpload, value = [], onChange, className }: FileUploadProps) {
|
||||
const [files, setFiles] = React.useState<File[]>(value)
|
||||
|
||||
const onDrop = React.useCallback((acceptedFiles: File[]) => {
|
||||
setFiles(prev => [...prev, ...acceptedFiles])
|
||||
onChange?.(acceptedFiles)
|
||||
onUpload(acceptedFiles)
|
||||
}, [onChange, onUpload])
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
'image/*': [],
|
||||
'application/pdf': [],
|
||||
'application/msword': [],
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': [],
|
||||
}
|
||||
})
|
||||
|
||||
const removeFile = (fileToRemove: File) => {
|
||||
const newFiles = files.filter(file => file !== fileToRemove)
|
||||
setFiles(newFiles)
|
||||
onChange?.(newFiles)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={cn(
|
||||
"border-2 border-dashed rounded-lg p-8 text-center hover:border-primary/50 transition-colors",
|
||||
isDragActive && "border-primary",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Cloud className="h-10 w-10 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Drag & drop files here, or click to select files
|
||||
</p>
|
||||
<Button variant="secondary" size="sm">
|
||||
Select Files
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{files.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
{files.map((file, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-2 rounded-md border p-2"
|
||||
>
|
||||
<File className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm flex-1 truncate">{file.name}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => removeFile(file)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,55 +1,21 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Sparkles } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
const IframeWithSkeleton = () => {
|
||||
const [iframeLoaded, setIframeLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const iframe = document.getElementById('youtube-iframe') as HTMLIFrameElement;
|
||||
if (iframe) {
|
||||
const handleIframeLoad = () => {
|
||||
setIframeLoaded(true);
|
||||
};
|
||||
|
||||
iframe.addEventListener('load', handleIframeLoad);
|
||||
|
||||
return () => {
|
||||
iframe.removeEventListener('load', handleIframeLoad);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!iframeLoaded && <Skeleton className="w-full max-w-2xl h-auto aspect-video" />}
|
||||
<iframe
|
||||
id="youtube-iframe"
|
||||
src="https://www.youtube.com/embed/Q6jDdtbkMIU?si=YtgU89RhYiwt5-U5"
|
||||
title="YouTube Video Player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
className={`w-full max-w-2xl h-auto aspect-video rounded-[6px] ${iframeLoaded ? '' : 'hidden'}`}
|
||||
></iframe>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Header = () => {
|
||||
return (
|
||||
<div className="space-y-20 mt-32">
|
||||
<div className="mx-auto grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div className="flex flex-col justify-center text-center lg:text-left ">
|
||||
<div className="mx-auto grid grid-cols-1 gap-8">
|
||||
<div className="flex flex-col justify-center text-center">
|
||||
<h2 className="text-4xl font-extrabold sm:text-5xl">
|
||||
Clone. Build. Ship.
|
||||
</h2>
|
||||
<p className="mt-4 text-lg text-foreground">
|
||||
Build your SaaS faster with our fully customizable template.
|
||||
</p>
|
||||
<div className="flex justify-center lg:justify-start items-center mt-4">
|
||||
<div className="flex justify-center items-center mt-4">
|
||||
<Link href="/overview">
|
||||
<Button className="gap-2">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
|
|
@ -58,9 +24,6 @@ export const Header = () => {
|
|||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center rounded-lg overflow-hidden">
|
||||
<IframeWithSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { ModeToggle } from '@/components/mode-toggle'
|
||||
import Image from 'next/image'
|
||||
import { UserButton } from '@/components/user-button'
|
||||
import { MobileSidebar } from '@/components/mobile-sidebar'
|
||||
import { Logo } from '@/components/logo'
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
export const navPages = [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
link: '/dashboard'
|
||||
},
|
||||
{
|
||||
title: 'Pricing',
|
||||
link: '/#pricing'
|
||||
},
|
||||
{
|
||||
title: 'Items',
|
||||
link: '/#items'
|
||||
}
|
||||
]
|
||||
|
||||
export const Navbar = () => {
|
||||
const pathname = usePathname()
|
||||
|
||||
// Don't show navbar on home page
|
||||
if (pathname === '/') return null
|
||||
|
||||
return (
|
||||
<nav className="top-0 w-full z-50 transition">
|
||||
<div className="max-w-6xl mx-auto px-6 py-4">
|
||||
|
|
|
|||
91
components/project-select.tsx
Normal file
91
components/project-select.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import * as React from "react"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { PlusCircle } from "lucide-react"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
export function ProjectSelect() {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [projectName, setProjectName] = React.useState("")
|
||||
|
||||
// This would be fetched from your backend
|
||||
const projects = [
|
||||
{ id: "1", name: "Marketing Website" },
|
||||
{ id: "2", name: "Mobile App" },
|
||||
{ id: "3", name: "Dashboard Redesign" },
|
||||
]
|
||||
|
||||
const handleCreateProject = () => {
|
||||
// Handle project creation here
|
||||
setOpen(false)
|
||||
setProjectName("")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Select>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="Select project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Projects</SelectLabel>
|
||||
{projects.map((project) => (
|
||||
<SelectItem key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<PlusCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a new project to organize your tasks.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Project name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={projectName}
|
||||
onChange={(e) => setProjectName(e.target.value)}
|
||||
placeholder="Enter project name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={handleCreateProject}>Create Project</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
101
components/rich-text-editor.tsx
Normal file
101
components/rich-text-editor.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { useEditor, EditorContent } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { Toggle } from "@/components/ui/toggle"
|
||||
import {
|
||||
Bold,
|
||||
Italic,
|
||||
List,
|
||||
ListOrdered,
|
||||
Quote,
|
||||
Heading2,
|
||||
Code,
|
||||
} from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const MenuBar = ({ editor }: { editor: any }) => {
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-b flex flex-wrap gap-1 p-1">
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive('bold')}
|
||||
onPressedChange={() => editor.chain().focus().toggleBold().run()}
|
||||
>
|
||||
<Bold className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive('italic')}
|
||||
onPressedChange={() => editor.chain().focus().toggleItalic().run()}
|
||||
>
|
||||
<Italic className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive('heading')}
|
||||
onPressedChange={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
>
|
||||
<Heading2 className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive('bulletList')}
|
||||
onPressedChange={() => editor.chain().focus().toggleBulletList().run()}
|
||||
>
|
||||
<List className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive('orderedList')}
|
||||
onPressedChange={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
>
|
||||
<ListOrdered className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive('blockquote')}
|
||||
onPressedChange={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
>
|
||||
<Quote className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={editor.isActive('code')}
|
||||
onPressedChange={() => editor.chain().focus().toggleCode().run()}
|
||||
>
|
||||
<Code className="h-4 w-4" />
|
||||
</Toggle>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface RichTextEditorProps {
|
||||
content?: string
|
||||
onChange?: (content: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function RichTextEditor({ content, onChange, className }: RichTextEditorProps) {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
],
|
||||
content,
|
||||
onUpdate: ({ editor }) => {
|
||||
onChange?.(editor.getHTML())
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={cn("border rounded-md", className)}>
|
||||
<MenuBar editor={editor} />
|
||||
<EditorContent
|
||||
editor={editor}
|
||||
className="prose prose-sm dark:prose-invert max-w-none p-4"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
54
components/task-card.tsx
Normal file
54
components/task-card.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { useSortable } from "@dnd-kit/sortable"
|
||||
import { CSS } from "@dnd-kit/utilities"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface TaskCardProps {
|
||||
id: string
|
||||
title: string
|
||||
dueDate: string
|
||||
progress: number
|
||||
status: string
|
||||
}
|
||||
|
||||
export function TaskCard({ id, title, dueDate, progress, status }: TaskCardProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id: id,
|
||||
data: {
|
||||
type: "Task",
|
||||
task: { id, title, dueDate, progress, status },
|
||||
},
|
||||
})
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
|
||||
<Card
|
||||
className={cn(
|
||||
"p-3 cursor-move hover:border-primary/50 transition-colors",
|
||||
isDragging && "opacity-50 border-dashed"
|
||||
)}
|
||||
>
|
||||
<h5 className="font-medium">{title}</h5>
|
||||
<p className="text-sm text-muted-foreground mt-1">Due: {dueDate}</p>
|
||||
<div className="mt-3 h-1.5 w-full bg-secondary rounded-full">
|
||||
<div
|
||||
className="h-full bg-primary rounded-full transition-all"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
52
components/task-column.tsx
Normal file
52
components/task-column.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { useDroppable } from "@dnd-kit/core"
|
||||
import {
|
||||
SortableContext,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { TaskCard } from "./task-card"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { LucideIcon } from "lucide-react"
|
||||
|
||||
interface Task {
|
||||
id: string
|
||||
title: string
|
||||
dueDate: string
|
||||
progress: number
|
||||
status: string
|
||||
}
|
||||
|
||||
interface TaskColumnProps {
|
||||
id: string
|
||||
title: string
|
||||
icon: LucideIcon
|
||||
tasks: Task[]
|
||||
}
|
||||
|
||||
export function TaskColumn({ id, title, icon: Icon, tasks }: TaskColumnProps) {
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
id,
|
||||
})
|
||||
|
||||
const taskIds = tasks.map(task => task.id)
|
||||
|
||||
return (
|
||||
<Card className={cn("p-4", isOver && "ring-2 ring-primary")}>
|
||||
<h4 className="font-medium mb-4 flex items-center">
|
||||
<Icon className="h-4 w-4 mr-2" />
|
||||
{title}
|
||||
</h4>
|
||||
<div ref={setNodeRef} className="space-y-4">
|
||||
<SortableContext
|
||||
id={id}
|
||||
items={taskIds}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{tasks.map((task) => (
|
||||
<TaskCard key={task.id} {...task} />
|
||||
))}
|
||||
</SortableContext>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
42
components/ui/toggle.tsx
Normal file
42
components/ui/toggle.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import * as React from "react"
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-3",
|
||||
sm: "h-9 px-2.5",
|
||||
lg: "h-11 px-5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toggle = React.forwardRef<
|
||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, ...props }, ref) => (
|
||||
<TogglePrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
Toggle.displayName = TogglePrimitive.Root.displayName
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
Loading…
Add table
Add a link
Reference in a new issue