mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-06-12 09:50:55 +02:00
Merge pull request #378 from wasp-lang/filip-refactor-user-module
This commit is contained in:
commit
64dcb48234
@ -1,14 +1,14 @@
|
||||
--- template/app/src/admin/dashboards/users/UsersTable.tsx
|
||||
+++ opensaas-sh/app/src/admin/dashboards/users/UsersTable.tsx
|
||||
@@ -9,6 +9,7 @@
|
||||
@@ -17,6 +17,7 @@
|
||||
const [skip, setskip] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [email, setEmail] = useState<string | undefined>(undefined);
|
||||
+
|
||||
const [isAdminFilter, setIsAdminFilter] = useState<boolean | undefined>(undefined);
|
||||
const [statusOptions, setStatusOptions] = useState<SubscriptionStatus[]>([]);
|
||||
const { data, isLoading, error } = useQuery(getPaginatedUsers, {
|
||||
@@ -211,7 +212,7 @@
|
||||
const { data, isLoading } = useQuery(getPaginatedUsers, {
|
||||
@@ -223,7 +224,7 @@
|
||||
<p className='text-sm text-black dark:text-white'>{user.subscriptionStatus}</p>
|
||||
</div>
|
||||
<div className='col-span-2 flex items-center'>
|
||||
|
@ -1,18 +1,27 @@
|
||||
--- template/app/src/user/operations.ts
|
||||
+++ opensaas-sh/app/src/user/operations.ts
|
||||
@@ -41,7 +41,10 @@
|
||||
@@ -2,7 +2,7 @@
|
||||
import { type UpdateIsUserAdminById, type GetPaginatedUsers } from 'wasp/server/operations';
|
||||
import { type User } from 'wasp/entities';
|
||||
import { HttpError } from 'wasp/server';
|
||||
-import { subscriptionStatusSchema, type SubscriptionStatus } from '../payment/plans';
|
||||
+import { subscriptionStatusSchema } from '../payment/plans';
|
||||
import { ensureArgsSchemaOrThrowHttpError } from '../server/validation';
|
||||
|
||||
const updateUserAdminByIdInputSchema = z.object({
|
||||
@@ -33,10 +33,7 @@
|
||||
};
|
||||
|
||||
type GetPaginatedUsersOutput = {
|
||||
- users: Pick<User, 'id' | 'email' | 'username' | 'subscriptionStatus' | 'paymentProcessorUserId'>[];
|
||||
+ users: Pick<
|
||||
+ User,
|
||||
+ 'id' | 'email' | 'username' | 'subscriptionStatus' | 'stripeId'
|
||||
+ >[];
|
||||
- users: Pick<
|
||||
- User,
|
||||
- 'id' | 'email' | 'username' | 'subscriptionStatus' | 'paymentProcessorUserId' | 'isAdmin'
|
||||
- >[];
|
||||
+ users: Pick<User, 'id' | 'email' | 'username' | 'subscriptionStatus' | 'stripeId' | 'isAdmin'>[];
|
||||
totalPages: number;
|
||||
};
|
||||
|
||||
@@ -85,6 +88,7 @@
|
||||
@@ -80,6 +77,7 @@
|
||||
mode: 'insensitive',
|
||||
},
|
||||
isAdmin,
|
||||
@ -20,7 +29,7 @@
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
@@ -108,7 +112,7 @@
|
||||
@@ -103,7 +101,7 @@
|
||||
username: true,
|
||||
isAdmin: true,
|
||||
subscriptionStatus: true,
|
||||
@ -29,7 +38,7 @@
|
||||
},
|
||||
orderBy: {
|
||||
id: 'desc',
|
||||
@@ -124,6 +128,7 @@
|
||||
@@ -119,6 +117,7 @@
|
||||
mode: 'insensitive',
|
||||
},
|
||||
isAdmin,
|
||||
|
@ -1,33 +0,0 @@
|
||||
import { type User } from 'wasp/entities';
|
||||
import { useState } from 'react';
|
||||
import { cn } from '../../../client/cn';
|
||||
|
||||
const SwitcherOne = ({ user, updateIsUserAdminById }: { user?: Partial<User>; updateIsUserAdminById?: any }) => {
|
||||
const [enabled, setEnabled] = useState<boolean>(user?.isAdmin || false);
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<label htmlFor={`toggle1-${user?.id}`} className='flex cursor-pointer select-none items-center'>
|
||||
<div className='relative'>
|
||||
<input
|
||||
type='checkbox'
|
||||
id={`toggle1-${user?.id}`}
|
||||
className='sr-only'
|
||||
onChange={() => {
|
||||
setEnabled(!enabled);
|
||||
updateIsUserAdminById && updateIsUserAdminById({ id: user?.id, data: { isAdmin: !enabled } });
|
||||
}}
|
||||
/>
|
||||
<div className='reblock h-8 w-14 rounded-full bg-meta-9 dark:bg-[#5A616B]'></div>
|
||||
<div
|
||||
className={cn('absolute left-1 top-1 h-6 w-6 rounded-full bg-white dark:bg-gray-400 transition', {
|
||||
'!right-1 !translate-x-full !bg-primary dark:!bg-white': enabled,
|
||||
})}
|
||||
></div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SwitcherOne;
|
@ -1,17 +1,25 @@
|
||||
import { type SubscriptionStatus } from '../../../payment/plans';
|
||||
import { updateIsUserAdminById, useQuery, getPaginatedUsers } from 'wasp/client/operations';
|
||||
import { useQuery, getPaginatedUsers } from 'wasp/client/operations';
|
||||
import { useState, useEffect } from 'react';
|
||||
import SwitcherOne from './SwitcherOne';
|
||||
import SwitcherOne from '../../elements/forms/SwitcherOne';
|
||||
import LoadingSpinner from '../../layout/LoadingSpinner';
|
||||
import DropdownEditDelete from './DropdownEditDelete';
|
||||
import { updateIsUserAdminById } from 'wasp/client/operations';
|
||||
import { type User } from 'wasp/entities';
|
||||
|
||||
const UsersTable = () => {
|
||||
function AdminSwitch({ id, isAdmin }: Pick<User, 'id' | 'isAdmin'>) {
|
||||
return (
|
||||
<SwitcherOne isOn={isAdmin} onChange={(value) => updateIsUserAdminById({ id: id, isAdmin: value })} />
|
||||
);
|
||||
}
|
||||
|
||||
function UsersTable() {
|
||||
const [skip, setskip] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [email, setEmail] = useState<string | undefined>(undefined);
|
||||
const [isAdminFilter, setIsAdminFilter] = useState<boolean | undefined>(undefined);
|
||||
const [statusOptions, setStatusOptions] = useState<SubscriptionStatus[]>([]);
|
||||
const { data, isLoading, error } = useQuery(getPaginatedUsers, {
|
||||
const { data, isLoading } = useQuery(getPaginatedUsers, {
|
||||
skip,
|
||||
emailContains: email,
|
||||
isAdmin: isAdminFilter,
|
||||
@ -51,7 +59,7 @@ const UsersTable = () => {
|
||||
<div className='flex-grow relative z-20 rounded border border-stroke pr-8 outline-none bg-white transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input'>
|
||||
<div className='flex items-center'>
|
||||
{!!statusOptions && statusOptions.length > 0 ? (
|
||||
statusOptions.map((opt, idx) => (
|
||||
statusOptions.map((opt) => (
|
||||
<span
|
||||
key={opt}
|
||||
className='z-30 flex items-center my-1 mx-2 py-1 px-2 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
|
||||
@ -109,7 +117,11 @@ const UsersTable = () => {
|
||||
<option value=''>Select filters</option>
|
||||
{['past_due', 'canceled', 'active', 'deleted', null].map((status) => {
|
||||
if (!statusOptions.includes(status as SubscriptionStatus)) {
|
||||
return <option value={status || ''}>{status ? status : 'has not subscribed'}</option>;
|
||||
return (
|
||||
<option key={status} value={status || ''}>
|
||||
{status ? status : 'has not subscribed'}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</select>
|
||||
@ -215,7 +227,7 @@ const UsersTable = () => {
|
||||
</div>
|
||||
<div className='col-span-1 flex items-center'>
|
||||
<div className='text-sm text-black dark:text-white'>
|
||||
<SwitcherOne user={user} updateIsUserAdminById={updateIsUserAdminById} />
|
||||
<AdminSwitch {...user} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='col-span-1 flex items-center'>
|
||||
@ -226,6 +238,6 @@ const UsersTable = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default UsersTable;
|
||||
|
@ -2,9 +2,10 @@ import { type AuthUser } from 'wasp/auth';
|
||||
import Breadcrumb from '../../layout/Breadcrumb';
|
||||
import DefaultLayout from '../../layout/DefaultLayout';
|
||||
import CheckboxOne from './CheckboxOne';
|
||||
import SwitcherOne from '../../dashboards/users/SwitcherOne';
|
||||
import SwitcherTwo from './SwitcherTwo';
|
||||
import SwitcherOne from './SwitcherOne';
|
||||
import { useRedirectHomeUnlessUserIsAdmin } from '../../useRedirectHomeUnlessUserIsAdmin';
|
||||
import { useState } from 'react';
|
||||
|
||||
const FormElements = ({ user }: { user: AuthUser }) => {
|
||||
useRedirectHomeUnlessUserIsAdmin({ user });
|
||||
@ -56,10 +57,7 @@ const FormElements = ({ user }: { user: AuthUser }) => {
|
||||
<div className='border-b border-stroke py-4 px-6.5 dark:border-strokedark'>
|
||||
<h3 className='font-medium text-black dark:text-white'>Toggle switch input</h3>
|
||||
</div>
|
||||
<div className='flex flex-col gap-5.5 p-6.5'>
|
||||
<SwitcherOne />
|
||||
<SwitcherTwo />
|
||||
</div>
|
||||
<SwitchExamples />
|
||||
</div>
|
||||
|
||||
{/* <!-- Time and date --> */}
|
||||
@ -172,6 +170,70 @@ const FormElements = ({ user }: { user: AuthUser }) => {
|
||||
<label className='mb-3 block text-black dark:text-white'>Select Country</label>
|
||||
<div className='relative z-20 bg-white dark:bg-form-input'>
|
||||
<span className='absolute top-1/2 left-4 z-30 -translate-y-1/2'>
|
||||
<GlobeIcon />
|
||||
</span>
|
||||
<select className='relative z-20 w-full appearance-none rounded border border-stroke bg-transparent py-3 px-12 outline-none transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input'>
|
||||
<option value=''>USA</option>
|
||||
<option value=''>UK</option>
|
||||
<option value=''>Canada</option>
|
||||
</select>
|
||||
<span className='absolute top-1/2 right-4 z-10 -translate-y-1/2'>
|
||||
<ChevronDownIcon />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='mb-3 block text-black dark:text-white'>Multiselect Dropdown</label>
|
||||
<div className='relative z-20 w-full rounded border border-stroke p-1.5 pr-8 font-medium outline-none transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input'>
|
||||
<div className='flex flex-wrap items-center'>
|
||||
<span className='m-1.5 flex items-center justify-center rounded border-[.5px] border-stroke bg-gray py-1.5 px-2.5 text-sm font-medium dark:border-strokedark dark:bg-white/30'>
|
||||
Design
|
||||
<span className='cursor-pointer pl-2 hover:text-danger'>
|
||||
<XIcon />
|
||||
</span>
|
||||
</span>
|
||||
<span className='m-1.5 flex items-center justify-center rounded border-[.5px] border-stroke bg-gray py-1.5 px-2.5 text-sm font-medium dark:border-strokedark dark:bg-white/30'>
|
||||
Development
|
||||
<span className='cursor-pointer pl-2 hover:text-danger'>
|
||||
<XIcon />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<select
|
||||
name=''
|
||||
id=''
|
||||
className='absolute top-0 left-0 z-20 h-full w-full bg-transparent opacity-0'
|
||||
>
|
||||
<option value=''>Option</option>
|
||||
<option value=''>Option</option>
|
||||
</select>
|
||||
<span className='absolute top-1/2 right-4 z-10 -translate-y-1/2'>
|
||||
<ChevronDownIcon />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
);
|
||||
};
|
||||
|
||||
function SwitchExamples() {
|
||||
const [isFirstOn, setIsFirstOn] = useState<boolean>(false);
|
||||
const [isSecondOn, setIsSecondOn] = useState<boolean>(false);
|
||||
return (
|
||||
<div className='flex flex-col gap-5.5 p-6.5'>
|
||||
<SwitcherOne isOn={isFirstOn} onChange={setIsFirstOn} />
|
||||
<SwitcherTwo isOn={isSecondOn} onChange={setIsSecondOn} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GlobeIcon() {
|
||||
return (
|
||||
<svg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<g opacity='0.8'>
|
||||
<path
|
||||
@ -194,82 +256,35 @@ const FormElements = ({ user }: { user: AuthUser }) => {
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
<select className='relative z-20 w-full appearance-none rounded border border-stroke bg-transparent py-3 px-12 outline-none transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input'>
|
||||
<option value=''>USA</option>
|
||||
<option value=''>UK</option>
|
||||
<option value=''>Canada</option>
|
||||
</select>
|
||||
<span className='absolute top-1/2 right-4 z-10 -translate-y-1/2'>
|
||||
<svg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<g opacity='0.8'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M5.29289 8.29289C5.68342 7.90237 6.31658 7.90237 6.70711 8.29289L12 13.5858L17.2929 8.29289C17.6834 7.90237 18.3166 7.90237 18.7071 8.29289C19.0976 8.68342 19.0976 9.31658 18.7071 9.70711L12.7071 15.7071C12.3166 16.0976 11.6834 16.0976 11.2929 15.7071L5.29289 9.70711C4.90237 9.31658 4.90237 8.68342 5.29289 8.29289Z'
|
||||
fill='#637381'
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='mb-3 block text-black dark:text-white'>Multiselect Dropdown</label>
|
||||
<div className='relative z-20 w-full rounded border border-stroke p-1.5 pr-8 font-medium outline-none transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input'>
|
||||
<div className='flex flex-wrap items-center'>
|
||||
<span className='m-1.5 flex items-center justify-center rounded border-[.5px] border-stroke bg-gray py-1.5 px-2.5 text-sm font-medium dark:border-strokedark dark:bg-white/30'>
|
||||
Design
|
||||
<span className='cursor-pointer pl-2 hover:text-danger'>
|
||||
<svg width='12' height='12' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M9.35355 3.35355C9.54882 3.15829 9.54882 2.84171 9.35355 2.64645C9.15829 2.45118 8.84171 2.45118 8.64645 2.64645L6 5.29289L3.35355 2.64645C3.15829 2.45118 2.84171 2.45118 2.64645 2.64645C2.45118 2.84171 2.45118 3.15829 2.64645 3.35355L5.29289 6L2.64645 8.64645C2.45118 8.84171 2.45118 9.15829 2.64645 9.35355C2.84171 9.54882 3.15829 9.54882 3.35355 9.35355L6 6.70711L8.64645 9.35355C8.84171 9.54882 9.15829 9.54882 9.35355 9.35355C9.54882 9.15829 9.54882 8.84171 9.35355 8.64645L6.70711 6L9.35355 3.35355Z'
|
||||
fill='currentColor'
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
<span className='m-1.5 flex items-center justify-center rounded border-[.5px] border-stroke bg-gray py-1.5 px-2.5 text-sm font-medium dark:border-strokedark dark:bg-white/30'>
|
||||
Development
|
||||
<span className='cursor-pointer pl-2 hover:text-danger'>
|
||||
<svg width='12' height='12' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M9.35355 3.35355C9.54882 3.15829 9.54882 2.84171 9.35355 2.64645C9.15829 2.45118 8.84171 2.45118 8.64645 2.64645L6 5.29289L3.35355 2.64645C3.15829 2.45118 2.84171 2.45118 2.64645 2.64645C2.45118 2.84171 2.45118 3.15829 2.64645 3.35355L5.29289 6L2.64645 8.64645C2.45118 8.84171 2.45118 9.15829 2.64645 9.35355C2.84171 9.54882 3.15829 9.54882 3.35355 9.35355L6 6.70711L8.64645 9.35355C8.84171 9.54882 9.15829 9.54882 9.35355 9.35355C9.54882 9.15829 9.54882 8.84171 9.35355 8.64645L6.70711 6L9.35355 3.35355Z'
|
||||
fill='currentColor'
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<select name='' id='' className='absolute top-0 left-0 z-20 h-full w-full bg-transparent opacity-0'>
|
||||
<option value=''>Option</option>
|
||||
<option value=''>Option</option>
|
||||
</select>
|
||||
<span className='absolute top-1/2 right-4 z-10 -translate-y-1/2'>
|
||||
<svg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<g opacity='0.8'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M5.29289 8.29289C5.68342 7.90237 6.31658 7.90237 6.70711 8.29289L12 13.5858L17.2929 8.29289C17.6834 7.90237 18.3166 7.90237 18.7071 8.29289C19.0976 8.68342 19.0976 9.31658 18.7071 9.70711L12.7071 15.7071C12.3166 16.0976 11.6834 16.0976 11.2929 15.7071L5.29289 9.70711C4.90237 9.31658 4.90237 8.68342 5.29289 8.29289Z'
|
||||
fill='#637381'
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function ChevronDownIcon() {
|
||||
return (
|
||||
<svg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<g opacity='0.8'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M5.29289 8.29289C5.68342 7.90237 6.31658 7.90237 6.70711 8.29289L12 13.5858L17.2929 8.29289C17.6834 7.90237 18.3166 7.90237 18.7071 8.29289C19.0976 8.68342 19.0976 9.31658 18.7071 9.70711L12.7071 15.7071C12.3166 16.0976 11.6834 16.0976 11.2929 15.7071L5.29289 9.70711C4.90237 9.31658 4.90237 8.68342 5.29289 8.29289Z'
|
||||
fill='#637381'
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function XIcon() {
|
||||
return (
|
||||
<svg width='12' height='12' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M9.35355 3.35355C9.54882 3.15829 9.54882 2.84171 9.35355 2.64645C9.15829 2.45118 8.84171 2.45118 8.64645 2.64645L6 5.29289L3.35355 2.64645C3.15829 2.45118 2.84171 2.45118 2.64645 2.64645C2.45118 2.84171 2.45118 3.15829 2.64645 3.35355L5.29289 6L2.64645 8.64645C2.45118 8.84171 2.45118 9.15829 2.64645 9.35355C2.84171 9.54882 3.15829 9.54882 3.35355 9.35355L6 6.70711L8.64645 9.35355C8.84171 9.54882 9.15829 9.54882 9.35355 9.35355C9.54882 9.15829 9.54882 8.84171 9.35355 8.64645L6.70711 6L9.35355 3.35355Z'
|
||||
fill='currentColor'
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default FormElements;
|
||||
|
30
template/app/src/admin/elements/forms/SwitcherOne.tsx
Normal file
30
template/app/src/admin/elements/forms/SwitcherOne.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { useId } from 'react';
|
||||
import { cn } from '../../../client/cn';
|
||||
|
||||
function SwitcherOne({ isOn, onChange }: { isOn: boolean; onChange: (value: boolean) => void }) {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<label htmlFor={id} className='flex cursor-pointer select-none items-center'>
|
||||
<div className='relative'>
|
||||
<input
|
||||
id={id}
|
||||
type='checkbox'
|
||||
className='sr-only'
|
||||
checked={isOn}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
/>
|
||||
<div className='reblock h-8 w-14 rounded-full bg-meta-9 dark:bg-[#5A616B]'></div>
|
||||
<div
|
||||
className={cn('absolute left-1 top-1 h-6 w-6 rounded-full bg-white dark:bg-gray-400 transition', {
|
||||
'!right-1 !translate-x-full !bg-primary dark:!bg-white': isOn,
|
||||
})}
|
||||
></div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SwitcherOne;
|
@ -1,31 +1,57 @@
|
||||
import { useState } from 'react';
|
||||
import { useId } from 'react';
|
||||
import { cn } from '../../../client/cn';
|
||||
|
||||
const SwitcherTwo = () => {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
function SwitcherTwo({ isOn, onChange }: { isOn: boolean; onChange: (value: boolean) => void }) {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor='toggle3' className='flex cursor-pointer select-none items-center'>
|
||||
<label htmlFor={id} className='flex cursor-pointer select-none items-center'>
|
||||
<div className='relative'>
|
||||
<input
|
||||
type='checkbox'
|
||||
id='toggle3'
|
||||
id={id}
|
||||
className='sr-only'
|
||||
onChange={() => {
|
||||
setEnabled(!enabled);
|
||||
}}
|
||||
checked={isOn}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
/>
|
||||
<div className='block h-8 w-14 rounded-full bg-meta-9 dark:bg-[#5A616B]'></div>
|
||||
<div
|
||||
className={cn(
|
||||
'dot absolute left-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-white transition',
|
||||
{
|
||||
'!right-1 !translate-x-full !bg-primary dark:!bg-white': enabled,
|
||||
'!right-1 !translate-x-full !bg-primary dark:!bg-white': isOn,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className={cn('hidden', { '!block': enabled })}>
|
||||
<span className={cn('hidden', { '!block': isOn })}>
|
||||
<CheckIcon />
|
||||
</span>
|
||||
<span className={cn({ hidden: isOn })}>
|
||||
<XIcon />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const XIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
className='h-4 w-4 stroke-current'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M6 18L18 6M6 6l12 12'></path>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const CheckIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
className='fill-white dark:fill-black'
|
||||
width='11'
|
||||
@ -41,25 +67,6 @@ const SwitcherTwo = () => {
|
||||
strokeWidth='0.4'
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
className={cn({
|
||||
hidden: enabled,
|
||||
})}
|
||||
>
|
||||
<svg
|
||||
className='h-4 w-4 stroke-current'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M6 18L18 6M6 6l12 12'></path>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -7,9 +7,7 @@ import { ensureArgsSchemaOrThrowHttpError } from '../server/validation';
|
||||
|
||||
const updateUserAdminByIdInputSchema = z.object({
|
||||
id: z.string().nonempty(),
|
||||
data: z.object({
|
||||
isAdmin: z.boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
type UpdateUserAdminByIdInput = z.infer<typeof updateUserAdminByIdInputSchema>;
|
||||
@ -18,30 +16,27 @@ export const updateIsUserAdminById: UpdateIsUserAdminById<UpdateUserAdminByIdInp
|
||||
rawArgs,
|
||||
context
|
||||
) => {
|
||||
const { id, data } = ensureArgsSchemaOrThrowHttpError(updateUserAdminByIdInputSchema, rawArgs);
|
||||
const { id, isAdmin } = ensureArgsSchemaOrThrowHttpError(updateUserAdminByIdInputSchema, rawArgs);
|
||||
|
||||
if (!context.user) {
|
||||
throw new HttpError(401);
|
||||
throw new HttpError(401, 'Only authenticated users are allowed to perform this operation');
|
||||
}
|
||||
|
||||
if (!context.user.isAdmin) {
|
||||
throw new HttpError(403);
|
||||
throw new HttpError(403, 'Only admins are allowed to perform this operation');
|
||||
}
|
||||
|
||||
const updatedUser = await context.entities.User.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
isAdmin: data.isAdmin,
|
||||
},
|
||||
return context.entities.User.update({
|
||||
where: { id },
|
||||
data: { isAdmin },
|
||||
});
|
||||
|
||||
return updatedUser;
|
||||
};
|
||||
|
||||
type GetPaginatedUsersOutput = {
|
||||
users: Pick<User, 'id' | 'email' | 'username' | 'subscriptionStatus' | 'paymentProcessorUserId'>[];
|
||||
users: Pick<
|
||||
User,
|
||||
'id' | 'email' | 'username' | 'subscriptionStatus' | 'paymentProcessorUserId' | 'isAdmin'
|
||||
>[];
|
||||
totalPages: number;
|
||||
};
|
||||
|
||||
@ -59,7 +54,7 @@ export const getPaginatedUsers: GetPaginatedUsers<GetPaginatedUsersInput, GetPag
|
||||
rawArgs,
|
||||
context
|
||||
) => {
|
||||
const { skip, cursor, emailContains, isAdmin, subscriptionStatus } = ensureArgsSchemaOrThrowHttpError(
|
||||
const { skip, emailContains, isAdmin, subscriptionStatus } = ensureArgsSchemaOrThrowHttpError(
|
||||
getPaginatorArgsSchema,
|
||||
rawArgs
|
||||
);
|
||||
@ -70,7 +65,7 @@ export const getPaginatedUsers: GetPaginatedUsers<GetPaginatedUsersInput, GetPag
|
||||
|
||||
const allSubscriptionStatusOptions = subscriptionStatus;
|
||||
const hasNotSubscribed = allSubscriptionStatusOptions?.find((status) => status === null);
|
||||
let subscriptionStatusStrings = allSubscriptionStatusOptions?.filter((status) => status !== null) as
|
||||
const subscriptionStatusStrings = allSubscriptionStatusOptions?.filter((status) => status !== null) as
|
||||
| string[]
|
||||
| undefined;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user