mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 10:19:43 +00:00
Update
This commit is contained in:
1 parent
88543c7775
commit
a3b88395ad
12 files changed
+6
-623
No files matched your search
@@ -1,17 +1,13 @@
|
|||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
|
||||||
COPY turbo.json ./
|
|
||||||
COPY apps/admin/package.json ./apps/admin/
|
|
||||||
COPY packages/shared/package.json ./packages/shared/
|
|
||||||
|
|
||||||
# Install dependencies (only what's needed for admin)
|
# Copy everything first (workspaces need full structure)
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
# Copy source
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Install all dependencies
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
# Build shared package first
|
# Build shared package first
|
||||||
RUN npx turbo run build --filter=@sauci/shared
|
RUN npx turbo run build --filter=@sauci/shared
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,4 @@ services:
|
|||||||
- VITE_AI_MODEL_GENERATE=${VITE_AI_MODEL_GENERATE}
|
- VITE_AI_MODEL_GENERATE=${VITE_AI_MODEL_GENERATE}
|
||||||
- VITE_AI_MODEL_FIX=${VITE_AI_MODEL_FIX}
|
- VITE_AI_MODEL_FIX=${VITE_AI_MODEL_FIX}
|
||||||
- VITE_AI_MODEL_POLISH=${VITE_AI_MODEL_POLISH}
|
- VITE_AI_MODEL_POLISH=${VITE_AI_MODEL_POLISH}
|
||||||
ports:
|
|
||||||
- "3000:80"
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -5,7 +5,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "vite build",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
import { DataProvider, CreateParams, UpdateParams, DeleteParams, DeleteManyParams } from 'react-admin';
|
|
||||||
import { dataProvider as baseDataProvider, supabase } from './supabaseProvider';
|
|
||||||
|
|
||||||
// Tables to audit
|
|
||||||
const AUDITED_TABLES = [
|
|
||||||
'question_packs',
|
|
||||||
'questions',
|
|
||||||
'profiles',
|
|
||||||
'couples',
|
|
||||||
'responses',
|
|
||||||
'matches',
|
|
||||||
'messages',
|
|
||||||
'subscriptions',
|
|
||||||
'feedback',
|
|
||||||
];
|
|
||||||
|
|
||||||
const logAction = async (
|
|
||||||
tableName: string,
|
|
||||||
recordId: string,
|
|
||||||
action: 'INSERT' | 'UPDATE' | 'DELETE',
|
|
||||||
oldValues?: Record<string, unknown> | null,
|
|
||||||
newValues?: Record<string, unknown> | null
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
await supabase.rpc('log_admin_action', {
|
|
||||||
p_table_name: tableName,
|
|
||||||
p_record_id: recordId,
|
|
||||||
p_action: action,
|
|
||||||
p_old_values: oldValues || null,
|
|
||||||
p_new_values: newValues || null,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to log audit action:', error);
|
|
||||||
// Don't throw - logging failure shouldn't block the operation
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const auditDataProvider: DataProvider = {
|
|
||||||
...baseDataProvider,
|
|
||||||
|
|
||||||
create: async <RecordType extends { id: string } = { id: string }>(
|
|
||||||
resource: string,
|
|
||||||
params: CreateParams
|
|
||||||
) => {
|
|
||||||
const result = await baseDataProvider.create<RecordType>(resource, params);
|
|
||||||
|
|
||||||
if (AUDITED_TABLES.includes(resource)) {
|
|
||||||
await logAction(resource, result.data.id, 'INSERT', null, result.data as Record<string, unknown>);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
update: async <RecordType extends { id: string } = { id: string }>(
|
|
||||||
resource: string,
|
|
||||||
params: UpdateParams
|
|
||||||
) => {
|
|
||||||
// Fetch old values before update
|
|
||||||
let oldValues: Record<string, unknown> | null = null;
|
|
||||||
if (AUDITED_TABLES.includes(resource)) {
|
|
||||||
const { data } = await supabase
|
|
||||||
.from(resource)
|
|
||||||
.select('*')
|
|
||||||
.eq('id', params.id)
|
|
||||||
.single();
|
|
||||||
oldValues = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await baseDataProvider.update<RecordType>(resource, params);
|
|
||||||
|
|
||||||
if (AUDITED_TABLES.includes(resource)) {
|
|
||||||
await logAction(resource, params.id as string, 'UPDATE', oldValues, result.data as Record<string, unknown>);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
delete: async <RecordType extends { id: string } = { id: string }>(
|
|
||||||
resource: string,
|
|
||||||
params: DeleteParams
|
|
||||||
) => {
|
|
||||||
// Fetch old values before delete
|
|
||||||
let oldValues: Record<string, unknown> | null = null;
|
|
||||||
if (AUDITED_TABLES.includes(resource)) {
|
|
||||||
const { data } = await supabase
|
|
||||||
.from(resource)
|
|
||||||
.select('*')
|
|
||||||
.eq('id', params.id)
|
|
||||||
.single();
|
|
||||||
oldValues = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await baseDataProvider.delete<RecordType>(resource, params);
|
|
||||||
|
|
||||||
if (AUDITED_TABLES.includes(resource)) {
|
|
||||||
await logAction(resource, params.id as string, 'DELETE', oldValues, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteMany: async (resource: string, params: DeleteManyParams) => {
|
|
||||||
// Fetch old values before delete
|
|
||||||
if (AUDITED_TABLES.includes(resource)) {
|
|
||||||
const { data: oldRecords } = await supabase
|
|
||||||
.from(resource)
|
|
||||||
.select('*')
|
|
||||||
.in('id', params.ids);
|
|
||||||
|
|
||||||
const result = await baseDataProvider.deleteMany(resource, params);
|
|
||||||
|
|
||||||
// Log each deletion
|
|
||||||
for (const record of oldRecords || []) {
|
|
||||||
await logAction(resource, record.id, 'DELETE', record, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseDataProvider.deleteMany(resource, params);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { supabaseDataProvider } from 'ra-supabase';
|
|
||||||
import { supabase } from '../config';
|
|
||||||
|
|
||||||
export { supabase };
|
|
||||||
|
|
||||||
const resources = {
|
|
||||||
categories: ['id', 'name', 'description', 'icon', 'sort_order', 'created_at'],
|
|
||||||
question_packs: ['id', 'name', 'description', 'icon', 'is_premium', 'is_public', 'is_explicit', 'sort_order', 'created_at'],
|
|
||||||
questions: ['id', 'pack_id', 'text', 'partner_text', 'intensity', 'allowed_couple_genders', 'target_user_genders', 'created_at'],
|
|
||||||
profiles: ['id', 'name', 'avatar_url', 'is_premium', 'couple_id', 'created_at'],
|
|
||||||
couples: ['id', 'invite_code', 'created_at'],
|
|
||||||
responses: ['id', 'user_id', 'question_id', 'couple_id', 'answer', 'created_at'],
|
|
||||||
matches: ['id', 'couple_id', 'question_id', 'match_type', 'is_new', 'created_at'],
|
|
||||||
messages: ['id', 'match_id', 'user_id', 'content', 'read_at', 'created_at'],
|
|
||||||
subscriptions: ['id', 'user_id', 'status', 'product_id', 'expires_at', 'created_at'],
|
|
||||||
feedback: ['id', 'user_id', 'type', 'title', 'description', 'status', 'created_at'],
|
|
||||||
audit_logs: ['id', 'table_name', 'record_id', 'action', 'old_values', 'new_values', 'changed_fields', 'admin_user_id', 'admin_role', 'created_at'],
|
|
||||||
admin_users: ['id', 'user_id', 'role', 'created_at'],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const dataProvider = supabaseDataProvider(supabase, resources);
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import {
|
|
||||||
List,
|
|
||||||
Datagrid,
|
|
||||||
TextField,
|
|
||||||
DateField,
|
|
||||||
FunctionField,
|
|
||||||
SelectInput,
|
|
||||||
TextInput,
|
|
||||||
DateInput,
|
|
||||||
} from 'react-admin';
|
|
||||||
|
|
||||||
const auditLogFilters = [
|
|
||||||
<TextInput key="table" label="Table" source="table_name" alwaysOn />,
|
|
||||||
<SelectInput
|
|
||||||
key="action"
|
|
||||||
source="action"
|
|
||||||
choices={[
|
|
||||||
{ id: 'INSERT', name: 'Create' },
|
|
||||||
{ id: 'UPDATE', name: 'Update' },
|
|
||||||
{ id: 'DELETE', name: 'Delete' },
|
|
||||||
]}
|
|
||||||
/>,
|
|
||||||
<DateInput key="from" source="created_at@gte" label="From Date" />,
|
|
||||||
<DateInput key="to" source="created_at@lte" label="To Date" />,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const AuditLogList = () => (
|
|
||||||
<List
|
|
||||||
filters={auditLogFilters}
|
|
||||||
sort={{ field: 'created_at', order: 'DESC' }}
|
|
||||||
>
|
|
||||||
<Datagrid bulkActionButtons={false}>
|
|
||||||
<DateField source="created_at" showTime label="Timestamp" />
|
|
||||||
<TextField source="table_name" label="Table" />
|
|
||||||
<FunctionField
|
|
||||||
label="Action"
|
|
||||||
render={(record: { action: string }) => {
|
|
||||||
const actionMap: Record<string, string> = {
|
|
||||||
INSERT: 'Create',
|
|
||||||
UPDATE: 'Update',
|
|
||||||
DELETE: 'Delete',
|
|
||||||
};
|
|
||||||
return actionMap[record.action] || record.action;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextField source="record_id" label="Record ID" />
|
|
||||||
<TextField source="admin_role" label="Role" />
|
|
||||||
<FunctionField
|
|
||||||
label="Changed Fields"
|
|
||||||
render={(record: { changed_fields?: string[] }) =>
|
|
||||||
record.changed_fields?.join(', ') || '-'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<FunctionField
|
|
||||||
label="Summary"
|
|
||||||
render={(record: { action: string; changed_fields?: string[] }) => {
|
|
||||||
if (record.action === 'INSERT') return 'New record created';
|
|
||||||
if (record.action === 'DELETE') return 'Record deleted';
|
|
||||||
return `${record.changed_fields?.length || 0} fields changed`;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Datagrid>
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import {
|
|
||||||
List,
|
|
||||||
Datagrid,
|
|
||||||
TextField,
|
|
||||||
DateField,
|
|
||||||
TextInput,
|
|
||||||
} from 'react-admin';
|
|
||||||
|
|
||||||
const coupleFilters = [
|
|
||||||
<TextInput key="invite" label="Invite Code" source="invite_code" alwaysOn />,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const CoupleList = () => (
|
|
||||||
<List filters={coupleFilters}>
|
|
||||||
<Datagrid bulkActionButtons={false}>
|
|
||||||
<TextField source="id" label="Couple ID" />
|
|
||||||
<TextField source="invite_code" label="Invite Code" />
|
|
||||||
<DateField source="created_at" label="Created" />
|
|
||||||
</Datagrid>
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import {
|
|
||||||
Show,
|
|
||||||
SimpleShowLayout,
|
|
||||||
TextField,
|
|
||||||
DateField,
|
|
||||||
ReferenceField,
|
|
||||||
ReferenceManyField,
|
|
||||||
List,
|
|
||||||
Datagrid,
|
|
||||||
BooleanField,
|
|
||||||
Labeled,
|
|
||||||
} from 'react-admin';
|
|
||||||
|
|
||||||
export const MatchShow = () => (
|
|
||||||
<Show>
|
|
||||||
<SimpleShowLayout>
|
|
||||||
<TextField source="id" label="Match ID" />
|
|
||||||
<ReferenceField source="question_id" reference="questions" label="Question">
|
|
||||||
<TextField source="text" />
|
|
||||||
</ReferenceField>
|
|
||||||
<TextField source="match_type" />
|
|
||||||
<BooleanField source="is_new" label="Unread" />
|
|
||||||
<DateField source="created_at" />
|
|
||||||
|
|
||||||
<Labeled label="Chat Messages" fullWidth>
|
|
||||||
<ReferenceManyField
|
|
||||||
reference="messages"
|
|
||||||
target="match_id"
|
|
||||||
sort={{ field: 'created_at', order: 'ASC' }}
|
|
||||||
>
|
|
||||||
<Datagrid bulkActionButtons={false}>
|
|
||||||
<ReferenceField source="user_id" reference="profiles" link={false}>
|
|
||||||
<TextField source="name" />
|
|
||||||
</ReferenceField>
|
|
||||||
<TextField source="content" label="Message" />
|
|
||||||
<DateField source="created_at" showTime label="Sent" />
|
|
||||||
<DateField source="read_at" showTime label="Read" emptyText="-" />
|
|
||||||
</Datagrid>
|
|
||||||
</ReferenceManyField>
|
|
||||||
</Labeled>
|
|
||||||
</SimpleShowLayout>
|
|
||||||
</Show>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const MatchList = () => (
|
|
||||||
<List>
|
|
||||||
<Datagrid rowClick="show" bulkActionButtons={false}>
|
|
||||||
<ReferenceField source="couple_id" reference="couples" link={false}>
|
|
||||||
<TextField source="id" />
|
|
||||||
</ReferenceField>
|
|
||||||
<ReferenceField source="question_id" reference="questions">
|
|
||||||
<TextField source="text" />
|
|
||||||
</ReferenceField>
|
|
||||||
<TextField source="match_type" />
|
|
||||||
<BooleanField source="is_new" label="Unread" />
|
|
||||||
<DateField source="created_at" />
|
|
||||||
</Datagrid>
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import {
|
|
||||||
List,
|
|
||||||
Datagrid,
|
|
||||||
TextField,
|
|
||||||
DateField,
|
|
||||||
ReferenceField,
|
|
||||||
TextInput,
|
|
||||||
} from 'react-admin';
|
|
||||||
|
|
||||||
const messageFilters = [
|
|
||||||
<TextInput key="content" label="Search content" source="content@ilike" alwaysOn />,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const MessageList = () => (
|
|
||||||
<List filters={messageFilters} sort={{ field: 'created_at', order: 'DESC' }}>
|
|
||||||
<Datagrid bulkActionButtons={false}>
|
|
||||||
<ReferenceField source="user_id" reference="profiles" label="Sender">
|
|
||||||
<TextField source="name" />
|
|
||||||
</ReferenceField>
|
|
||||||
<TextField source="content" />
|
|
||||||
<DateField source="created_at" showTime label="Sent At" />
|
|
||||||
<DateField source="read_at" showTime label="Read At" />
|
|
||||||
</Datagrid>
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { List, Datagrid, TextField, BooleanField, EditButton, Create, SimpleForm, TextInput, BooleanInput, Edit } from 'react-admin';
|
|
||||||
|
|
||||||
export const PackList = () => (
|
|
||||||
<List>
|
|
||||||
<Datagrid rowClick="edit">
|
|
||||||
<TextField source="name" />
|
|
||||||
<TextField source="description" />
|
|
||||||
<BooleanField source="is_premium" />
|
|
||||||
<BooleanField source="is_public" />
|
|
||||||
<EditButton />
|
|
||||||
</Datagrid>
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const PackEdit = () => (
|
|
||||||
<Edit>
|
|
||||||
<SimpleForm>
|
|
||||||
<TextInput source="name" />
|
|
||||||
<TextInput source="description" multiline />
|
|
||||||
<TextInput source="icon" />
|
|
||||||
<BooleanInput source="is_premium" />
|
|
||||||
<BooleanInput source="is_public" />
|
|
||||||
<TextInput source="sort_order" />
|
|
||||||
</SimpleForm>
|
|
||||||
</Edit>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const PackCreate = () => (
|
|
||||||
<Create>
|
|
||||||
<SimpleForm>
|
|
||||||
<TextInput source="name" />
|
|
||||||
<TextInput source="description" multiline />
|
|
||||||
<TextInput source="icon" />
|
|
||||||
<BooleanInput source="is_premium" />
|
|
||||||
<BooleanInput source="is_public" />
|
|
||||||
<TextInput source="sort_order" />
|
|
||||||
</SimpleForm>
|
|
||||||
</Create>
|
|
||||||
);
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
import {
|
|
||||||
List,
|
|
||||||
Datagrid,
|
|
||||||
TextField,
|
|
||||||
ReferenceField,
|
|
||||||
NumberField,
|
|
||||||
EditButton,
|
|
||||||
Create,
|
|
||||||
SimpleForm,
|
|
||||||
TextInput,
|
|
||||||
NumberInput,
|
|
||||||
ReferenceInput,
|
|
||||||
SelectInput,
|
|
||||||
Edit,
|
|
||||||
CheckboxGroupInput,
|
|
||||||
ArrayField,
|
|
||||||
ChipField,
|
|
||||||
SingleFieldList,
|
|
||||||
BooleanField,
|
|
||||||
FunctionField,
|
|
||||||
} from 'react-admin';
|
|
||||||
import { Typography, Box, Chip, Tooltip, IconButton } from '@mui/material';
|
|
||||||
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
|
|
||||||
|
|
||||||
const COUPLE_GENDER_CHOICES = [
|
|
||||||
{ id: 'male+female', name: 'Male + Female' },
|
|
||||||
{ id: 'male+male', name: 'Male + Male' },
|
|
||||||
{ id: 'female+female', name: 'Female + Female' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const TARGET_USER_GENDER_CHOICES = [
|
|
||||||
{ id: 'male', name: 'Male' },
|
|
||||||
{ id: 'female', name: 'Female' },
|
|
||||||
{ id: 'non-binary', name: 'Non-binary' },
|
|
||||||
{ id: 'prefer-not-to-say', name: 'Prefer not to say' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const LabelWithTooltip = ({ label, tooltip }: { label: string; tooltip: string }) => (
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
||||||
<Typography component="span">{label}</Typography>
|
|
||||||
<Tooltip title={tooltip} arrow placement="right">
|
|
||||||
<IconButton size="small" sx={{ p: 0.25 }}>
|
|
||||||
<HelpOutlineIcon sx={{ fontSize: 16, color: 'text.secondary' }} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
const GenderChips = ({ record, source }: { record?: any; source: string }) => {
|
|
||||||
const values = record?.[source];
|
|
||||||
if (!values || values.length === 0) {
|
|
||||||
return <Chip label="All" size="small" variant="outlined" />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
|
||||||
{values.map((v: string) => (
|
|
||||||
<Chip key={v} label={v} size="small" />
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const QuestionList = () => (
|
|
||||||
<List>
|
|
||||||
<Datagrid rowClick="edit">
|
|
||||||
<TextField source="text" />
|
|
||||||
<ReferenceField source="pack_id" reference="question_packs">
|
|
||||||
<TextField source="name" />
|
|
||||||
</ReferenceField>
|
|
||||||
<NumberField source="intensity" />
|
|
||||||
<FunctionField
|
|
||||||
label="Two-part"
|
|
||||||
render={(record: any) => record?.partner_text ? 'Yes' : 'No'}
|
|
||||||
/>
|
|
||||||
<FunctionField
|
|
||||||
label="Target Users"
|
|
||||||
render={(record: any) => {
|
|
||||||
const values = record?.target_user_genders;
|
|
||||||
if (!values || values.length === 0) return 'All';
|
|
||||||
return values.join(', ');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<EditButton />
|
|
||||||
</Datagrid>
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
|
|
||||||
const QuestionForm = () => (
|
|
||||||
<SimpleForm>
|
|
||||||
<ReferenceInput source="pack_id" reference="question_packs">
|
|
||||||
<SelectInput optionText="name" />
|
|
||||||
</ReferenceInput>
|
|
||||||
|
|
||||||
<Typography variant="subtitle2" sx={{ mt: 2, mb: 1 }}>
|
|
||||||
Question Text
|
|
||||||
</Typography>
|
|
||||||
<TextInput
|
|
||||||
source="text"
|
|
||||||
multiline
|
|
||||||
fullWidth
|
|
||||||
helperText="Primary text shown to the initiator (first person to see this question)"
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
source="partner_text"
|
|
||||||
multiline
|
|
||||||
fullWidth
|
|
||||||
helperText="Optional: Text shown to the partner after the initiator has answered. Leave blank for single-text questions."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<NumberInput source="intensity" min={1} max={5} />
|
|
||||||
|
|
||||||
<Typography variant="subtitle2" sx={{ mt: 3, mb: 1 }}>
|
|
||||||
Targeting
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<CheckboxGroupInput
|
|
||||||
source="target_user_genders"
|
|
||||||
label={
|
|
||||||
<LabelWithTooltip
|
|
||||||
label="Show to (as initiator)"
|
|
||||||
tooltip="The 'initiator' is the first person in the couple to see and answer a question. This filter controls which genders can be the initiator. Their partner will always see the question after the initiator answers, regardless of this setting."
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
choices={TARGET_USER_GENDER_CHOICES}
|
|
||||||
helperText="Leave empty to show to all genders as initiator."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CheckboxGroupInput
|
|
||||||
source="allowed_couple_genders"
|
|
||||||
label={
|
|
||||||
<LabelWithTooltip
|
|
||||||
label="Couple types"
|
|
||||||
tooltip="Filters which couple compositions can see this question. A 'male+female' couple has one male and one female partner. This ensures questions are only shown to couples where the content is relevant to their relationship dynamic."
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
choices={COUPLE_GENDER_CHOICES}
|
|
||||||
helperText="Leave empty to show to all couple types."
|
|
||||||
/>
|
|
||||||
</SimpleForm>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const QuestionEdit = () => (
|
|
||||||
<Edit>
|
|
||||||
<QuestionForm />
|
|
||||||
</Edit>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const QuestionCreate = () => (
|
|
||||||
<Create>
|
|
||||||
<QuestionForm />
|
|
||||||
</Create>
|
|
||||||
);
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import {
|
|
||||||
List,
|
|
||||||
Datagrid,
|
|
||||||
TextField,
|
|
||||||
BooleanField,
|
|
||||||
DateField,
|
|
||||||
ReferenceField,
|
|
||||||
Edit,
|
|
||||||
SimpleForm,
|
|
||||||
TextInput,
|
|
||||||
BooleanInput,
|
|
||||||
ReferenceInput,
|
|
||||||
SelectInput,
|
|
||||||
Show,
|
|
||||||
SimpleShowLayout,
|
|
||||||
ReferenceManyField,
|
|
||||||
useRecordContext,
|
|
||||||
TabbedShowLayout,
|
|
||||||
} from 'react-admin';
|
|
||||||
|
|
||||||
export const ProfileList = () => (
|
|
||||||
<List>
|
|
||||||
<Datagrid rowClick="show">
|
|
||||||
<TextField source="name" />
|
|
||||||
<BooleanField source="is_premium" />
|
|
||||||
<ReferenceField source="couple_id" reference="couples" link={false}>
|
|
||||||
<TextField source="id" />
|
|
||||||
</ReferenceField>
|
|
||||||
<DateField source="created_at" />
|
|
||||||
</Datagrid>
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const ProfileShow = () => (
|
|
||||||
<Show>
|
|
||||||
<TabbedShowLayout>
|
|
||||||
<TabbedShowLayout.Tab label="Profile">
|
|
||||||
<TextField source="id" />
|
|
||||||
<TextField source="name" />
|
|
||||||
<BooleanField source="is_premium" />
|
|
||||||
<ReferenceField source="couple_id" reference="couples">
|
|
||||||
<TextField source="invite_code" />
|
|
||||||
</ReferenceField>
|
|
||||||
<DateField source="created_at" />
|
|
||||||
</TabbedShowLayout.Tab>
|
|
||||||
|
|
||||||
<TabbedShowLayout.Tab label="Matches & Chats">
|
|
||||||
<ReferenceManyField
|
|
||||||
label="Matches"
|
|
||||||
reference="matches"
|
|
||||||
target="couple_id"
|
|
||||||
source="couple_id"
|
|
||||||
>
|
|
||||||
<Datagrid rowClick="show" bulkActionButtons={false}>
|
|
||||||
<ReferenceField source="question_id" reference="questions">
|
|
||||||
<TextField source="text" />
|
|
||||||
</ReferenceField>
|
|
||||||
<TextField source="match_type" />
|
|
||||||
<BooleanField source="is_new" label="Unread" />
|
|
||||||
<DateField source="created_at" />
|
|
||||||
</Datagrid>
|
|
||||||
</ReferenceManyField>
|
|
||||||
</TabbedShowLayout.Tab>
|
|
||||||
|
|
||||||
<TabbedShowLayout.Tab label="Responses">
|
|
||||||
<ReferenceManyField
|
|
||||||
label="Responses"
|
|
||||||
reference="responses"
|
|
||||||
target="user_id"
|
|
||||||
>
|
|
||||||
<Datagrid bulkActionButtons={false}>
|
|
||||||
<ReferenceField source="question_id" reference="questions">
|
|
||||||
<TextField source="text" />
|
|
||||||
</ReferenceField>
|
|
||||||
<TextField source="answer" />
|
|
||||||
<DateField source="created_at" />
|
|
||||||
</Datagrid>
|
|
||||||
</ReferenceManyField>
|
|
||||||
</TabbedShowLayout.Tab>
|
|
||||||
</TabbedShowLayout>
|
|
||||||
</Show>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const ProfileEdit = () => (
|
|
||||||
<Edit>
|
|
||||||
<SimpleForm>
|
|
||||||
<TextInput source="name" />
|
|
||||||
<BooleanInput source="is_premium" label="Premium Status" />
|
|
||||||
<ReferenceInput source="couple_id" reference="couples">
|
|
||||||
<SelectInput optionText="id" />
|
|
||||||
</ReferenceInput>
|
|
||||||
</SimpleForm>
|
|
||||||
</Edit>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const ResponseList = () => (
|
|
||||||
<List>
|
|
||||||
<Datagrid bulkActionButtons={false}>
|
|
||||||
<ReferenceField source="user_id" reference="profiles">
|
|
||||||
<TextField source="name" />
|
|
||||||
</ReferenceField>
|
|
||||||
<ReferenceField source="question_id" reference="questions">
|
|
||||||
<TextField source="text" />
|
|
||||||
</ReferenceField>
|
|
||||||
<TextField source="answer" />
|
|
||||||
<DateField source="created_at" />
|
|
||||||
</Datagrid>
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
Reference in new issue
Block a user