mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: UI improvements & migration script
This commit is contained in:
parent
e464b30147
commit
6cd1cf7e9c
5 changed files with 154 additions and 63 deletions
90
apps/api/migrations/versions/0314ec7791e1_payments.py
Normal file
90
apps/api/migrations/versions/0314ec7791e1_payments.py
Normal 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 ###
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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()}
|
||||||
|
|
|
||||||
|
|
@ -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'}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue