feat: UI improvements & migration script

This commit is contained in:
swve 2024-11-23 20:24:39 +01:00
parent e464b30147
commit 6cd1cf7e9c
5 changed files with 154 additions and 63 deletions

View file

@ -0,0 +1,90 @@
"""Payments
Revision ID: 0314ec7791e1
Revises: 040ccb1d456e
Create Date: 2024-11-23 19:41:14.064680
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa # noqa: F401
import sqlmodel # noqa: F401
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '0314ec7791e1'
down_revision: Union[str, None] = '040ccb1d456e'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('paymentsconfig',
sa.Column('enabled', sa.Boolean(), nullable=False),
sa.Column('active', sa.Boolean(), nullable=False),
sa.Column('provider', postgresql.ENUM('STRIPE', name='paymentproviderenum', create_type=False), nullable=False),
sa.Column('provider_specific_id', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('provider_config', sa.JSON(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('org_id', sa.BigInteger(), nullable=True),
sa.Column('creation_date', sa.DateTime(), nullable=False),
sa.Column('update_date', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['org_id'], ['organization.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('paymentsproduct',
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('product_type', postgresql.ENUM('SUBSCRIPTION', 'ONE_TIME', name='paymentproducttypeenum', create_type=False), nullable=False),
sa.Column('price_type', postgresql.ENUM('CUSTOMER_CHOICE', 'FIXED_PRICE', name='paymentpricetypeenum', create_type=False), nullable=False),
sa.Column('benefits', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('amount', sa.Float(), nullable=False),
sa.Column('currency', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('org_id', sa.BigInteger(), nullable=True),
sa.Column('payments_config_id', sa.BigInteger(), nullable=True),
sa.Column('provider_product_id', sa.String(), nullable=True),
sa.Column('creation_date', sa.DateTime(), nullable=False),
sa.Column('update_date', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['org_id'], ['organization.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['payments_config_id'], ['paymentsconfig.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('paymentscourse',
sa.Column('course_id', sa.BigInteger(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('payment_product_id', sa.BigInteger(), nullable=True),
sa.Column('org_id', sa.BigInteger(), nullable=True),
sa.Column('creation_date', sa.DateTime(), nullable=False),
sa.Column('update_date', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['course_id'], ['course.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['org_id'], ['organization.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['payment_product_id'], ['paymentsproduct.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('paymentsuser',
sa.Column('status', postgresql.ENUM('PENDING', 'COMPLETED', 'ACTIVE', 'CANCELLED', 'FAILED', 'REFUNDED', name='paymentstatusenum', create_type=False), nullable=False),
sa.Column('provider_specific_data', sa.JSON(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.BigInteger(), nullable=True),
sa.Column('org_id', sa.BigInteger(), nullable=True),
sa.Column('payment_product_id', sa.BigInteger(), nullable=True),
sa.Column('creation_date', sa.DateTime(), nullable=False),
sa.Column('update_date', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['org_id'], ['organization.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['payment_product_id'], ['paymentsproduct.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('paymentsuser')
op.drop_table('paymentscourse')
op.drop_table('paymentsproduct')
op.drop_table('paymentsconfig')
# ### end Alembic commands ###

View file

@ -26,7 +26,7 @@ import toast from 'react-hot-toast'
import { mutate } from 'swr' import { mutate } from 'swr'
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal' import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
import { useMediaQuery } from 'usehooks-ts' import { useMediaQuery } from 'usehooks-ts'
import PaidCourseActivity from '@components/Objects/Courses/CourseActions/PaidCourseActivity' import PaidCourseActivityDisclaimer from '@components/Objects/Courses/CourseActions/PaidCourseActivityDisclaimer'
interface ActivityClientProps { interface ActivityClientProps {
activityid: string activityid: string
@ -81,7 +81,6 @@ function ActivityClient(props: ActivityClientProps) {
else { else {
setBgColor('bg-zinc-950'); setBgColor('bg-zinc-950');
} }
console.log(activity.content)
} }
, [activity, pathname]) , [activity, pathname])
@ -175,45 +174,44 @@ function ActivityClient(props: ActivityClientProps) {
)} )}
{activity && activity.published == true && ( {activity && activity.published == true && (
<div <>
className={`p-7 drop-shadow-sm rounded-lg ${bgColor}`} {activity.content.paid_access == false ? (
> <PaidCourseActivityDisclaimer course={course} />
{/* Paid Courses */} ) : (
{activity.content.paid_access == false && ( <div className={`p-7 drop-shadow-sm rounded-lg ${bgColor}`}>
<PaidCourseActivity course={course} /> {/* Activity Types */}
)}
{/* Activity Types */}
<div>
{activity.activity_type == 'TYPE_DYNAMIC' && (
<Canva content={activity.content} activity={activity} />
)}
{/* todo : use apis & streams instead of this */}
{activity.activity_type == 'TYPE_VIDEO' && (
<VideoActivity course={course} activity={activity} />
)}
{activity.activity_type == 'TYPE_DOCUMENT' && (
<DocumentPdfActivity
course={course}
activity={activity}
/>
)}
{activity.activity_type == 'TYPE_ASSIGNMENT' && (
<div> <div>
{assignment ? ( {activity.activity_type == 'TYPE_DYNAMIC' && (
<AssignmentProvider assignment_uuid={assignment?.assignment_uuid}> <Canva content={activity.content} activity={activity} />
<AssignmentsTaskProvider> )}
<AssignmentSubmissionProvider assignment_uuid={assignment?.assignment_uuid}> {activity.activity_type == 'TYPE_VIDEO' && (
<AssignmentStudentActivity /> <VideoActivity course={course} activity={activity} />
</AssignmentSubmissionProvider> )}
</AssignmentsTaskProvider> {activity.activity_type == 'TYPE_DOCUMENT' && (
</AssignmentProvider> <DocumentPdfActivity
) : ( course={course}
<div></div> activity={activity}
/>
)}
{activity.activity_type == 'TYPE_ASSIGNMENT' && (
<div>
{assignment ? (
<AssignmentProvider assignment_uuid={assignment?.assignment_uuid}>
<AssignmentsTaskProvider>
<AssignmentSubmissionProvider assignment_uuid={assignment?.assignment_uuid}>
<AssignmentStudentActivity />
</AssignmentSubmissionProvider>
</AssignmentsTaskProvider>
</AssignmentProvider>
) : (
<div></div>
)}
</div>
)} )}
</div> </div>
)} </div>
</div> )}
</div> </>
)} )}
{<div style={{ height: '100px' }}></div>} {<div style={{ height: '100px' }}></div>}
</div> </div>

View file

@ -7,6 +7,7 @@ import {
Eye, Eye,
File, File,
FilePenLine, FilePenLine,
FileSymlink,
Globe, Globe,
Lock, Lock,
MoreVertical, MoreVertical,
@ -27,6 +28,7 @@ import { useOrg } from '@components/Contexts/OrgContext'
import { useCourse } from '@components/Contexts/CourseContext' import { useCourse } from '@components/Contexts/CourseContext'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { useMediaQuery } from 'usehooks-ts' import { useMediaQuery } from 'usehooks-ts'
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
type ActivitiyElementProps = { type ActivitiyElementProps = {
orgslug: string orgslug: string
@ -176,24 +178,26 @@ function ActivityElement(props: ActivitiyElementProps) {
)} )}
<span>{!props.activity.published ? 'Publish' : 'Unpublish'}</span> <span>{!props.activity.published ? 'Publish' : 'Unpublish'}</span>
</button> </button>
<Link <div className="w-px h-3 bg-gray-300 mx-1 self-center rounded-full hidden sm:block" />
href={ <ToolTip content="Preview Activity" sideOffset={8}>
getUriWithOrg(props.orgslug, '') + <Link
`/course/${props.course_uuid.replace( href={
'course_', getUriWithOrg(props.orgslug, '') +
'' `/course/${props.course_uuid.replace(
)}/activity/${props.activity.activity_uuid.replace( 'course_',
'activity_', ''
'' )}/activity/${props.activity.activity_uuid.replace(
)}` 'activity_',
} ''
prefetch )}`
className="p-1 px-2 sm:px-3 bg-gradient-to-bl text-cyan-800 from-sky-400/50 to-cyan-200/80 border border-cyan-600/10 shadow-md rounded-md font-bold text-xs flex items-center space-x-1 transition-colors duration-200 hover:from-sky-500/50 hover:to-cyan-300/80" }
rel="noopener noreferrer" prefetch
> className="p-1 px-2 sm:px-3 bg-gradient-to-bl text-cyan-800 from-sky-400/50 to-cyan-200/80 border border-cyan-600/10 shadow-md rounded-md font-bold text-xs flex items-center space-x-1 transition-colors duration-200 hover:from-sky-500/50 hover:to-cyan-300/80"
<Eye strokeWidth={2} size={12} className="text-sky-600" /> rel="noopener noreferrer"
<span>Preview</span> >
</Link> <Eye strokeWidth={2} size={14} className="text-sky-600" />
</Link>
</ToolTip>
{/* Delete Button */} {/* Delete Button */}
<ConfirmationModal <ConfirmationModal
confirmationMessage="Are you sure you want to delete this activity ?" confirmationMessage="Are you sure you want to delete this activity ?"
@ -205,7 +209,6 @@ function ActivityElement(props: ActivitiyElementProps) {
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<X size={15} className="text-rose-200 font-bold" /> <X size={15} className="text-rose-200 font-bold" />
{!isMobile && <span className="text-rose-200 font-bold text-xs">Delete</span>}
</button> </button>
} }
functionToExecute={() => deleteActivityUI()} functionToExecute={() => deleteActivityUI()}

View file

@ -74,7 +74,7 @@ function CoursePaidOptions({ course }: CoursePaidOptionsProps) {
<div key={product.id} className="bg-slate-50/30 p-4 rounded-lg nice-shadow flex flex-col"> <div key={product.id} className="bg-slate-50/30 p-4 rounded-lg nice-shadow flex flex-col">
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start mb-2">
<div className="flex flex-col space-y-1 items-start"> <div className="flex flex-col space-y-1 items-start">
<Badge className='w-fit flex items-center space-x-2' variant="outline"> <Badge className='w-fit flex items-center space-x-2 bg-gray-100/50' variant="outline">
{product.product_type === 'subscription' ? <RefreshCcw size={12} /> : <SquareCheck size={12} />} {product.product_type === 'subscription' ? <RefreshCcw size={12} /> : <SquareCheck size={12} />}
<span className='text-sm'> <span className='text-sm'>
{product.product_type === 'subscription' ? 'Subscription' : 'One-time payment'} {product.product_type === 'subscription' ? 'Subscription' : 'One-time payment'}

View file

@ -6,16 +6,16 @@ interface PaidCourseActivityProps {
course: any; course: any;
} }
function PaidCourseActivity({ course }: PaidCourseActivityProps) { function PaidCourseActivityDisclaimer({ course }: PaidCourseActivityProps) {
return ( return (
<div className="space-y-4 "> <div className="space-y-4 max-w-lg mx-auto">
<div className="p-4 bg-amber-50 border border-amber-200 rounded-lg nice-shadow"> <div className="p-4 bg-amber-50 border border-amber-200 rounded-lg ">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<AlertCircle className="w-5 h-5 text-amber-800" /> <AlertCircle className="w-5 h-5 text-amber-800" />
<h3 className="text-amber-800 font-semibold">Paid Content</h3> <h3 className="text-amber-800 font-semibold">Paid Content</h3>
</div> </div>
<p className="text-amber-700 text-sm mt-1"> <p className="text-amber-700 text-sm mt-1">
This content requires a course purchase to access. Please purchase the course to continue. This content requires a course purchase to access.
</p> </p>
</div> </div>
<CoursePaidOptions course={course} /> <CoursePaidOptions course={course} />
@ -23,4 +23,4 @@ function PaidCourseActivity({ course }: PaidCourseActivityProps) {
) )
} }
export default PaidCourseActivity export default PaidCourseActivityDisclaimer