Select and upload templates

This commit is contained in:
adilallo
2026-02-09 19:05:28 -07:00
parent 2e1538770c
commit 4bb6fe0a89
9 changed files with 606 additions and 9 deletions
@@ -0,0 +1,29 @@
"use client";
import { memo } from "react";
import UploadView from "./Upload.view";
import type { UploadProps } from "./Upload.types";
const UploadContainer = memo<UploadProps>(
({
active = true,
label,
showHelpIcon = true,
onClick,
className = "",
}) => {
return (
<UploadView
active={active}
label={label}
showHelpIcon={showHelpIcon}
onClick={onClick}
className={className}
/>
);
},
);
UploadContainer.displayName = "Upload";
export default UploadContainer;
@@ -0,0 +1,34 @@
export interface UploadProps {
/**
* Whether the upload component is in active state.
* When active, button has white background with black text.
* When inactive, button has dark background with gray text.
* @default true
*/
active?: boolean;
/**
* Label text displayed above the upload component
*/
label?: string;
/**
* Whether to show help icon next to label
* @default true
*/
showHelpIcon?: boolean;
/**
* Callback when upload button is clicked
*/
onClick?: () => void;
/**
* Additional CSS classes
*/
className?: string;
}
export interface UploadViewProps {
active: boolean;
label?: string;
showHelpIcon: boolean;
onClick?: () => void;
className: string;
}
@@ -0,0 +1,113 @@
"use client";
import { memo } from "react";
import InputLabel from "../../utility/InputLabel";
import type { UploadViewProps } from "./Upload.types";
function UploadView({
active = true,
label,
showHelpIcon = true,
onClick,
className = "",
}: UploadViewProps) {
const isActive = active;
// Button styles based on active state
const buttonBgClass = isActive
? "bg-[var(--color-surface-invert-primary,white)]"
: "bg-[var(--color-surface-default-secondary,#141414)]";
const buttonTextColor = isActive
? "text-[color:var(--color-content-invert-primary,black)]"
: "text-[color:var(--color-content-invert-tertiary,#2d2d2d)]";
// Description text color based on active state
const descriptionTextColor = isActive
? "text-[color:var(--color-content-default-primary,white)]"
: "text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
// Icon color based on active state
const iconColor = isActive
? "text-[color:var(--color-content-invert-primary,black)]"
: "text-[color:var(--color-content-invert-tertiary,#2d2d2d)]";
return (
<div className={`flex flex-col gap-[var(--measures-spacing-300,12px)] items-start relative w-full ${className}`}>
{/* Label using InputLabel component */}
{label && (
<InputLabel
label={label}
helpIcon={showHelpIcon}
asterisk={false}
helperText={false}
size="S"
palette="Default"
/>
)}
{/* Upload container */}
<div className="bg-[var(--color-surface-default-secondary,#141414)] flex gap-[24px] items-center justify-center px-[var(--measures-spacing-600,24px)] py-[var(--measures-spacing-1200,48px)] rounded-[var(--measures-radius-200,8px)] shrink-0 w-full">
{/* Upload button */}
<button
type="button"
onClick={onClick}
className={`${buttonBgClass} flex gap-[var(--measures-spacing-150,6px)] items-center justify-center overflow-clip p-[var(--measures-spacing-300,12px)] rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity`}
aria-label="Upload"
>
{/* Upload icon */}
<div className={`relative shrink-0 size-[20px] ${iconColor}`}>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="size-full"
>
<path
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<polyline
points="17 8 12 3 7 8"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<line
x1="12"
y1="3"
x2="12"
y2="15"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
{/* Button text */}
<div className={`flex flex-col font-inter font-medium justify-center leading-[0] relative shrink-0 text-[length:var(--sizing-400,16px)] whitespace-nowrap ${buttonTextColor}`}>
<p className="leading-[20px]">Upload</p>
</div>
</button>
{/* Description text */}
<div className={`flex flex-[1_0_0] flex-col font-inter font-normal h-[32px] justify-center leading-[0] min-h-px min-w-px relative text-[length:var(--sizing-350,14px)] ${descriptionTextColor}`}>
<p className="leading-[20px] whitespace-pre-wrap">
Add images, PDFs, and other files to the policy
</p>
</div>
</div>
</div>
);
}
UploadView.displayName = "UploadView";
export default memo(UploadView);
+2
View File
@@ -0,0 +1,2 @@
export { default } from "./Upload.container";
export type { UploadProps } from "./Upload.types";
+156
View File
@@ -0,0 +1,156 @@
"use client";
import { useState } from "react";
import { useMediaQuery } from "../../hooks/useMediaQuery";
import HeaderLockup from "../../components/type/HeaderLockup";
import MultiSelect from "../../components/controls/MultiSelect";
/**
* Select page for the create flow
*
* Displays selection options using HeaderLockup and MultiSelect components.
* Responsive layout: two-column at 640px+, single column below 640px.
* Responsive sizing: uses L/M for HeaderLockup and S for MultiSelect based on 640px breakpoint.
*/
export default function SelectPage() {
const isMdOrLarger = useMediaQuery("(min-width: 640px)");
// Sample options for MultiSelect components
const [communitySizeOptions, setCommunitySizeOptions] = useState([
{ id: "1", label: "1 member", state: "Unselected" as const },
{ id: "2", label: "2-10 members", state: "Unselected" as const },
{ id: "3", label: "10-24 members", state: "Unselected" as const },
{ id: "4", label: "24-64 members", state: "Unselected" as const },
{ id: "5", label: "64-128 members", state: "Unselected" as const },
{ id: "6", label: "125-1000 members", state: "Unselected" as const },
{ id: "7", label: "1000+ members", state: "Unselected" as const },
]);
const [organizationTypeOptions, setOrganizationTypeOptions] = useState([
{ id: "1", label: "Non-profit", state: "Unselected" as const },
{ id: "2", label: "For-profit", state: "Unselected" as const },
{ id: "3", label: "Community", state: "Unselected" as const },
{ id: "4", label: "Educational", state: "Unselected" as const },
]);
const [governanceStyleOptions, setGovernanceStyleOptions] = useState([
{ id: "1", label: "Democratic", state: "Unselected" as const },
{ id: "2", label: "Consensus", state: "Unselected" as const },
{ id: "3", label: "Hierarchical", state: "Unselected" as const },
{ id: "4", label: "Flat", state: "Unselected" as const },
]);
const handleCommunitySizeClick = (chipId: string) => {
setCommunitySizeOptions((prev) =>
prev.map((opt) =>
opt.id === chipId
? { ...opt, state: opt.state === "Selected" ? "Unselected" : "Selected" }
: opt
)
);
};
const handleOrganizationTypeClick = (chipId: string) => {
setOrganizationTypeOptions((prev) =>
prev.map((opt) =>
opt.id === chipId
? { ...opt, state: opt.state === "Selected" ? "Unselected" : "Selected" }
: opt
)
);
};
const handleGovernanceStyleClick = (chipId: string) => {
setGovernanceStyleOptions((prev) =>
prev.map((opt) =>
opt.id === chipId
? { ...opt, state: opt.state === "Selected" ? "Unselected" : "Selected" }
: opt
)
);
};
return (
<div className="w-full flex flex-col items-center px-[var(--spacing-measures-spacing-500,20px)] md:px-[64px]">
{isMdOrLarger ? (
// Two-column layout for 640px+
<div className="flex gap-[var(--measures-spacing-1200,48px)] items-center justify-center w-full max-w-[1280px]">
{/* Left column: HeaderLockup */}
<div className="flex flex-[1_0_0] flex-col gap-[var(--measures-spacing-200,8px)] items-start justify-center max-w-[640px] min-h-px min-w-px py-[12px]">
<HeaderLockup
title="What is your community called?"
description="This will be the name of your community"
justification="left"
size="L"
/>
</div>
{/* Right column: Three MultiSelect components */}
<div className="flex flex-[1_0_0] flex-col gap-[var(--measures-spacing-800,32px)] items-start max-w-[640px] min-h-px min-w-px">
<MultiSelect
label="Label"
size="S"
options={communitySizeOptions}
onChipClick={handleCommunitySizeClick}
addButton={true}
addButtonText="Add organization type"
/>
<MultiSelect
label="Label"
size="S"
options={organizationTypeOptions}
onChipClick={handleOrganizationTypeClick}
addButton={true}
addButtonText="Add organization type"
/>
<MultiSelect
label="Label"
size="S"
options={governanceStyleOptions}
onChipClick={handleGovernanceStyleClick}
addButton={true}
addButtonText="Add organization type"
/>
</div>
</div>
) : (
// Single column layout below 640px
<div className="flex flex-col gap-[var(--measures-spacing-400,16px)] items-start w-full max-w-[640px]">
{/* HeaderLockup */}
<HeaderLockup
title="What is your community called?"
description="This will be the name of your community"
justification="left"
size="M"
/>
{/* Three MultiSelect components */}
<MultiSelect
label="Label"
size="S"
options={communitySizeOptions}
onChipClick={handleCommunitySizeClick}
addButton={true}
addButtonText="Add organization type"
/>
<MultiSelect
label="Label"
size="S"
options={organizationTypeOptions}
onChipClick={handleOrganizationTypeClick}
addButton={true}
addButtonText="Add organization type"
/>
<MultiSelect
label="Label"
size="S"
options={governanceStyleOptions}
onChipClick={handleGovernanceStyleClick}
addButton={true}
addButtonText="Add organization type"
/>
</div>
)}
</div>
);
}
+44
View File
@@ -0,0 +1,44 @@
"use client";
import { useMediaQuery } from "../../hooks/useMediaQuery";
import HeaderLockup from "../../components/type/HeaderLockup";
import Upload from "../../components/controls/Upload";
/**
* Upload page for the create flow
*
* Displays upload functionality using HeaderLockup and Upload components.
* Responsive layout: centered at 640px+, left-aligned below 640px.
* Responsive sizing: uses L/M for HeaderLockup based on 640px breakpoint.
*/
export default function UploadPage() {
const isMdOrLarger = useMediaQuery("(min-width: 640px)");
const handleUploadClick = () => {
// Handle upload button click
console.log("Upload clicked");
};
return (
<div className="w-full flex flex-col items-center px-[var(--spacing-measures-spacing-500,20px)] md:px-[64px]">
<div className="flex flex-col gap-[18px] items-center w-full max-w-[640px]">
{/* HeaderLockup: Center justification at 640px+, left below 640px */}
<HeaderLockup
title="How should conflicts be resolved?"
description="This will be the name of your community"
justification={isMdOrLarger ? "center" : "left"}
size={isMdOrLarger ? "L" : "M"}
/>
{/* Upload component: no label in create flow, max width 474px */}
<div className="w-full max-w-[474px]">
<Upload
active={true}
showHelpIcon={true}
onClick={handleUploadClick}
/>
</div>
</div>
</div>
);
}
+4 -9
View File
@@ -42,23 +42,18 @@ export function useMediaQuery(
mediaQuery = query;
}
// Initialize state with current match if available (SSR safety)
const [matches, setMatches] = useState(() => {
if (typeof window === "undefined") {
return false;
}
return window.matchMedia(mediaQuery).matches;
});
// Always start with false so server and first client render match (avoids hydration mismatch).
// Real value is set in useEffect after mount.
const [matches, setMatches] = useState(false);
useEffect(() => {
// Check if window is available (SSR safety)
if (typeof window === "undefined") {
return;
}
const media = window.matchMedia(mediaQuery);
setMatches(media.matches);
// Create listener for changes
const listener = (event: MediaQueryListEvent) => {
setMatches(event.matches);
};