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
13 changes: 9 additions & 4 deletions use-vibes/base/components/VibesButton/VibesButton.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ export const buttonColors: {
tertiary: '#FEDD00',
};

export type ButtonSize = 'default' | 'small';

export function getButtonStyle(
variant: keyof typeof buttonColors,
isHovered: boolean,
isActive: boolean
isActive: boolean,
size: ButtonSize = 'default'
): React.CSSProperties {
let transform = 'translate(0px, 0px)';
let boxShadow = `4px 5px 0px 0px ${buttonColors[variant]}`;
Expand All @@ -26,14 +29,16 @@ export function getButtonStyle(
boxShadow = 'none';
}

const isSmall = size === 'small';

return {
width: '100%',
padding: '1rem 2rem',
width: isSmall ? 'fit-content' : '100%',
padding: isSmall ? '0.5rem 1rem' : '1rem 2rem',
background: '#fff',
color: '#1a1a1a',
border: '3px solid #1a1a1a',
borderRadius: '12px',
fontSize: '1rem',
fontSize: isSmall ? '0.8rem' : '1rem',
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.05em',
Expand Down
6 changes: 4 additions & 2 deletions use-vibes/base/components/VibesButton/VibesButton.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import React, { useEffect, useState } from 'react';
import { getButtonStyle } from './VibesButton.styles.js';
import { getButtonStyle, ButtonSize } from './VibesButton.styles.js';

type ButtonVariant = 'primary' | 'secondary' | 'tertiary';

export interface MenuButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
children: React.ReactNode;
onHover?: () => void;
onUnhover?: () => void;
}

export function VibesButton({
variant = 'primary',
size = 'default',
children,
onHover,
onUnhover,
Expand All @@ -21,7 +23,7 @@ export function VibesButton({
const [isHovered, setHovered] = useState(false);
const [isActive, setActive] = useState(false);

const baseStyle = getButtonStyle(variant, isHovered, isActive);
const baseStyle = getButtonStyle(variant, isHovered, isActive, size);
const mergedStyle = { ...baseStyle, ...customStyle };

useEffect(() => {
Expand Down
51 changes: 51 additions & 0 deletions use-vibes/base/components/VibesPanel/VibesPanel.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { CSSProperties } from 'react';

export const getContainerStyle = (customStyle?: CSSProperties): CSSProperties => ({
padding: '12px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: '12px',
...customStyle,
});

export const innerContainerStyle: CSSProperties = {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: '12px',
width: '250px',
};

export const formStyle: CSSProperties = {
width: '100%',
display: 'flex',
flexDirection: 'column',
gap: '12px',
};

export const labelStyle: CSSProperties = {
alignSelf: 'flex-start',
fontWeight: 600,
};

export const inputCardStyle: CSSProperties = {
width: '100%',
};

export const inputStyle: CSSProperties = {
width: '100%',
border: 'none',
background: 'transparent',
color: 'inherit',
fontSize: 'inherit',
fontWeight: 'inherit',
letterSpacing: 'inherit',
padding: 0,
};

export const statusCardStyle: CSSProperties = {
textAlign: 'center',
};
213 changes: 100 additions & 113 deletions use-vibes/base/components/VibesPanel/VibesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ import React, { useState, useEffect, useId } from 'react';
import { VibesButton } from '../VibesButton/VibesButton.js';
import { BrutalistCard } from '../BrutalistCard/BrutalistCard.js';
import { generateFreshDataUrl, generateRemixUrl } from '../../utils/appSlug.js';
import {
getContainerStyle,
innerContainerStyle,
formStyle,
labelStyle,
inputCardStyle,
inputStyle,
statusCardStyle,
} from './VibesPanel.styles.js';

export interface VibesPanelProps {
/** Optional custom styling for the panel container */
Expand All @@ -16,23 +25,18 @@ export interface VibesPanelProps {
* This component provides the standard three-button layout used
* throughout the Vibes DIY platform for authentication and actions.
*/
type PanelMode = 'default' | 'mutate' | 'invite';
type PanelMode = 'default' | 'mutate' | 'invite' | 'accounts';

export function VibesPanel({ style, className }: VibesPanelProps = {}) {
const emailId = useId();
const [mode, setMode] = useState<PanelMode>('default');
const [email, setEmail] = useState('');
const [currentAccount, setCurrentAccount] = useState('');
const [inviteStatus, setInviteStatus] = useState<'idle' | 'sending' | 'success' | 'error'>(
'idle'
);
const [inviteMessage, setInviteMessage] = useState('');

const handleMutateClick = () => {
if (mode === 'default') {
setMode('mutate');
}
};

const handleInviteClick = () => {
if (mode === 'default') {
setMode('invite');
Expand Down Expand Up @@ -108,116 +112,99 @@ export function VibesPanel({ style, className }: VibesPanelProps = {}) {
};
}, []);

const containerStyle: React.CSSProperties = {
padding: '12px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: '12px',
...style,
};
useEffect(() => {
//Logic to get the Current Account
setCurrentAccount('Amber@vibes.diy');
}, []);
Comment on lines +115 to +118

Choose a reason for hiding this comment

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

P1 Badge Avoid hard-coded account email

The new account menu button always displays Amber@vibes.diy because the effect that initializes currentAccount sets it to that literal string. This means every signed-in user will see the same email regardless of who is actually authenticated, which misrepresents the current account and makes the logout action ambiguous. The value should be derived from the logged-in user’s data or omitted until real data is available.

Useful? React with 👍 / 👎.

Comment on lines +115 to +118
Copy link
Contributor

Choose a reason for hiding this comment

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

Hardcoding a specific email in production logic is brittle and leaks personal info. This will render an empty button on first paint (before useEffect runs) and then replace it with the hardcoded address, creating a flash of empty content and coupling the component to a placeholder.

Prefer sourcing the current account from auth state/props and rendering a fallback label. At minimum, remove the hardcoded email and show a safe placeholder when no account is available.

Suggestion
  • Remove the hardcoded email from the effect and rely on props/context, with a UI fallback.

Example minimal change to avoid shipping a hardcoded email and blank label:

// Remove this effect entirely
// useEffect(() => {
//   //Logic to get the Current Account
//   setCurrentAccount('Amber@vibes.diy');
// }, []);

// And ensure the render uses a fallback label:
// <VibesButton variant="primary" onClick={() => setMode('accounts')}>
//   {currentAccount || 'Accounts'}
// </VibesButton>

Optionally, plumb currentAccount?: string via props instead of local state, or load it from your auth context/store in a separate responsible layer.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.


return (
<div style={containerStyle} className={className}>
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: '12px',
width: '250px',
}}
>
{mode === 'mutate' ? (
// Mutate mode buttons
<>
<VibesButton variant="primary" onClick={handleFreshDataClick}>
Fresh Start
</VibesButton>
<VibesButton variant="secondary" onClick={handleChangeCodeClick}>
Remix Code
const currentlyDisplay: Record<PanelMode, React.ReactNode> = {
default: (
<>
<VibesButton variant="primary" onClick={() => setMode('accounts')}>
{currentAccount}
</VibesButton>
<VibesButton variant="secondary" onClick={() => setMode('mutate')}>
Mutate
</VibesButton>
<VibesButton variant="tertiary" onClick={handleInviteClick}>
Invite
</VibesButton>
</>
),
accounts: (
<>
<VibesButton variant="primary" onClick={handleLogoutClick}>
Logout
</VibesButton>
<VibesButton size="small" variant="tertiary" onClick={handleBackClick}>
← Back
</VibesButton>
</>
),
mutate: (
<>
<VibesButton variant="primary" onClick={handleFreshDataClick}>
Fresh Start
</VibesButton>
<VibesButton variant="secondary" onClick={handleChangeCodeClick}>
Remix Code
</VibesButton>
<VibesButton size="small" variant="tertiary" onClick={handleBackClick}>
← Back
</VibesButton>
</>
),
invite: (
<>
{inviteStatus === 'idle' ? (
<form onSubmit={handleInviteSubmit} style={formStyle}>
<label htmlFor={emailId} style={labelStyle}>
Invite by email
</label>
<BrutalistCard size="md" style={inputCardStyle}>
<input
id={emailId}
type="email"
placeholder="friend@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
style={inputStyle}
autoComplete="email"
required
/>
</BrutalistCard>
<VibesButton variant="primary" type="submit" disabled={!email.trim()}>
Send Invite
</VibesButton>
<VibesButton variant="tertiary" onClick={handleBackClick}>
← Back
</VibesButton>
</>
) : mode === 'invite' ? (
// Invite mode form
<>
{inviteStatus === 'idle' ? (
// Show form when idle
<form
onSubmit={handleInviteSubmit}
style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: '12px' }}
>
<label htmlFor={emailId} style={{ alignSelf: 'flex-start', fontWeight: 600 }}>
Invite by email
</label>
<BrutalistCard size="md" style={{ width: '100%' }}>
<input
id={emailId}
type="email"
placeholder="friend@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
style={{
width: '100%',
border: 'none',
background: 'transparent',
color: 'inherit',
fontSize: 'inherit',
fontWeight: 'inherit',
letterSpacing: 'inherit',
padding: 0,
}}
autoComplete="email"
required
/>
</BrutalistCard>
<VibesButton variant="primary" type="submit" disabled={!email.trim()}>
Send Invite
</VibesButton>
</form>
) : (
// Show status when sending/complete
<BrutalistCard
id="invite-status"
role="status"
aria-live="polite"
size="sm"
variant={
inviteStatus === 'sending'
? 'default'
: inviteStatus === 'error'
? 'error'
: 'success'
}
style={{ textAlign: 'center' }}
>
{inviteStatus === 'sending' ? 'Inviting...' : inviteMessage}
</BrutalistCard>
)}
<VibesButton variant="tertiary" onClick={handleBackClick}>
← Back
</VibesButton>
</>
</form>
) : (
// Default buttons
<>
<VibesButton variant="primary" onClick={handleLogoutClick}>
Logout
</VibesButton>
<VibesButton variant="secondary" onClick={handleMutateClick}>
Mutate
</VibesButton>
<VibesButton variant="tertiary" onClick={handleInviteClick}>
Invite
</VibesButton>
</>
<BrutalistCard
id="invite-status"
role="status"
aria-live="polite"
size="sm"
variant={
Comment on lines +181 to +186
Copy link
Contributor

Choose a reason for hiding this comment

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

id="invite-status" is static. If multiple VibesPanel instances are rendered on a page, this produces duplicate IDs, which can break document semantics and assistive tech expectations. Prefer using useId() to generate a stable, unique ID per instance.

Suggestion

Generate a per-instance status ID:

const statusId = useId();
...
<BrutalistCard
  id={statusId}
  role="status"
  aria-live="polite"
  ...
>

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.

inviteStatus === 'sending'
? 'default'
: inviteStatus === 'error'
? 'error'
: 'success'
}
style={statusCardStyle}
>
{inviteStatus === 'sending' ? 'Inviting...' : inviteMessage}
</BrutalistCard>
)}
</div>
<VibesButton size="small" variant="tertiary" onClick={handleBackClick}>
← Back
</VibesButton>
</>
),
};

return (
<div style={getContainerStyle(style)} className={className}>
<div style={innerContainerStyle}>{currentlyDisplay[mode]}</div>
</div>
);
}