Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ $project.set({
isDeleted: false,
userId: "userId",
domain: "new-2x9tcd",
tags: [],

marketplaceApprovalStatus: "UNLISTED",

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ const InvalidCollectionDataStub = forwardRef<
<a
style={{ color: "inherit" }}
target="_blank"
href="https://docs.webstudio.is/university/core-components/collection.md#whats-an-array"
href="https://docs.webstudio.is/university/core-components/collection#whats-an-array"
// avoid preventing click by events interceptor
onClickCapture={(event) => event.stopPropagation()}
>
Expand Down
2 changes: 2 additions & 0 deletions apps/builder/app/dashboard/dashboard.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const user = {
username: "Taylor",
teamId: null,
provider: "github",
projectsTags: [],
};

const createRouter = (element: JSX.Element, path: string, current?: string) =>
Expand Down Expand Up @@ -49,6 +50,7 @@ const projects = [
previewImageAssetId: "",
latestBuildVirtual: null,
marketplaceApprovalStatus: "UNLISTED" as const,
tags: [],
} as DashboardProject,
];

Expand Down
1 change: 1 addition & 0 deletions apps/builder/app/dashboard/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ export const Dashboard = () => {
projects={projects}
hasProPlan={userPlanFeatures.hasProPlan}
publisherHost={publisherHost}
projectsTags={user.projectsTags}
/>
)}
{view === "templates" && <Templates projects={templates} />}
Expand Down
6 changes: 6 additions & 0 deletions apps/builder/app/dashboard/projects/colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const colors = Array.from({ length: 50 }, (_, i) => {
const l = 55 + (i % 3) * 3; // Reduced variation in lightness (55-61%) to lower contrast
const c = 0.14 + (i % 2) * 0.02; // Reduced variation in chroma (0.14-0.16) for balance
const h = (i * 137.5) % 360; // Golden angle for pleasing hue distribution
return `oklch(${l}% ${c.toFixed(2)} ${h.toFixed(1)})`;
});
58 changes: 48 additions & 10 deletions apps/builder/app/dashboard/projects/project-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
} from "../shared/thumbnail";
import { Spinner } from "../shared/spinner";
import { Card, CardContent, CardFooter } from "../shared/card";
import type { User } from "~/shared/db/user.server";
import { Tag, TagsDialog } from "./tags";

const infoIconStyle = css({ flexShrink: 0 });

Expand Down Expand Up @@ -64,12 +66,14 @@ const Menu = ({
onRename,
onDuplicate,
onShare,
onUpdateTags,
}: {
tabIndex: number;
onDelete: () => void;
onRename: () => void;
onDuplicate: () => void;
onShare: () => void;
onUpdateTags: () => void;
}) => {
const [isOpen, setIsOpen] = useState(false);
return (
Expand All @@ -87,6 +91,7 @@ const Menu = ({
<DropdownMenuItem onSelect={onDuplicate}>Duplicate</DropdownMenuItem>
<DropdownMenuItem onSelect={onRename}>Rename</DropdownMenuItem>
<DropdownMenuItem onSelect={onShare}>Share</DropdownMenuItem>
<DropdownMenuItem onSelect={onUpdateTags}>Tags</DropdownMenuItem>
<DropdownMenuItem onSelect={onDelete}>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Expand All @@ -105,6 +110,7 @@ type ProjectCardProps = {
project: DashboardProject;
hasProPlan: boolean;
publisherHost: string;
projectsTags: User["projectsTags"];
};

export const ProjectCard = ({
Expand All @@ -116,18 +122,30 @@ export const ProjectCard = ({
createdAt,
latestBuildVirtual,
previewImageAsset,
tags,
},
hasProPlan,
publisherHost,
projectsTags,
...props
}: ProjectCardProps) => {
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [isShareDialogOpen, setIsShareDialogOpen] = useState(false);
const [isTagsDialogOpen, setIsTagsDialogOpen] = useState(false);
const [isHidden, setIsHidden] = useState(false);
const handleCloneProject = useCloneProject(id);
const [isTransitioning, setIsTransitioning] = useState(false);

// Makes sure there are no project tags that reference deleted User tags.
// We are not deleting project tag from project.tags when deleting User tags.
const projectTagsIds = (tags || [])
.map((tagId) => {
const tag = projectsTags.find((tag) => tag.id === tagId);
return tag ? tag.id : undefined;
})
.filter(Boolean) as string[];

useEffect(() => {
const linkPath = builderUrl({ origin: window.origin, projectId: id });

Expand Down Expand Up @@ -173,7 +191,25 @@ export const ProjectCard = ({
opacity: 0,
}}
/>

<Flex
wrap="wrap"
gap={1}
css={{
position: "absolute",
padding: theme.panel.padding,
}}
>
{projectsTags.map((tag, index) => {
const isApplied = projectTagsIds.includes(tag.id);
if (isApplied) {
return (
<Tag tag={tag} key={tag.id} index={index} state="pressed">
{tag.label}
</Tag>
);
}
})}
</Flex>
{previewImageAsset ? (
<ThumbnailLinkWithImage to={linkPath} name={previewImageAsset.name} />
) : (
Expand Down Expand Up @@ -225,16 +261,11 @@ export const ProjectCard = ({
</Flex>
<Menu
tabIndex={-1}
onDelete={() => {
setIsDeleteDialogOpen(true);
}}
onRename={() => {
setIsRenameDialogOpen(true);
}}
onShare={() => {
setIsShareDialogOpen(true);
}}
onDelete={() => setIsDeleteDialogOpen(true)}
onRename={() => setIsRenameDialogOpen(true)}
onShare={() => setIsShareDialogOpen(true)}
onDuplicate={handleCloneProject}
onUpdateTags={() => setIsTagsDialogOpen(true)}
/>
</CardFooter>
<RenameProjectDialog
Expand All @@ -256,6 +287,13 @@ export const ProjectCard = ({
projectId={id}
hasProPlan={hasProPlan}
/>
<TagsDialog
projectId={id}
projectsTags={projectsTags}
projectTagsIds={projectTagsIds}
isOpen={isTagsDialogOpen}
onOpenChange={setIsTagsDialogOpen}
/>
</Card>
);
};
56 changes: 52 additions & 4 deletions apps/builder/app/dashboard/projects/projects.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Box,
Flex,
Grid,
List,
Expand All @@ -11,11 +12,16 @@ import type { DashboardProject } from "@webstudio-is/dashboard";
import { ProjectCard } from "./project-card";
import { CreateProject } from "./project-dialogs";
import { Header, Main } from "../shared/layout";
import { useSearchParams } from "react-router-dom";
import { setIsSubsetOf } from "~/shared/shim";
import type { User } from "~/shared/db/user.server";
import { Tag } from "./tags";

export const ProjectsGrid = ({
projects,
hasProPlan,
publisherHost,
projectsTags,
}: ProjectsProps) => {
return (
<List asChild>
Expand All @@ -33,6 +39,7 @@ export const ProjectsGrid = ({
project={project}
hasProPlan={hasProPlan}
publisherHost={publisherHost}
projectsTags={projectsTags}
/>
</ListItem>
);
Expand All @@ -46,9 +53,19 @@ type ProjectsProps = {
projects: Array<DashboardProject>;
hasProPlan: boolean;
publisherHost: string;
projectsTags: User["projectsTags"];
};

export const Projects = (props: ProjectsProps) => {
const [searchParams] = useSearchParams();
const selectedTags = searchParams.getAll("tag");
let projects = props.projects;
if (selectedTags.length > 0) {
projects = projects.filter((project) =>
setIsSubsetOf(new Set(selectedTags), new Set(project.tags))
);
}

return (
<Main>
<Header variant="main">
Expand All @@ -60,12 +77,43 @@ export const Projects = (props: ProjectsProps) => {
</Flex>
</Header>
<Flex
direction="column"
gap="3"
css={{ paddingInline: theme.spacing[13] }}
gap="2"
wrap="wrap"
css={{
display: "none",
flexShrink: 0,
paddingInline: theme.spacing[13],
paddingBlockStart: theme.spacing[2],
paddingBlockEnd: theme.spacing[10],
"&:has(*:first-child)": {
display: "flex",
},
}}
>
<ProjectsGrid {...props} />
{props.projectsTags.map((tag, index) => {
return (
<Tag
tag={tag}
key={tag.id}
index={index}
state={selectedTags.includes(tag.id) ? "pressed" : "auto"}
>
{tag.label}
</Tag>
);
})}
</Flex>
<Box css={{ paddingInline: theme.spacing[13] }}>
{projects.length === 0 && (
<Text
variant="brandRegular"
css={{ padding: theme.spacing[13], textAlign: "center" }}
>
No projects found
</Text>
)}
<ProjectsGrid {...props} projects={projects} />
</Box>
</Main>
);
};
Loading