Skip to content
Merged
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
6 changes: 5 additions & 1 deletion frontend/src/components/prompts/prompt-detail-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ export function PromptDetailDialog({
aria-labelledby="prompt-detail-title"
onMouseDown={(event) => event.stopPropagation()}
>
<h2 id="prompt-detail-title">{t('prompts.detail.title')}</h2>
<h2 id="prompt-detail-title">{prompt.title}</h2>
<div className="prompt-detail-body">
<div className="prompt-detail-meta prompt-detail-summary">
<div className="prompt-detail-field">
<span className="label">{t('prompts.detail.titleLabel')}</span>
<strong>{prompt.title}</strong>
</div>
<div className="prompt-detail-field">
<span className="label">{t('prompts.detail.model')}</span>
<strong>{prompt.model_name}</strong>
Expand Down
12 changes: 11 additions & 1 deletion frontend/src/components/prompts/prompt-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { PromptInput, PromptRecord } from '../../features/prompts/prompts-t
import { promptSchema } from '../../lib/validation/prompt-schemas'
import { Button } from '../ui/button'
import { ComboboxInput } from '../ui/combobox-input'
import { Input } from '../ui/input'
import { RatingInput } from '../ui/rating-input'
import { Select } from '../ui/select'
import { Textarea } from '../ui/textarea'
Expand Down Expand Up @@ -32,6 +33,7 @@ export function PromptForm({
}: PromptFormProps) {
const { t } = useTranslation()
const [form, setForm] = useState<PromptInput>({
title: initialValue?.title ?? '',
model_name: initialValue?.model_name ?? '',
prompt_text: initialValue?.prompt_text ?? '',
category: initialValue?.category ?? '',
Expand Down Expand Up @@ -74,12 +76,20 @@ export function PromptForm({
setFieldErrors({})
await onSubmit(parsed.data)
if (!isEdit) {
setForm({ model_name: '', prompt_text: '', category: '', rate: 3 })
setForm({ title: '', model_name: '', prompt_text: '', category: '', rate: 3 })
}
}

return (
<form className="prompt-form" onSubmit={handleSubmit}>
<Input
label={t('prompts.form.title')}
value={form.title}
onChange={(e) => handleChange('title', e.target.value)}
error={fieldErrors.title}
placeholder={t('prompts.form.titlePlaceholder')}
maxLength={120}
/>
<ComboboxInput
label={t('prompts.form.model')}
options={modelOptions}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/prompts/prompt-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ export function PromptList({ prompts, onEdit, onDelete }: PromptListProps) {
onClick={() => setSelectedPrompt(prompt)}
>
<span className="prompt-list-content">
<strong>{prompt.model_name}</strong>
<strong>{prompt.title}</strong>
<span className="prompt-list-summary">{summary}</span>
</span>
<span className="prompt-list-meta">
<span className="badge">{prompt.model_name}</span>
<span className="badge">{prompt.category}</span>
<span className="muted">{t('common.rating', { value: prompt.rate })}</span>
</span>
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/features/auth/auth-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type SessionResolved = {
username: string
userId: number
role: UserRole
preferredLanguage: UserRecord['preferred_language']
}

export async function loginAndResolveUserId(
Expand All @@ -34,6 +35,7 @@ export async function loginAndResolveUserId(
username: currentUser.username,
userId: currentUser.id,
role: currentUser.role,
preferredLanguage: currentUser.preferred_language,
}
}

Expand All @@ -44,6 +46,20 @@ export function signup(input: RegisterInput): Promise<SignupResponse> {
})
}

export async function signupAndResolveSession(
input: RegisterInput,
): Promise<SessionResolved> {
const response = await signup(input)

return {
token: response.access_token,
username: response.user.username,
userId: response.user.id,
role: response.user.role,
preferredLanguage: response.user.preferred_language,
}
}

export function getCurrentUser(token: string): Promise<UserRecord> {
return apiRequest<UserRecord>('/auth/profile', {
method: 'GET',
Expand Down
26 changes: 22 additions & 4 deletions frontend/src/features/auth/auth-store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,23 @@ import {
useState,
type ReactNode,
} from 'react'
import { getCurrentUser, loginAndResolveUserId } from './auth-service'
import type { UserRole } from './auth-types'
import { getCurrentUser, loginAndResolveUserId, signupAndResolveSession } from './auth-service'
import type { PreferredLanguage, RegisterInput, UserRole } from './auth-types'

type SessionState = {
token: string | null
username: string | null
userId: number | null
role: UserRole | null
preferredLanguage: PreferredLanguage | null
}

type AuthContextValue = {
session: SessionState
isAuthenticated: boolean
isReady: boolean
login: (username: string, password: string) => Promise<void>
signup: (input: RegisterInput) => Promise<void>
logout: () => void
}

Expand All @@ -42,6 +44,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
username: null,
userId: null,
role: null,
preferredLanguage: null,
})
const [isReady, setIsReady] = useState(() => !initialToken)

Expand All @@ -57,6 +60,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
username: user.username,
userId: user.id,
role: user.role,
preferredLanguage: user.preferred_language,
})
})
.catch(() => {
Expand All @@ -79,12 +83,25 @@ export function AuthProvider({ children }: AuthProviderProps) {
username: resolved.username,
userId: resolved.userId,
role: resolved.role,
preferredLanguage: resolved.preferredLanguage,
})
}, [])

const signup = useCallback(async (input: RegisterInput) => {
const resolved = await signupAndResolveSession(input)
sessionStorage.setItem(SESSION_TOKEN_KEY, resolved.token)
setSession({
token: resolved.token,
username: resolved.username,
userId: resolved.userId,
role: resolved.role,
preferredLanguage: resolved.preferredLanguage,
})
}, [])

const logout = useCallback(() => {
sessionStorage.removeItem(SESSION_TOKEN_KEY)
setSession({ token: null, username: null, userId: null, role: null })
setSession({ token: null, username: null, userId: null, role: null, preferredLanguage: null })
}, [])

const value = useMemo<AuthContextValue>(
Expand All @@ -93,9 +110,10 @@ export function AuthProvider({ children }: AuthProviderProps) {
isAuthenticated: Boolean(session.token),
isReady,
login,
signup,
logout,
}),
[isReady, login, logout, session],
[isReady, login, logout, session, signup],
)

return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/features/auth/auth-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ export type LoginResponse = {
token_type: string
}

export type PreferredLanguage = 'es' | 'en'

export type RegisterInput = {
username: string
password: string
name: string
last_name: string
email: string
preferred_language: PreferredLanguage
}

export type UserRole = 'user' | 'admin' | 'god'
Expand All @@ -24,11 +27,15 @@ export type UserRecord = {
name: string
last_name: string
email: string
preferred_language: PreferredLanguage
role: UserRole
}

export type SignupResponse = {
message: string
access_token: string
token_type: string
user: UserRecord
}

export type RecoveryGenerateInput = {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/features/prompts/prompts-types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
export type PromptRecord = {
id: number
user_id: number
title: string
model_name: string
prompt_text: string
category: string
rate: number
}

export type PromptInput = {
title: string
model_name: string
prompt_text: string
category: string
Expand Down
42 changes: 40 additions & 2 deletions frontend/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const en = {
app: {
name: 'Prompt Catalog',
name: 'iPrompt',
tagline: 'Personal library for prompts that work',
},
language: {
Expand Down Expand Up @@ -40,6 +40,39 @@ export const en = {
previewEyebrow: 'Catalog preview',
previewTitle: 'A warmer home for your reusable AI work',
previewCopy: 'Preview how saved prompts can be organized with ratings, models, categories, and quick context before you commit them to your own library.',
creatorOffers: {
eyebrow: 'Free creator starter pack',
title: '3 creator-built prompts included with your free account.',
copy: 'Sign up and iPrompt seeds your catalog with three complete, editable prompts in your preferred language.',
included: 'Create a free account to unlock the full prompt bodies in your private catalog.',
cta: 'Create free account',
cards: [
{
title: 'Appliance Value Expert',
category: 'Finance & Investing',
model: 'GPT',
rating: '5/5',
outcome: 'Compare appliance options in Mexico by price, warranty, ownership cost, reliability, and practical performance.',
locked: 'Full quote workflow unlocks after signup.',
},
{
title: 'Personal Meal Planning Expert',
category: 'Personal Development',
model: 'GPT',
rating: '5/5',
outcome: 'Plan realistic 30-day meals around budget, preferences, prep time, and safety-aware wellness context.',
locked: 'Educational meal-planning prompt included after signup; not a replacement for clinical care.',
},
{
title: 'Influencer Content Planner',
category: 'Marketing & Sales',
model: 'GPT',
rating: '5/5',
outcome: 'Build an editorial calendar with hooks, scripts, captions, repurposing ideas, and weekly metrics.',
locked: 'Full two-step planning prompt unlocks in your catalog.',
},
],
},
benefitsLabel: 'Prompt library benefits',
benefits: [
{
Expand Down Expand Up @@ -103,6 +136,8 @@ export const en = {
firstName: 'First name',
lastName: 'Last name',
email: 'Email',
preferredLanguage: 'Prompt language',
preferredLanguageHelp: 'Your starter prompts will be saved in this language. You can still switch the interface separately.',
creating: 'Creating...',
alreadyHaveAccount: 'Already have an account?',
invalidCredentials: 'Invalid credentials',
Expand Down Expand Up @@ -153,6 +188,7 @@ export const en = {
emptyRating: 'No ratings yet',
},
form: {
title: 'Title',
model: 'Model',
prompt: 'Prompt text',
category: 'Category',
Expand All @@ -161,10 +197,12 @@ export const en = {
update: 'Update prompt',
knownModel: 'Select a known model from the list.',
invalid: 'Invalid form data',
titlePlaceholder: 'Name this prompt so it is easy to find later.',
promptPlaceholder: 'Paste the prompt that worked, including any constraints or context you want to reuse.',
},
detail: {
title: 'Prompt details',
titleLabel: 'Title',
model: 'Model',
prompt: 'Prompt',
category: 'Category',
Expand All @@ -190,7 +228,7 @@ export const en = {
},
editTitle: 'Edit prompt as god',
allPrompts: 'All prompts',
promptMeta: 'User {{userId}} · {{category}} · Rating {{rating}}/5',
promptMeta: 'User {{userId}} · {{model}} · {{category}} · Rating {{rating}}/5',
noPrompts: 'No prompts found.',
deleteTitle: 'Delete this prompt?',
deleteDescription: 'This removes the selected prompt from the catalog for every admin view.',
Expand Down
Loading
Loading