mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
we're basically done
This commit is contained in:
@@ -42,7 +42,7 @@ export default async function BlogPage() {
|
||||
function BlogPostItem({ post }: { post: Post }) {
|
||||
return (
|
||||
<Link href={`/blog/${post.slug}`}>
|
||||
<div className="flex h-auto w-full items-center justify-between py-6 opacity-100 transition-opacity hover:opacity-75">
|
||||
<div className="flex h-auto w-full items-center justify-between py-6 opacity-100 hover:opacity-75">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-xl font-semibold">{post.title}</h2>
|
||||
<p className="text-muted-foreground">{post.description}</p>
|
||||
|
||||
@@ -180,7 +180,7 @@ function AllContributorsSection({
|
||||
href={contributor.html_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="opacity-100 transition-opacity hover:opacity-70"
|
||||
className="opacity-100 hover:opacity-70"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-2 p-2">
|
||||
<Avatar className="size-16">
|
||||
@@ -214,7 +214,7 @@ function ExternalToolsSection() {
|
||||
<p className="text-muted-foreground">Tools we use to build OpenCut</p>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto grid max-w-4xl grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3">
|
||||
<div className="mx-auto grid max-w-4xl grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{EXTERNAL_TOOLS.map((tool, index) => (
|
||||
<Link
|
||||
key={tool.url}
|
||||
@@ -224,10 +224,8 @@ function ExternalToolsSection() {
|
||||
style={{ animationDelay: `${index * 100}ms` }}
|
||||
>
|
||||
<Card className="h-full">
|
||||
<CardContent className="flex h-full flex-col gap-4 p-6 text-center">
|
||||
<div className="bg-muted/50 mx-auto flex size-12 items-center justify-center rounded-full">
|
||||
<tool.icon className="size-6" />
|
||||
</div>
|
||||
<CardContent className="flex items-center justify-center h-full flex-col gap-4 p-6 text-center">
|
||||
<tool.icon className="size-8" />
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<h3 className="text-lg font-semibold">{tool.name}</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
--popover: hsl(0, 0%, 100%);
|
||||
--popover-foreground: hsl(0 0% 2%);
|
||||
--primary: hsl(203, 100%, 50%);
|
||||
--primary-hover: hsl(203, 100%, 45%);
|
||||
--primary-foreground: hsl(0, 0%, 100%);
|
||||
--secondary: hsl(216, 13%, 92%);
|
||||
--secondary-foreground: hsl(0 0% 2%);
|
||||
@@ -44,9 +45,6 @@
|
||||
--sidebar-ring: hsl(0 0% 16.9%);
|
||||
--panel-background: hsl(216 13% 96%);
|
||||
--panel-accent: hsl(216, 8%, 90%);
|
||||
|
||||
--radius: 1rem;
|
||||
|
||||
--sidebar: hsl(0 0% 98%);
|
||||
}
|
||||
.dark {
|
||||
@@ -56,7 +54,8 @@
|
||||
--card-foreground: hsl(0 0% 89%);
|
||||
--popover: hsl(0 0% 14.9%);
|
||||
--popover-foreground: hsl(0 0% 98%);
|
||||
--primary: hsl(205, 84%, 53%);
|
||||
--primary: hsl(203, 100%, 50%);
|
||||
--primary-hover: hsl(203, 100%, 45%);
|
||||
--primary-foreground: hsl(0 0% 9%);
|
||||
--secondary: hsl(0 0% 14.9%);
|
||||
--secondary-foreground: hsl(0 0% 98%);
|
||||
@@ -124,15 +123,16 @@
|
||||
--font-sans: var(--font-inter), sans-serif;
|
||||
|
||||
/* Font sizes */
|
||||
--text-base: 0.95rem;
|
||||
--text-xl: 1.20rem;
|
||||
--text-base: 0.92rem;
|
||||
--text-base--line-height: calc(1.5 / 0.95);
|
||||
--text-xs: 0.75rem;
|
||||
--text-xs--line-height: calc(1 / 0.8);
|
||||
|
||||
/* Border radius */
|
||||
--radius-lg: var(--radius);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-sm: calc(var(--radius) - 8px);
|
||||
--radius-lg: 0.82rem;
|
||||
--radius-md: 0.65rem;
|
||||
--radius-sm: 0.35rem;
|
||||
|
||||
/* Palette mapped to root design tokens */
|
||||
--color-background: var(--background);
|
||||
@@ -146,7 +146,7 @@
|
||||
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
|
||||
--color-primary-hover: var(--primary-hover);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
|
||||
|
||||
@@ -110,7 +110,11 @@ export default function ProjectsPage() {
|
||||
}
|
||||
>
|
||||
{projectsToDisplay.map((project) => (
|
||||
<ProjectItem key={project.id} project={project} />
|
||||
<ProjectItem
|
||||
key={project.id}
|
||||
project={project}
|
||||
allProjectIds={projectsToDisplay.map((p) => p.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -144,10 +148,10 @@ function ProjectsHeader() {
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
|
||||
<div className="hidden md:flex rounded-full border p-1 h-10">
|
||||
<div className="hidden md:flex rounded-md border p-1 h-10">
|
||||
<button
|
||||
type="button"
|
||||
className={`p-2 rounded-full cursor-pointer ${isHydrated && viewMode === "grid" ? "bg-accent/75" : ""}`}
|
||||
className={`p-2 rounded-sm cursor-pointer ${isHydrated && viewMode === "grid" ? "bg-accent/75" : ""}`}
|
||||
onClick={() => setViewMode({ viewMode: "grid" })}
|
||||
onKeyDown={(event) =>
|
||||
event.key === "Enter" && setViewMode({ viewMode: "grid" })
|
||||
@@ -159,7 +163,7 @@ function ProjectsHeader() {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`p-2 rounded-full cursor-pointer ${isHydrated && viewMode === "list" ? "bg-accent/75" : ""}`}
|
||||
className={`p-2 rounded-sm cursor-pointer ${isHydrated && viewMode === "list" ? "bg-accent/75" : ""}`}
|
||||
onClick={() => setViewMode({ viewMode: "list" })}
|
||||
onKeyDown={(event) =>
|
||||
event.key === "Enter" && setViewMode({ viewMode: "list" })
|
||||
@@ -219,7 +223,7 @@ function ProjectsToolbar({ projectIds }: { projectIds: string[] }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sticky top-16 z-10 flex items-center justify-between px-6 h-14 bg-background">
|
||||
<div className="sticky top-16 z-10 flex items-center justify-between px-6 h-14 pt-2 bg-background">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label
|
||||
className="flex items-center gap-3 cursor-pointer px-2"
|
||||
@@ -298,7 +302,7 @@ function ProjectsToolbar({ projectIds }: { projectIds: string[] }) {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{selectedProjectCount > 1 ? <ProjectActions /> : null}
|
||||
{selectedProjectCount > 0 ? <ProjectActions /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -518,20 +522,24 @@ function NewProjectButton() {
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="flex px-5 md:px-6"
|
||||
onClick={handleCreateProject}
|
||||
>
|
||||
<HugeiconsIcon icon={PlusSignIcon} />
|
||||
<span className="text-sm font-medium hidden md:block">New project</span>
|
||||
<span className="text-sm font-medium block md:hidden">New</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectItem({ project }: { project: TProjectMetadata }) {
|
||||
const { selectedProjectIds, viewMode, setProjectSelected } =
|
||||
function ProjectItem({
|
||||
project,
|
||||
allProjectIds,
|
||||
}: {
|
||||
project: TProjectMetadata;
|
||||
allProjectIds: string[];
|
||||
}) {
|
||||
const { selectedProjectIds, viewMode, setProjectSelected, selectProjectRange } =
|
||||
useProjectsStore();
|
||||
const selectedProjectIdSet = new Set(selectedProjectIds);
|
||||
const isSelected = selectedProjectIdSet.has(project.id);
|
||||
@@ -541,6 +549,20 @@ function ProjectItem({ project }: { project: TProjectMetadata }) {
|
||||
const isMultiSelect = selectedProjectCount > 1;
|
||||
const isGridView = viewMode === "grid";
|
||||
|
||||
const handleCheckboxChange = ({
|
||||
checked,
|
||||
shiftKey,
|
||||
}: {
|
||||
checked: boolean;
|
||||
shiftKey: boolean;
|
||||
}) => {
|
||||
if (shiftKey && checked) {
|
||||
selectProjectRange({ projectId: project.id, allProjectIds });
|
||||
return;
|
||||
}
|
||||
setProjectSelected({ projectId: project.id, isSelected: checked });
|
||||
};
|
||||
|
||||
const gridContent = (
|
||||
<Card className="bg-background overflow-hidden border-none p-0">
|
||||
<div className="bg-muted relative aspect-video">
|
||||
@@ -567,7 +589,7 @@ function ProjectItem({ project }: { project: TProjectMetadata }) {
|
||||
</div>
|
||||
|
||||
<CardContent className="flex flex-col gap-2 px-0 pt-4">
|
||||
<h3 className="group-hover:text-foreground/90 line-clamp-2 text-sm leading-snug font-medium transition-colors">
|
||||
<h3 className="group-hover:text-foreground/90 line-clamp-2 text-sm leading-snug font-medium">
|
||||
{project.name}
|
||||
</h3>
|
||||
<div className="text-muted-foreground flex items-center gap-1.5 text-sm">
|
||||
@@ -595,7 +617,7 @@ function ProjectItem({ project }: { project: TProjectMetadata }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="group-hover:text-foreground/90 text-sm font-medium truncate flex-1 min-w-0 transition-colors">
|
||||
<h3 className="group-hover:text-foreground/90 text-sm font-medium truncate flex-1 min-w-0">
|
||||
{project.name}
|
||||
</h3>
|
||||
|
||||
@@ -617,12 +639,14 @@ function ProjectItem({ project }: { project: TProjectMetadata }) {
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(checked) =>
|
||||
setProjectSelected({
|
||||
projectId: project.id,
|
||||
isSelected: checked === true,
|
||||
})
|
||||
}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={(event) => {
|
||||
handleCheckboxChange({
|
||||
checked: !isSelected,
|
||||
shiftKey: event.shiftKey,
|
||||
});
|
||||
}}
|
||||
onCheckedChange={() => {}}
|
||||
className="size-5 shrink-0"
|
||||
/>
|
||||
|
||||
@@ -657,12 +681,14 @@ function ProjectItem({ project }: { project: TProjectMetadata }) {
|
||||
<>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(checked) =>
|
||||
setProjectSelected({
|
||||
projectId: project.id,
|
||||
isSelected: checked === true,
|
||||
})
|
||||
}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={(event) => {
|
||||
handleCheckboxChange({
|
||||
checked: !isSelected,
|
||||
shiftKey: event.shiftKey,
|
||||
});
|
||||
}}
|
||||
onCheckedChange={() => {}}
|
||||
className={`absolute z-10 size-5 top-3 left-3 ${
|
||||
isSelected || isDropdownOpen
|
||||
? "opacity-100"
|
||||
|
||||
@@ -10,6 +10,7 @@ interface ProjectsState {
|
||||
sortOrder: "asc" | "desc";
|
||||
viewMode: ProjectsViewMode;
|
||||
selectedProjectIds: string[];
|
||||
lastSelectedProjectId: string | null;
|
||||
isHydrated: boolean;
|
||||
setIsHydrated: ({ isHydrated }: { isHydrated: boolean }) => void;
|
||||
setSearchQuery: ({ query }: { query: string }) => void;
|
||||
@@ -26,6 +27,13 @@ interface ProjectsState {
|
||||
projectId: string;
|
||||
isSelected: boolean;
|
||||
}) => void;
|
||||
selectProjectRange: ({
|
||||
projectId,
|
||||
allProjectIds,
|
||||
}: {
|
||||
projectId: string;
|
||||
allProjectIds: string[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
const getNextSelectedProjectIds = ({
|
||||
@@ -56,6 +64,7 @@ export const useProjectsStore = create<ProjectsState>()(
|
||||
sortOrder: "desc",
|
||||
viewMode: "grid",
|
||||
selectedProjectIds: [],
|
||||
lastSelectedProjectId: null,
|
||||
isHydrated: false,
|
||||
setIsHydrated: ({ isHydrated }) => set({ isHydrated }),
|
||||
setSearchQuery: ({ query }) => set({ searchQuery: query }),
|
||||
@@ -68,7 +77,8 @@ export const useProjectsStore = create<ProjectsState>()(
|
||||
setViewMode: ({ viewMode }) => set({ viewMode }),
|
||||
setSelectedProjects: ({ projectIds }) =>
|
||||
set({ selectedProjectIds: projectIds }),
|
||||
clearSelectedProjects: () => set({ selectedProjectIds: [] }),
|
||||
clearSelectedProjects: () =>
|
||||
set({ selectedProjectIds: [], lastSelectedProjectId: null }),
|
||||
setProjectSelected: ({ projectId, isSelected }) =>
|
||||
set((state) => ({
|
||||
selectedProjectIds: getNextSelectedProjectIds({
|
||||
@@ -76,7 +86,37 @@ export const useProjectsStore = create<ProjectsState>()(
|
||||
projectId,
|
||||
isSelected,
|
||||
}),
|
||||
lastSelectedProjectId: isSelected ? projectId : state.lastSelectedProjectId,
|
||||
})),
|
||||
selectProjectRange: ({ projectId, allProjectIds }) =>
|
||||
set((state) => {
|
||||
const anchorId = state.lastSelectedProjectId;
|
||||
if (!anchorId) {
|
||||
return {
|
||||
selectedProjectIds: [projectId],
|
||||
lastSelectedProjectId: projectId,
|
||||
};
|
||||
}
|
||||
|
||||
const anchorIndex = allProjectIds.indexOf(anchorId);
|
||||
const targetIndex = allProjectIds.indexOf(projectId);
|
||||
|
||||
if (anchorIndex === -1 || targetIndex === -1) {
|
||||
return {
|
||||
selectedProjectIds: [projectId],
|
||||
lastSelectedProjectId: projectId,
|
||||
};
|
||||
}
|
||||
|
||||
const startIndex = Math.min(anchorIndex, targetIndex);
|
||||
const endIndex = Math.max(anchorIndex, targetIndex);
|
||||
const rangeIds = allProjectIds.slice(startIndex, endIndex + 1);
|
||||
|
||||
const merged = new Set([...state.selectedProjectIds, ...rangeIds]);
|
||||
return {
|
||||
selectedProjectIds: Array.from(merged),
|
||||
};
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: "projects-view-mode",
|
||||
|
||||
Reference in New Issue
Block a user