mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Update
This commit is contained in:
1 parent
358c2be0ee
commit
b10eae1bf3
26 files changed
+2509
-53
No files matched your search
@@ -0,0 +1,20 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
COPY turbo.json ./
|
||||
COPY apps/admin/package.json ./apps/admin/
|
||||
COPY packages/shared/package.json ./packages/shared/
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npx turbo run build --filter=@sauci/shared
|
||||
|
||||
WORKDIR /app/apps/admin
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
CMD ["npm", "run", "dev"]
|
||||
@@ -0,0 +1,16 @@
|
||||
services:
|
||||
admin:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: apps/admin/Dockerfile.dev
|
||||
ports:
|
||||
- "3000:3001"
|
||||
environment:
|
||||
- VITE_SUPABASE_URL=${VITE_SUPABASE_URL}
|
||||
- VITE_SUPABASE_ANON_KEY=${VITE_SUPABASE_ANON_KEY}
|
||||
volumes:
|
||||
- ./src:/app/apps/admin/src
|
||||
- ./index.html:/app/apps/admin/index.html
|
||||
- ./vite.config.ts:/app/apps/admin/vite.config.ts
|
||||
- ./tsconfig.json:/app/apps/admin/tsconfig.json
|
||||
restart: unless-stopped
|
||||
+72
-26
@@ -1,34 +1,80 @@
|
||||
import { Admin, Resource } from "react-admin";
|
||||
import { dataProvider } from "./providers/supabaseProvider";
|
||||
import { auditDataProvider } from "./providers/auditDataProvider";
|
||||
import { PackList, PackEdit, PackCreate } from "./resources/packs";
|
||||
import { QuestionList, QuestionEdit, QuestionCreate } from "./resources/questions";
|
||||
import { ProfileList, ResponseList } from "./resources/users";
|
||||
import { authProvider } from "./providers/authProvider";
|
||||
import { ProfileList, ProfileShow, ProfileEdit, ResponseList } from "./resources/users";
|
||||
import { CoupleList } from "./resources/couples";
|
||||
import { MatchList, MatchShow } from "./resources/matches";
|
||||
import { AuditLogList } from "./resources/auditLogs";
|
||||
import { authProvider, AdminPermissions } from "./providers/authProvider";
|
||||
|
||||
const App = () => (
|
||||
<Admin dataProvider={dataProvider} authProvider={authProvider} requireAuth>
|
||||
<Resource
|
||||
name="question_packs"
|
||||
list={PackList}
|
||||
edit={PackEdit}
|
||||
create={PackCreate}
|
||||
recordRepresentation="name"
|
||||
/>
|
||||
<Resource
|
||||
name="questions"
|
||||
list={QuestionList}
|
||||
edit={QuestionEdit}
|
||||
create={QuestionCreate}
|
||||
/>
|
||||
<Resource
|
||||
name="profiles"
|
||||
list={ProfileList}
|
||||
/>
|
||||
<Resource
|
||||
name="responses"
|
||||
list={ResponseList}
|
||||
/>
|
||||
<Resource name="couples" />
|
||||
<Admin
|
||||
dataProvider={auditDataProvider}
|
||||
authProvider={authProvider}
|
||||
requireAuth
|
||||
>
|
||||
{(permissions: AdminPermissions) => (
|
||||
<>
|
||||
{/* Question Packs - accessible to all admins */}
|
||||
<Resource
|
||||
name="question_packs"
|
||||
list={PackList}
|
||||
edit={PackEdit}
|
||||
create={PackCreate}
|
||||
recordRepresentation="name"
|
||||
options={{ label: 'Question Packs' }}
|
||||
/>
|
||||
|
||||
{/* Questions - accessible to all admins */}
|
||||
<Resource
|
||||
name="questions"
|
||||
list={QuestionList}
|
||||
edit={QuestionEdit}
|
||||
create={QuestionCreate}
|
||||
options={{ label: 'Questions' }}
|
||||
/>
|
||||
|
||||
{/* Super admin only resources */}
|
||||
{permissions?.role === 'super_admin' && (
|
||||
<>
|
||||
<Resource
|
||||
name="profiles"
|
||||
list={ProfileList}
|
||||
show={ProfileShow}
|
||||
edit={ProfileEdit}
|
||||
recordRepresentation="name"
|
||||
options={{ label: 'Users' }}
|
||||
/>
|
||||
<Resource
|
||||
name="couples"
|
||||
list={CoupleList}
|
||||
options={{ label: 'Relationships' }}
|
||||
/>
|
||||
<Resource
|
||||
name="matches"
|
||||
list={MatchList}
|
||||
show={MatchShow}
|
||||
options={{ label: 'Matches & Chats' }}
|
||||
/>
|
||||
<Resource
|
||||
name="responses"
|
||||
list={ResponseList}
|
||||
options={{ label: 'Responses' }}
|
||||
/>
|
||||
<Resource
|
||||
name="messages"
|
||||
options={{ label: 'Messages' }}
|
||||
/>
|
||||
<Resource
|
||||
name="audit_logs"
|
||||
list={AuditLogList}
|
||||
options={{ label: 'Audit Logs' }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Admin>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
export const supabaseConfig = {
|
||||
url: import.meta.env.VITE_SUPABASE_URL,
|
||||
anonKey: import.meta.env.VITE_SUPABASE_ANON_KEY,
|
||||
};
|
||||
|
||||
export const supabase = createClient(
|
||||
supabaseConfig.url,
|
||||
supabaseConfig.anonKey
|
||||
);
|
||||
@@ -0,0 +1,122 @@
|
||||
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,6 +1,37 @@
|
||||
import { AuthProvider } from 'react-admin';
|
||||
import { supabase } from '../config';
|
||||
|
||||
// Define admin roles
|
||||
export type AdminRole = 'pack_creator' | 'super_admin';
|
||||
|
||||
// Permission structure
|
||||
export interface AdminPermissions {
|
||||
role: AdminRole;
|
||||
permissions: Array<{ action: string; resource: string }>;
|
||||
}
|
||||
|
||||
// Define permissions by role
|
||||
const ROLE_PERMISSIONS: Record<AdminRole, Array<{ action: string; resource: string }>> = {
|
||||
pack_creator: [
|
||||
{ action: 'list', resource: 'question_packs' },
|
||||
{ action: 'create', resource: 'question_packs' },
|
||||
{ action: 'edit', resource: 'question_packs' },
|
||||
{ action: 'delete', resource: 'question_packs' },
|
||||
{ action: 'show', resource: 'question_packs' },
|
||||
{ action: 'list', resource: 'questions' },
|
||||
{ action: 'create', resource: 'questions' },
|
||||
{ action: 'edit', resource: 'questions' },
|
||||
{ action: 'delete', resource: 'questions' },
|
||||
{ action: 'show', resource: 'questions' },
|
||||
],
|
||||
super_admin: [
|
||||
// Full access to all resources
|
||||
{ action: '*', resource: '*' },
|
||||
],
|
||||
};
|
||||
|
||||
const PERMISSIONS_KEY = 'admin_permissions';
|
||||
|
||||
export const authProvider: AuthProvider = {
|
||||
login: async ({ username, password }) => {
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
@@ -10,21 +41,96 @@ export const authProvider: AuthProvider = {
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
// Fetch admin role
|
||||
const { data: adminData, error: adminError } = await supabase
|
||||
.from('admin_users')
|
||||
.select('role')
|
||||
.eq('user_id', data.user.id)
|
||||
.single();
|
||||
|
||||
if (adminError || !adminData) {
|
||||
await supabase.auth.signOut();
|
||||
throw new Error('Access denied. You are not an administrator.');
|
||||
}
|
||||
|
||||
// Store permissions
|
||||
const permissions: AdminPermissions = {
|
||||
role: adminData.role as AdminRole,
|
||||
permissions: ROLE_PERMISSIONS[adminData.role as AdminRole],
|
||||
};
|
||||
localStorage.setItem(PERMISSIONS_KEY, JSON.stringify(permissions));
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
localStorage.removeItem(PERMISSIONS_KEY);
|
||||
await supabase.auth.signOut();
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
checkError: async (error) => {
|
||||
if (error.status === 401 || error.status === 403) {
|
||||
localStorage.removeItem(PERMISSIONS_KEY);
|
||||
return Promise.reject();
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
checkAuth: async () => {
|
||||
const session = await supabase.auth.getSession();
|
||||
return session.data.session ? Promise.resolve() : Promise.reject();
|
||||
const permissions = localStorage.getItem(PERMISSIONS_KEY);
|
||||
|
||||
if (!session.data.session || !permissions) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
getPermissions: async () => {
|
||||
const permissions = localStorage.getItem(PERMISSIONS_KEY);
|
||||
if (!permissions) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return JSON.parse(permissions) as AdminPermissions;
|
||||
},
|
||||
|
||||
canAccess: async ({ resource, action }) => {
|
||||
const permissionsStr = localStorage.getItem(PERMISSIONS_KEY);
|
||||
if (!permissionsStr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { permissions } = JSON.parse(permissionsStr) as AdminPermissions;
|
||||
|
||||
// Check for wildcard (super_admin)
|
||||
if (permissions.some(p => p.action === '*' && p.resource === '*')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check specific permission
|
||||
return permissions.some(
|
||||
p => p.resource === resource && (p.action === action || p.action === '*')
|
||||
);
|
||||
},
|
||||
|
||||
getIdentity: async () => {
|
||||
const { data } = await supabase.auth.getUser();
|
||||
if (!data.user) {
|
||||
throw new Error('No user');
|
||||
}
|
||||
|
||||
const permissionsStr = localStorage.getItem(PERMISSIONS_KEY);
|
||||
const role = permissionsStr
|
||||
? (JSON.parse(permissionsStr) as AdminPermissions).role
|
||||
: 'unknown';
|
||||
|
||||
return {
|
||||
id: data.user.id,
|
||||
fullName: data.user.email,
|
||||
avatar: undefined,
|
||||
role,
|
||||
};
|
||||
},
|
||||
getPermissions: () => Promise.resolve(),
|
||||
};
|
||||
@@ -1,22 +1,20 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { supabaseConfig } from '../config';
|
||||
|
||||
export const supabase = createClient(
|
||||
supabaseConfig.url,
|
||||
supabaseConfig.anonKey
|
||||
);
|
||||
|
||||
// Custom data provider wrapper if needed for ra-data-supabase
|
||||
import { supabaseDataProvider } from 'ra-supabase';
|
||||
import { supabase } from '../config';
|
||||
|
||||
export const dataProvider = supabaseDataProvider(
|
||||
supabase,
|
||||
{
|
||||
resources: {
|
||||
question_packs: 'question_packs',
|
||||
questions: 'questions',
|
||||
profiles: 'profiles',
|
||||
couples: 'couples'
|
||||
}
|
||||
}
|
||||
);
|
||||
export { supabase };
|
||||
|
||||
const resources = {
|
||||
question_packs: ['id', 'name', 'description', 'icon', 'is_premium', 'is_public', 'sort_order', 'created_at'],
|
||||
questions: ['id', 'pack_id', 'text', 'partner_text', 'intensity', '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);
|
||||
@@ -0,0 +1,64 @@
|
||||
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>
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
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>
|
||||
);
|
||||
@@ -0,0 +1,59 @@
|
||||
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>
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
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,12 +1,29 @@
|
||||
import { List, Datagrid, TextField, BooleanField, DateField, ReferenceField } from 'react-admin';
|
||||
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>
|
||||
<Datagrid rowClick="show">
|
||||
<TextField source="name" />
|
||||
<TextField source="email" />
|
||||
<BooleanField source="is_premium" />
|
||||
<ReferenceField source="couple_id" reference="couples">
|
||||
<ReferenceField source="couple_id" reference="couples" link={false}>
|
||||
<TextField source="id" />
|
||||
</ReferenceField>
|
||||
<DateField source="created_at" />
|
||||
@@ -14,9 +31,71 @@ export const ProfileList = () => (
|
||||
</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>
|
||||
<Datagrid bulkActionButtons={false}>
|
||||
<ReferenceField source="user_id" reference="profiles">
|
||||
<TextField source="name" />
|
||||
</ReferenceField>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -6,5 +6,8 @@ export default defineConfig({
|
||||
server: {
|
||||
host: true,
|
||||
port: 3001,
|
||||
watch: {
|
||||
usePolling: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user