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
74 changes: 62 additions & 12 deletions app/components/Nametag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ type NametagProps = {
forcedEditMode?: boolean
onDataChange?: (data: NametagData) => void
readOnly?: boolean
validationErrors?: {
profilePhoto?: boolean
fullName?: boolean
}
showRequiredAsterisks?: boolean
}

export const Nametag = ({
Expand All @@ -34,7 +39,9 @@ export const Nametag = ({
initialEditing = false,
forcedEditMode = false,
onDataChange,
readOnly = false
readOnly = false,
validationErrors = {},
showRequiredAsterisks = false
}: NametagProps) => {
const [isEditing, setIsEditing] = useState(initialEditing || forcedEditMode)
const [formData, setFormData] = useState<NametagData>(data)
Expand Down Expand Up @@ -212,7 +219,10 @@ export const Nametag = ({
</SaveButtonWrapper>
)}
<NametagLeft>
<PhotoFrame onClick={readOnly ? undefined : () => fileInputRef.current?.click()}>
<PhotoFrame
onClick={readOnly ? undefined : () => fileInputRef.current?.click()}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Define a handlePhotoFrameClick handler function and implement the logic there.

$error={validationErrors.profilePhoto}
>
{uploading ? (
<PlaceholderAvatar>Loading...</PlaceholderAvatar>
) : formData.profilePhoto ? (
Expand All @@ -228,6 +238,12 @@ export const Nametag = ({
</PhotoOverlay>
)}
</PhotoFrame>
{showRequiredAsterisks && (
<PhotoRequiredLabel>
Profile Photo <RequiredAsterisk>*</RequiredAsterisk>
</PhotoRequiredLabel>
)}
{validationErrors.profilePhoto && <FieldError>Please upload a profile photo</FieldError>}
{!readOnly && (
<input
type="file"
Expand All @@ -241,9 +257,16 @@ export const Nametag = ({

<NametagRight>
<NametagInputGroup>
<NametagLabel>HELLO my name is</NametagLabel>
<NametagLabel>
HELLO my name is
{showRequiredAsterisks && <RequiredAsterisk> *</RequiredAsterisk>}
</NametagLabel>
<InputWithHelpContainer>
<NametagInputWrapper $fontSize="1.5rem" $fontWeight="700">
<NametagInputWrapper
$fontSize="1.5rem"
$fontWeight="700"
$error={validationErrors.fullName}
>
<TextInput
variant="secondary"
size="default"
Expand All @@ -256,10 +279,11 @@ export const Nametag = ({
}
}}
placeholder="Your Name"
required
error={validationErrors.fullName}
/>
</NametagInputWrapper>
</InputWithHelpContainer>
{validationErrors.fullName && <FieldError>Please enter your name</FieldError>}
</NametagInputGroup>

<NametagInputGroup>
Expand All @@ -277,7 +301,6 @@ export const Nametag = ({
}
}}
placeholder="Title"
required
/>
</NametagInputWrapper>
<HelpInfoButton>Your job title or role.</HelpInfoButton>
Expand All @@ -299,7 +322,6 @@ export const Nametag = ({
}
}}
placeholder="Affiliation"
required
/>
</NametagInputWrapper>
<HelpInfoButton>Your company, organization, or school name.</HelpInfoButton>
Expand Down Expand Up @@ -533,16 +555,17 @@ const PhotoOverlay = styled.div`
pointer-events: none;
`

const PhotoFrame = styled.div`
const PhotoFrame = styled.div<{ $error?: boolean }>`
width: 120px;
height: 120px;
border-radius: 8px;
overflow: hidden;
background-color: rgba(255, 255, 255, 0.1);
border: 2px solid rgba(255, 255, 255, 0.3);
border: 2px solid ${(props) => (props.$error ? "var(--error-color)" : "rgba(255, 255, 255, 0.3)")};
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
cursor: pointer;
position: relative;
transition: border-color 0.2s ease;

&:hover ${PhotoOverlay} {
opacity: 1;
Expand Down Expand Up @@ -600,27 +623,34 @@ const InputWithHelpContainer = styled.div`
position: relative;
`

const NametagInputWrapper = styled.div<{ $fontSize?: string; $fontWeight?: string }>`
const NametagInputWrapper = styled.div<{
$fontSize?: string
$fontWeight?: string
$error?: boolean
}>`
flex: 1;
width: 100%;

input {
background: transparent;
border: none;
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
border-bottom: 2px solid
${(props) => (props.$error ? "var(--error-color)" : "rgba(255, 255, 255, 0.2)")};
padding: 0.25rem 0;
font-size: ${(props) => props.$fontSize || "1rem"};
font-weight: ${(props) => props.$fontWeight || "normal"};
color: rgba(255, 255, 255, 0.95);
width: 100%;
transition: border-bottom-color 0.2s ease;

&::placeholder {
color: rgba(255, 255, 255, 0.5);
}

&:focus {
outline: none;
border-bottom-color: rgba(156, 163, 255, 0.8);
border-bottom-color: ${(props) =>
props.$error ? "var(--error-color)" : "rgba(156, 163, 255, 0.8)"};
background: rgba(255, 255, 255, 0.05);
}
}
Expand All @@ -632,3 +662,23 @@ const NametagDisplayText = styled.div<{ $fontSize?: string; $fontWeight?: string
color: rgba(255, 255, 255, 0.95);
padding: 0.25rem 0;
`

const RequiredAsterisk = styled.span`
color: var(--error-color);
font-weight: 700;
`

const PhotoRequiredLabel = styled.div`
color: rgba(255, 255, 255, 0.7);
font-size: 0.75rem;
font-weight: 500;
text-align: center;
margin-top: 0.5rem;
`

const FieldError = styled.p`
color: var(--error-color);
font-size: 0.75rem;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

font-size here is not consistent with font-size in TextInput.

font-weight: 500;
margin-top: 0.5rem;
`
38 changes: 30 additions & 8 deletions app/components/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,26 @@ type BaseInputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">
interface TextInputProps extends BaseInputProps {
variant?: "primary" | "secondary"
size?: "small" | "default"
error?: boolean
}

// Components //

export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
({ variant = "secondary", size = "small", ...props }, ref) => {
return <StyledInput ref={ref} $variant={variant} $size={size} {...props} />
({ variant = "secondary", size = "small", error = false, ...props }, ref) => {
return <StyledInput ref={ref} $variant={variant} $size={size} $error={error} {...props} />
}
)

TextInput.displayName = "TextInput"

// Styled Components //

const StyledInput = styled.input<{ $variant: "primary" | "secondary"; $size: "small" | "default" }>`
const StyledInput = styled.input<{
$variant: "primary" | "secondary"
$size: "small" | "default"
$error?: boolean
}>`
padding: ${(props) => (props.$size === "small" ? "0.5rem 1rem" : "0.75rem 1.5rem")};
border-radius: 0.25rem;
font-weight: ${(props) => (props.$size === "small" ? "500" : "600")};
Expand All @@ -35,14 +40,31 @@ const StyledInput = styled.input<{ $variant: "primary" | "secondary"; $size: "sm
box-sizing: border-box;
background-color: ${(props) => (props.$variant === "primary" ? "white" : "transparent")};
color: ${(props) => (props.$variant === "primary" ? "black" : "white")};
border: ${(props) =>
props.$variant === "secondary"
? "1px solid rgba(255, 255, 255, 0.3)"
: "1px solid rgba(0, 0, 0, 0.2)"};
border: ${(props) => {
if (props.$error) {
return "1px solid var(--error-color)"
}

if (props.$variant === "secondary") {
return "1px solid rgba(255, 255, 255, 0.3)"
}

return "1px solid rgba(0, 0, 0, 0.2)"
}};

&:focus {
outline: none;
border-color: ${(props) => (props.$variant === "secondary" ? "white" : "rgba(0, 0, 0, 0.4)")};
border-color: ${(props) => {
if (props.$error) {
return "var(--error-color)"
}

if (props.$variant === "secondary") {
return "white"
}

return "rgba(0, 0, 0, 0.4)"
}};
background-color: ${(props) =>
props.$variant === "primary" ? "white" : "rgba(255, 255, 255, 0.05)"};
}
Expand Down
3 changes: 3 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
@tailwind components;
@tailwind utilities;

:root {
--error-color: #f87171;
}
:root {
--b1: #000000;
--n: 21% 0.003 296.813;
Expand Down
Loading