mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: format with prettier
This commit is contained in:
parent
03fb09c3d6
commit
a147ad6610
164 changed files with 11257 additions and 8154 deletions
|
|
@ -2,44 +2,43 @@ import { useOrg } from '@components/Contexts/OrgContext'
|
|||
import React from 'react'
|
||||
|
||||
interface UseGetAIFeatures {
|
||||
feature: 'editor' | 'activity_ask' | 'course_ask' | 'global_ai_ask',
|
||||
feature: 'editor' | 'activity_ask' | 'course_ask' | 'global_ai_ask'
|
||||
}
|
||||
|
||||
|
||||
function useGetAIFeatures(props: UseGetAIFeatures) {
|
||||
const org = useOrg() as any
|
||||
const [isEnabled, setisEnabled] = React.useState(false)
|
||||
|
||||
function checkAvailableAIFeaturesOnOrg(feature: string) {
|
||||
const config = org?.config?.config?.AIConfig;
|
||||
const config = org?.config?.config?.AIConfig
|
||||
|
||||
if (!config) {
|
||||
console.log("AI or Organization config is not defined.");
|
||||
return false;
|
||||
console.log('AI or Organization config is not defined.')
|
||||
return false
|
||||
}
|
||||
|
||||
if (!config.enabled) {
|
||||
console.log("AI is not enabled for this Organization.");
|
||||
return false;
|
||||
console.log('AI is not enabled for this Organization.')
|
||||
return false
|
||||
}
|
||||
|
||||
if (!config.features[feature]) {
|
||||
console.log(`Feature ${feature} is not enabled for this Organization.`);
|
||||
return false;
|
||||
console.log(`Feature ${feature} is not enabled for this Organization.`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (org) { // Check if org is not null or undefined
|
||||
if (org) {
|
||||
// Check if org is not null or undefined
|
||||
let isEnabledStatus = checkAvailableAIFeaturesOnOrg(props.feature)
|
||||
setisEnabled(isEnabledStatus)
|
||||
}
|
||||
}, [org])
|
||||
|
||||
return isEnabled
|
||||
|
||||
}
|
||||
|
||||
export default useGetAIFeatures
|
||||
export default useGetAIFeatures
|
||||
|
|
|
|||
|
|
@ -1,76 +1,74 @@
|
|||
'use client';
|
||||
import { AIMessage } from '@components/Objects/Activities/AI/AIActivityAsk';
|
||||
'use client'
|
||||
import { AIMessage } from '@components/Objects/Activities/AI/AIActivityAsk'
|
||||
import React, { createContext, useContext, useReducer } from 'react'
|
||||
export const AIChatBotContext = createContext(null) as any;
|
||||
export const AIChatBotDispatchContext = createContext(null) as any;
|
||||
export const AIChatBotContext = createContext(null) as any
|
||||
export const AIChatBotDispatchContext = createContext(null) as any
|
||||
|
||||
export type AIChatBotStateTypes = {
|
||||
messages: AIMessage[],
|
||||
isModalOpen: boolean,
|
||||
aichat_uuid: string,
|
||||
isWaitingForResponse: boolean,
|
||||
chatInputValue: string
|
||||
error: AIError
|
||||
messages: AIMessage[]
|
||||
isModalOpen: boolean
|
||||
aichat_uuid: string
|
||||
isWaitingForResponse: boolean
|
||||
chatInputValue: string
|
||||
error: AIError
|
||||
}
|
||||
|
||||
type AIError = {
|
||||
isError: boolean
|
||||
status: number
|
||||
error_message: string
|
||||
isError: boolean
|
||||
status: number
|
||||
error_message: string
|
||||
}
|
||||
|
||||
function AIChatBotProvider({ children }: { children: React.ReactNode }) {
|
||||
const [aiChatBotState, dispatchAIChatBot] = useReducer(aiChatBotReducer,
|
||||
{
|
||||
messages: [] as AIMessage[],
|
||||
isModalOpen: false,
|
||||
aichat_uuid: null,
|
||||
isWaitingForResponse: false,
|
||||
chatInputValue: '',
|
||||
error: { isError: false, status: 0, error_message: ' ' } as AIError
|
||||
}
|
||||
);
|
||||
return (
|
||||
<AIChatBotContext.Provider value={aiChatBotState}>
|
||||
<AIChatBotDispatchContext.Provider value={dispatchAIChatBot}>
|
||||
{children}
|
||||
</AIChatBotDispatchContext.Provider>
|
||||
</AIChatBotContext.Provider>
|
||||
)
|
||||
const [aiChatBotState, dispatchAIChatBot] = useReducer(aiChatBotReducer, {
|
||||
messages: [] as AIMessage[],
|
||||
isModalOpen: false,
|
||||
aichat_uuid: null,
|
||||
isWaitingForResponse: false,
|
||||
chatInputValue: '',
|
||||
error: { isError: false, status: 0, error_message: ' ' } as AIError,
|
||||
})
|
||||
return (
|
||||
<AIChatBotContext.Provider value={aiChatBotState}>
|
||||
<AIChatBotDispatchContext.Provider value={dispatchAIChatBot}>
|
||||
{children}
|
||||
</AIChatBotDispatchContext.Provider>
|
||||
</AIChatBotContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default AIChatBotProvider
|
||||
|
||||
export function useAIChatBot() {
|
||||
return useContext(AIChatBotContext);
|
||||
return useContext(AIChatBotContext)
|
||||
}
|
||||
|
||||
export function useAIChatBotDispatch() {
|
||||
return useContext(AIChatBotDispatchContext);
|
||||
return useContext(AIChatBotDispatchContext)
|
||||
}
|
||||
|
||||
function aiChatBotReducer(state: any, action: any) {
|
||||
switch (action.type) {
|
||||
case 'setMessages':
|
||||
return { ...state, messages: action.payload };
|
||||
case 'addMessage':
|
||||
return { ...state, messages: [...state.messages, action.payload] };
|
||||
case 'setIsModalOpen':
|
||||
return { ...state, isModalOpen: true };
|
||||
case 'setIsModalClose':
|
||||
return { ...state, isModalOpen: false };
|
||||
case 'setAichat_uuid':
|
||||
return { ...state, aichat_uuid: action.payload };
|
||||
case 'setIsWaitingForResponse':
|
||||
return { ...state, isWaitingForResponse: true };
|
||||
case 'setIsNoLongerWaitingForResponse':
|
||||
return { ...state, isWaitingForResponse: false };
|
||||
case 'setChatInputValue':
|
||||
return { ...state, chatInputValue: action.payload };
|
||||
case 'setError':
|
||||
return { ...state, error: action.payload };
|
||||
switch (action.type) {
|
||||
case 'setMessages':
|
||||
return { ...state, messages: action.payload }
|
||||
case 'addMessage':
|
||||
return { ...state, messages: [...state.messages, action.payload] }
|
||||
case 'setIsModalOpen':
|
||||
return { ...state, isModalOpen: true }
|
||||
case 'setIsModalClose':
|
||||
return { ...state, isModalOpen: false }
|
||||
case 'setAichat_uuid':
|
||||
return { ...state, aichat_uuid: action.payload }
|
||||
case 'setIsWaitingForResponse':
|
||||
return { ...state, isWaitingForResponse: true }
|
||||
case 'setIsNoLongerWaitingForResponse':
|
||||
return { ...state, isWaitingForResponse: false }
|
||||
case 'setChatInputValue':
|
||||
return { ...state, chatInputValue: action.payload }
|
||||
case 'setError':
|
||||
return { ...state, error: action.payload }
|
||||
|
||||
default:
|
||||
throw new Error(`Unhandled action type: ${action.type}`)
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unhandled action type: ${action.type}`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,92 +1,93 @@
|
|||
'use client';
|
||||
import { AIMessage } from '@components/Objects/Activities/AI/AIActivityAsk';
|
||||
'use client'
|
||||
import { AIMessage } from '@components/Objects/Activities/AI/AIActivityAsk'
|
||||
import React, { createContext, useContext, useReducer } from 'react'
|
||||
export const AIEditorContext = createContext(null) as any;
|
||||
export const AIEditorDispatchContext = createContext(null) as any;
|
||||
export const AIEditorContext = createContext(null) as any
|
||||
export const AIEditorDispatchContext = createContext(null) as any
|
||||
|
||||
export type AIEditorStateTypes = {
|
||||
|
||||
messages: AIMessage[],
|
||||
isModalOpen: boolean,
|
||||
isFeedbackModalOpen: boolean,
|
||||
aichat_uuid: string,
|
||||
isWaitingForResponse: boolean,
|
||||
chatInputValue: string,
|
||||
selectedTool: 'Writer' | 'ContinueWriting' | 'MakeLonger' | 'GenerateQuiz' | 'Translate'
|
||||
isUserInputEnabled: boolean
|
||||
error: AIError
|
||||
messages: AIMessage[]
|
||||
isModalOpen: boolean
|
||||
isFeedbackModalOpen: boolean
|
||||
aichat_uuid: string
|
||||
isWaitingForResponse: boolean
|
||||
chatInputValue: string
|
||||
selectedTool:
|
||||
| 'Writer'
|
||||
| 'ContinueWriting'
|
||||
| 'MakeLonger'
|
||||
| 'GenerateQuiz'
|
||||
| 'Translate'
|
||||
isUserInputEnabled: boolean
|
||||
error: AIError
|
||||
}
|
||||
|
||||
type AIError = {
|
||||
isError: boolean
|
||||
status: number
|
||||
error_message: string
|
||||
isError: boolean
|
||||
status: number
|
||||
error_message: string
|
||||
}
|
||||
|
||||
function AIEditorProvider({ children }: { children: React.ReactNode }) {
|
||||
const [aIEditorState, dispatchAIEditor] = useReducer(aIEditorReducer,
|
||||
{
|
||||
messages: [] as AIMessage[],
|
||||
isModalOpen: false,
|
||||
isFeedbackModalOpen: false,
|
||||
aichat_uuid: null,
|
||||
isWaitingForResponse: false,
|
||||
chatInputValue: '',
|
||||
selectedTool: 'Writer',
|
||||
isUserInputEnabled: true,
|
||||
error: { isError: false, status: 0, error_message: ' ' } as AIError
|
||||
}
|
||||
);
|
||||
return (
|
||||
<AIEditorContext.Provider value={aIEditorState}>
|
||||
<AIEditorDispatchContext.Provider value={dispatchAIEditor}>
|
||||
{children}
|
||||
</AIEditorDispatchContext.Provider>
|
||||
</AIEditorContext.Provider>
|
||||
)
|
||||
const [aIEditorState, dispatchAIEditor] = useReducer(aIEditorReducer, {
|
||||
messages: [] as AIMessage[],
|
||||
isModalOpen: false,
|
||||
isFeedbackModalOpen: false,
|
||||
aichat_uuid: null,
|
||||
isWaitingForResponse: false,
|
||||
chatInputValue: '',
|
||||
selectedTool: 'Writer',
|
||||
isUserInputEnabled: true,
|
||||
error: { isError: false, status: 0, error_message: ' ' } as AIError,
|
||||
})
|
||||
return (
|
||||
<AIEditorContext.Provider value={aIEditorState}>
|
||||
<AIEditorDispatchContext.Provider value={dispatchAIEditor}>
|
||||
{children}
|
||||
</AIEditorDispatchContext.Provider>
|
||||
</AIEditorContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default AIEditorProvider
|
||||
|
||||
export function useAIEditor() {
|
||||
return useContext(AIEditorContext);
|
||||
return useContext(AIEditorContext)
|
||||
}
|
||||
|
||||
export function useAIEditorDispatch() {
|
||||
return useContext(AIEditorDispatchContext);
|
||||
return useContext(AIEditorDispatchContext)
|
||||
}
|
||||
|
||||
function aIEditorReducer(state: any, action: any) {
|
||||
switch (action.type) {
|
||||
case 'setMessages':
|
||||
return { ...state, messages: action.payload };
|
||||
case 'addMessage':
|
||||
return { ...state, messages: [...state.messages, action.payload] };
|
||||
case 'setIsModalOpen':
|
||||
return { ...state, isModalOpen: true };
|
||||
case 'setIsModalClose':
|
||||
return { ...state, isModalOpen: false };
|
||||
case 'setAichat_uuid':
|
||||
return { ...state, aichat_uuid: action.payload };
|
||||
case 'setIsWaitingForResponse':
|
||||
return { ...state, isWaitingForResponse: true };
|
||||
case 'setIsNoLongerWaitingForResponse':
|
||||
return { ...state, isWaitingForResponse: false };
|
||||
case 'setChatInputValue':
|
||||
return { ...state, chatInputValue: action.payload };
|
||||
case 'setSelectedTool':
|
||||
return { ...state, selectedTool: action.payload };
|
||||
case 'setIsFeedbackModalOpen':
|
||||
return { ...state, isFeedbackModalOpen: true };
|
||||
case 'setIsFeedbackModalClose':
|
||||
return { ...state, isFeedbackModalOpen: false };
|
||||
case 'setIsUserInputEnabled':
|
||||
return { ...state, isUserInputEnabled: action.payload };
|
||||
case 'setError':
|
||||
return { ...state, error: action.payload };
|
||||
switch (action.type) {
|
||||
case 'setMessages':
|
||||
return { ...state, messages: action.payload }
|
||||
case 'addMessage':
|
||||
return { ...state, messages: [...state.messages, action.payload] }
|
||||
case 'setIsModalOpen':
|
||||
return { ...state, isModalOpen: true }
|
||||
case 'setIsModalClose':
|
||||
return { ...state, isModalOpen: false }
|
||||
case 'setAichat_uuid':
|
||||
return { ...state, aichat_uuid: action.payload }
|
||||
case 'setIsWaitingForResponse':
|
||||
return { ...state, isWaitingForResponse: true }
|
||||
case 'setIsNoLongerWaitingForResponse':
|
||||
return { ...state, isWaitingForResponse: false }
|
||||
case 'setChatInputValue':
|
||||
return { ...state, chatInputValue: action.payload }
|
||||
case 'setSelectedTool':
|
||||
return { ...state, selectedTool: action.payload }
|
||||
case 'setIsFeedbackModalOpen':
|
||||
return { ...state, isFeedbackModalOpen: true }
|
||||
case 'setIsFeedbackModalClose':
|
||||
return { ...state, isFeedbackModalOpen: false }
|
||||
case 'setIsUserInputEnabled':
|
||||
return { ...state, isUserInputEnabled: action.payload }
|
||||
case 'setError':
|
||||
return { ...state, error: action.payload }
|
||||
|
||||
|
||||
default:
|
||||
throw new Error(`Unhandled action type: ${action.type}`)
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unhandled action type: ${action.type}`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,63 +1,70 @@
|
|||
'use client';
|
||||
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
import { swrFetcher } from '@services/utils/ts/requests';
|
||||
'use client'
|
||||
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||
import { getAPIUrl } from '@services/config/config'
|
||||
import { swrFetcher } from '@services/utils/ts/requests'
|
||||
import React, { createContext, useContext, useEffect, useReducer } from 'react'
|
||||
import useSWR from 'swr';
|
||||
import useSWR from 'swr'
|
||||
|
||||
export const CourseContext = createContext(null) as any;
|
||||
export const CourseDispatchContext = createContext(null) as any;
|
||||
export const CourseContext = createContext(null) as any
|
||||
export const CourseDispatchContext = createContext(null) as any
|
||||
|
||||
export function CourseProvider({ children, courseuuid }: { children: React.ReactNode, courseuuid: string }) {
|
||||
const { data: courseStructureData } = useSWR(`${getAPIUrl()}courses/${courseuuid}/meta`, swrFetcher);
|
||||
const [courseStructure, dispatchCourseStructure] = useReducer(courseReducer,
|
||||
{
|
||||
courseStructure: courseStructureData ? courseStructureData : {},
|
||||
courseOrder: {},
|
||||
isSaved: true
|
||||
}
|
||||
);
|
||||
export function CourseProvider({
|
||||
children,
|
||||
courseuuid,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
courseuuid: string
|
||||
}) {
|
||||
const { data: courseStructureData } = useSWR(
|
||||
`${getAPIUrl()}courses/${courseuuid}/meta`,
|
||||
swrFetcher
|
||||
)
|
||||
const [courseStructure, dispatchCourseStructure] = useReducer(courseReducer, {
|
||||
courseStructure: courseStructureData ? courseStructureData : {},
|
||||
courseOrder: {},
|
||||
isSaved: true,
|
||||
})
|
||||
|
||||
// When courseStructureData is loaded, update the state
|
||||
useEffect(() => {
|
||||
if (courseStructureData) {
|
||||
dispatchCourseStructure({
|
||||
type: 'setCourseStructure',
|
||||
payload: courseStructureData,
|
||||
})
|
||||
}
|
||||
}, [courseStructureData])
|
||||
|
||||
// When courseStructureData is loaded, update the state
|
||||
useEffect(() => {
|
||||
if (courseStructureData) {
|
||||
dispatchCourseStructure({ type: 'setCourseStructure', payload: courseStructureData });
|
||||
}
|
||||
}, [courseStructureData]);
|
||||
if (!courseStructureData) return <PageLoading></PageLoading>
|
||||
|
||||
|
||||
if (!courseStructureData) return <PageLoading></PageLoading>
|
||||
|
||||
|
||||
return (
|
||||
<CourseContext.Provider value={courseStructure}>
|
||||
<CourseDispatchContext.Provider value={dispatchCourseStructure}>
|
||||
{children}
|
||||
</CourseDispatchContext.Provider>
|
||||
</CourseContext.Provider>
|
||||
)
|
||||
return (
|
||||
<CourseContext.Provider value={courseStructure}>
|
||||
<CourseDispatchContext.Provider value={dispatchCourseStructure}>
|
||||
{children}
|
||||
</CourseDispatchContext.Provider>
|
||||
</CourseContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useCourse() {
|
||||
return useContext(CourseContext);
|
||||
return useContext(CourseContext)
|
||||
}
|
||||
|
||||
export function useCourseDispatch() {
|
||||
return useContext(CourseDispatchContext);
|
||||
return useContext(CourseDispatchContext)
|
||||
}
|
||||
|
||||
function courseReducer(state: any, action: any) {
|
||||
switch (action.type) {
|
||||
case 'setCourseStructure':
|
||||
return { ...state, courseStructure: action.payload };
|
||||
case 'setCourseOrder':
|
||||
return { ...state, courseOrder: action.payload };
|
||||
case 'setIsSaved':
|
||||
return { ...state, isSaved: true };
|
||||
case 'setIsNotSaved':
|
||||
return { ...state, isSaved: false };
|
||||
default:
|
||||
throw new Error(`Unhandled action type: ${action.type}`);
|
||||
}
|
||||
}
|
||||
switch (action.type) {
|
||||
case 'setCourseStructure':
|
||||
return { ...state, courseStructure: action.payload }
|
||||
case 'setCourseOrder':
|
||||
return { ...state, courseOrder: action.payload }
|
||||
case 'setIsSaved':
|
||||
return { ...state, isSaved: true }
|
||||
case 'setIsNotSaved':
|
||||
return { ...state, isSaved: false }
|
||||
default:
|
||||
throw new Error(`Unhandled action type: ${action.type}`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,30 @@
|
|||
'use client';
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
|
||||
export const EditorProviderContext = React.createContext(null) as any;
|
||||
export const EditorProviderContext = React.createContext(null) as any
|
||||
|
||||
type EditorProviderProps = {
|
||||
children: React.ReactNode
|
||||
options: EditorProviderState
|
||||
children: React.ReactNode
|
||||
options: EditorProviderState
|
||||
}
|
||||
|
||||
type EditorProviderState = {
|
||||
isEditable: boolean
|
||||
isEditable: boolean
|
||||
}
|
||||
|
||||
function EditorOptionsProvider({ children, options }: EditorProviderProps) {
|
||||
const [editorOptions, setEditorOptions] = useState<EditorProviderState>(options);
|
||||
const [editorOptions, setEditorOptions] =
|
||||
useState<EditorProviderState>(options)
|
||||
|
||||
return (
|
||||
<EditorProviderContext.Provider value={editorOptions}>
|
||||
{children}
|
||||
</EditorProviderContext.Provider>
|
||||
)
|
||||
return (
|
||||
<EditorProviderContext.Provider value={editorOptions}>
|
||||
{children}
|
||||
</EditorProviderContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditorOptionsProvider
|
||||
|
||||
export function useEditorProvider() {
|
||||
return React.useContext(EditorProviderContext);
|
||||
return React.useContext(EditorProviderContext)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
'use client';
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
import { swrFetcher } from '@services/utils/ts/requests';
|
||||
'use client'
|
||||
import { getAPIUrl } from '@services/config/config'
|
||||
import { swrFetcher } from '@services/utils/ts/requests'
|
||||
import React, { useContext, useEffect } from 'react'
|
||||
import useSWR from 'swr';
|
||||
import { createContext } from 'react';
|
||||
import useSWR from 'swr'
|
||||
import { createContext } from 'react'
|
||||
|
||||
export const OrgContext = createContext({}) as any;
|
||||
export const OrgContext = createContext({}) as any
|
||||
|
||||
export function OrgProvider({ children, orgslug }: { children: React.ReactNode, orgslug: string }) {
|
||||
const { data: org } = useSWR(`${getAPIUrl()}orgs/slug/${orgslug}`, swrFetcher);
|
||||
useEffect(() => {
|
||||
export function OrgProvider({
|
||||
children,
|
||||
orgslug,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
orgslug: string
|
||||
}) {
|
||||
const { data: org } = useSWR(`${getAPIUrl()}orgs/slug/${orgslug}`, swrFetcher)
|
||||
useEffect(() => {}, [org])
|
||||
|
||||
}, [org]);
|
||||
|
||||
return (
|
||||
<OrgContext.Provider value={org}>
|
||||
{children}
|
||||
</OrgContext.Provider>
|
||||
)
|
||||
return <OrgContext.Provider value={org}>{children}</OrgContext.Provider>
|
||||
}
|
||||
|
||||
export function useOrg() {
|
||||
return useContext(OrgContext);
|
||||
return useContext(OrgContext)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,59 +1,77 @@
|
|||
'use client';
|
||||
import { getNewAccessTokenUsingRefreshToken, getUserSession } from '@services/auth/auth';
|
||||
'use client'
|
||||
import {
|
||||
getNewAccessTokenUsingRefreshToken,
|
||||
getUserSession,
|
||||
} from '@services/auth/auth'
|
||||
import React, { useContext, createContext, useEffect } from 'react'
|
||||
|
||||
export const SessionContext = createContext({}) as any;
|
||||
export const SessionContext = createContext({}) as any
|
||||
|
||||
type Session = {
|
||||
access_token: string;
|
||||
user: any;
|
||||
roles: any;
|
||||
isLoading: boolean;
|
||||
isAuthenticated: boolean;
|
||||
access_token: string
|
||||
user: any
|
||||
roles: any
|
||||
isLoading: boolean
|
||||
isAuthenticated: boolean
|
||||
}
|
||||
|
||||
function SessionProvider({ children }: { children: React.ReactNode }) {
|
||||
const [session, setSession] = React.useState<Session>({ access_token: "", user: {}, roles: {}, isLoading: true, isAuthenticated: false });
|
||||
const [session, setSession] = React.useState<Session>({
|
||||
access_token: '',
|
||||
user: {},
|
||||
roles: {},
|
||||
isLoading: true,
|
||||
isAuthenticated: false,
|
||||
})
|
||||
|
||||
async function getNewAccessTokenUsingRefreshTokenUI() {
|
||||
let data = await getNewAccessTokenUsingRefreshToken();
|
||||
return data.access_token;
|
||||
async function getNewAccessTokenUsingRefreshTokenUI() {
|
||||
let data = await getNewAccessTokenUsingRefreshToken()
|
||||
return data.access_token
|
||||
}
|
||||
|
||||
async function checkSession() {
|
||||
// Get new access token using refresh token
|
||||
const access_token = await getNewAccessTokenUsingRefreshTokenUI()
|
||||
|
||||
if (access_token) {
|
||||
// Get user session info
|
||||
const user_session = await getUserSession(access_token)
|
||||
|
||||
// Set session
|
||||
setSession({
|
||||
access_token: access_token,
|
||||
user: user_session.user,
|
||||
roles: user_session.roles,
|
||||
isLoading: false,
|
||||
isAuthenticated: true,
|
||||
})
|
||||
}
|
||||
|
||||
async function checkSession() {
|
||||
// Get new access token using refresh token
|
||||
const access_token = await getNewAccessTokenUsingRefreshTokenUI();
|
||||
|
||||
if (access_token) {
|
||||
// Get user session info
|
||||
const user_session = await getUserSession(access_token);
|
||||
|
||||
// Set session
|
||||
setSession({ access_token: access_token, user: user_session.user, roles: user_session.roles, isLoading: false, isAuthenticated: true });
|
||||
}
|
||||
|
||||
if (!access_token) {
|
||||
setSession({ access_token: "", user: {}, roles: {}, isLoading: false, isAuthenticated: false });
|
||||
}
|
||||
if (!access_token) {
|
||||
setSession({
|
||||
access_token: '',
|
||||
user: {},
|
||||
roles: {},
|
||||
isLoading: false,
|
||||
isAuthenticated: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Check session
|
||||
checkSession()
|
||||
}, [])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// Check session
|
||||
checkSession();
|
||||
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<SessionContext.Provider value={session}>
|
||||
{children}
|
||||
</SessionContext.Provider>
|
||||
)
|
||||
return (
|
||||
<SessionContext.Provider value={session}>
|
||||
{children}
|
||||
</SessionContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useSession() {
|
||||
return useContext(SessionContext);
|
||||
return useContext(SessionContext)
|
||||
}
|
||||
|
||||
export default SessionProvider
|
||||
export default SessionProvider
|
||||
|
|
|
|||
|
|
@ -1,166 +1,208 @@
|
|||
import FormLayout, { FormField, FormLabelAndMessage, Input, Textarea } from '@components/StyledElements/Form/Form';
|
||||
import { useFormik } from 'formik';
|
||||
import FormLayout, {
|
||||
FormField,
|
||||
FormLabelAndMessage,
|
||||
Input,
|
||||
Textarea,
|
||||
} from '@components/StyledElements/Form/Form'
|
||||
import { useFormik } from 'formik'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
import * as Switch from '@radix-ui/react-switch';
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
import * as Switch from '@radix-ui/react-switch'
|
||||
import * as Form from '@radix-ui/react-form'
|
||||
import React from 'react'
|
||||
import { useCourse, useCourseDispatch } from '../../../Contexts/CourseContext';
|
||||
import ThumbnailUpdate from './ThumbnailUpdate';
|
||||
|
||||
import { useCourse, useCourseDispatch } from '../../../Contexts/CourseContext'
|
||||
import ThumbnailUpdate from './ThumbnailUpdate'
|
||||
|
||||
type EditCourseStructureProps = {
|
||||
orgslug: string,
|
||||
course_uuid?: string,
|
||||
orgslug: string
|
||||
course_uuid?: string
|
||||
}
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {};
|
||||
const errors: any = {}
|
||||
|
||||
if (!values.name) {
|
||||
errors.name = 'Required';
|
||||
}
|
||||
if (!values.name) {
|
||||
errors.name = 'Required'
|
||||
}
|
||||
|
||||
if (values.name.length > 100) {
|
||||
errors.name = 'Must be 100 characters or less';
|
||||
}
|
||||
if (values.name.length > 100) {
|
||||
errors.name = 'Must be 100 characters or less'
|
||||
}
|
||||
|
||||
if (!values.description) {
|
||||
errors.description = 'Required'
|
||||
}
|
||||
|
||||
if (!values.description) {
|
||||
errors.description = 'Required';
|
||||
if (values.description.length > 1000) {
|
||||
errors.description = 'Must be 1000 characters or less'
|
||||
}
|
||||
|
||||
}
|
||||
if (!values.learnings) {
|
||||
errors.learnings = 'Required'
|
||||
}
|
||||
|
||||
if (values.description.length > 1000) {
|
||||
errors.description = 'Must be 1000 characters or less';
|
||||
}
|
||||
|
||||
|
||||
if (!values.learnings) {
|
||||
errors.learnings = 'Required';
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
|
||||
function EditCourseGeneral(props: EditCourseStructureProps) {
|
||||
const [error, setError] = React.useState('');
|
||||
const course = useCourse() as any;
|
||||
const dispatchCourse = useCourseDispatch() as any;
|
||||
|
||||
const courseStructure = course.courseStructure;
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
name: String(courseStructure.name),
|
||||
description: String(courseStructure.description),
|
||||
about: String(courseStructure.about),
|
||||
learnings: String(courseStructure.learnings),
|
||||
tags: String(courseStructure.tags),
|
||||
public: String(courseStructure.public),
|
||||
},
|
||||
validate,
|
||||
onSubmit: async values => {
|
||||
|
||||
},
|
||||
enableReinitialize: true,
|
||||
});
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
// This code will run whenever form values are updated
|
||||
if (formik.values !== formik.initialValues) {
|
||||
dispatchCourse({ type: 'setIsNotSaved' });
|
||||
const updatedCourse = {
|
||||
...courseStructure,
|
||||
name: formik.values.name,
|
||||
description: formik.values.description,
|
||||
about: formik.values.about,
|
||||
learnings: formik.values.learnings,
|
||||
tags: formik.values.tags,
|
||||
public: formik.values.public,
|
||||
}
|
||||
dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse });
|
||||
}
|
||||
|
||||
}, [course, formik.values, formik.initialValues]);
|
||||
|
||||
return (
|
||||
<div> <div className="h-6"></div>
|
||||
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'>
|
||||
|
||||
{course.courseStructure && (
|
||||
<div className="editcourse-form">
|
||||
{error && (
|
||||
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
|
||||
<AlertTriangle size={18} />
|
||||
<div className="font-bold text-sm">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
<FormLayout onSubmit={formik.handleSubmit}>
|
||||
<FormField name="name">
|
||||
<FormLabelAndMessage label='Name' message={formik.errors.name} />
|
||||
<Form.Control asChild>
|
||||
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.name} type="text" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="description">
|
||||
<FormLabelAndMessage label='Description' message={formik.errors.description} />
|
||||
<Form.Control asChild>
|
||||
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.description} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="about">
|
||||
<FormLabelAndMessage label='About' message={formik.errors.about} />
|
||||
<Form.Control asChild>
|
||||
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.about} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="learnings">
|
||||
<FormLabelAndMessage label='Learnings' message={formik.errors.learnings} />
|
||||
<Form.Control asChild>
|
||||
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.learnings} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="tags">
|
||||
<FormLabelAndMessage label='Tags' message={formik.errors.tags} />
|
||||
<Form.Control asChild>
|
||||
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.tags} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="thumbnail">
|
||||
<FormLabelAndMessage label='Thumbnail' />
|
||||
<Form.Control asChild>
|
||||
<ThumbnailUpdate />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField className="flex items-center h-10" name="public">
|
||||
<div className='flex my-auto items-center'>
|
||||
<label className="text-black text-[15px] leading-none pr-[15px]" htmlFor="public-course">
|
||||
Public Course
|
||||
</label>
|
||||
<Switch.Root
|
||||
className="w-[42px] h-[25px] bg-neutral-200 rounded-full relative data-[state=checked]:bg-neutral-500 outline-none cursor-default"
|
||||
id="public-course"
|
||||
onCheckedChange={checked => formik.setFieldValue('public', checked)}
|
||||
checked={formik.values.public === 'true'}
|
||||
>
|
||||
<Switch.Thumb className="block w-[21px] h-[21px] bg-white rounded-full shadow-[0_2px_2px] shadow-neutral-300 transition-transform duration-100 translate-x-0.5 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
||||
</Switch.Root>
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
</FormLayout>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return errors
|
||||
}
|
||||
|
||||
export default EditCourseGeneral
|
||||
function EditCourseGeneral(props: EditCourseStructureProps) {
|
||||
const [error, setError] = React.useState('')
|
||||
const course = useCourse() as any
|
||||
const dispatchCourse = useCourseDispatch() as any
|
||||
|
||||
const courseStructure = course.courseStructure
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
name: String(courseStructure.name),
|
||||
description: String(courseStructure.description),
|
||||
about: String(courseStructure.about),
|
||||
learnings: String(courseStructure.learnings),
|
||||
tags: String(courseStructure.tags),
|
||||
public: String(courseStructure.public),
|
||||
},
|
||||
validate,
|
||||
onSubmit: async (values) => {},
|
||||
enableReinitialize: true,
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
// This code will run whenever form values are updated
|
||||
if (formik.values !== formik.initialValues) {
|
||||
dispatchCourse({ type: 'setIsNotSaved' })
|
||||
const updatedCourse = {
|
||||
...courseStructure,
|
||||
name: formik.values.name,
|
||||
description: formik.values.description,
|
||||
about: formik.values.about,
|
||||
learnings: formik.values.learnings,
|
||||
tags: formik.values.tags,
|
||||
public: formik.values.public,
|
||||
}
|
||||
dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse })
|
||||
}
|
||||
}, [course, formik.values, formik.initialValues])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{' '}
|
||||
<div className="h-6"></div>
|
||||
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5">
|
||||
{course.courseStructure && (
|
||||
<div className="editcourse-form">
|
||||
{error && (
|
||||
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
|
||||
<AlertTriangle size={18} />
|
||||
<div className="font-bold text-sm">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
<FormLayout onSubmit={formik.handleSubmit}>
|
||||
<FormField name="name">
|
||||
<FormLabelAndMessage
|
||||
label="Name"
|
||||
message={formik.errors.name}
|
||||
/>
|
||||
<Form.Control asChild>
|
||||
<Input
|
||||
style={{ backgroundColor: 'white' }}
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name}
|
||||
type="text"
|
||||
required
|
||||
/>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="description">
|
||||
<FormLabelAndMessage
|
||||
label="Description"
|
||||
message={formik.errors.description}
|
||||
/>
|
||||
<Form.Control asChild>
|
||||
<Textarea
|
||||
style={{ backgroundColor: 'white' }}
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.description}
|
||||
required
|
||||
/>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="about">
|
||||
<FormLabelAndMessage
|
||||
label="About"
|
||||
message={formik.errors.about}
|
||||
/>
|
||||
<Form.Control asChild>
|
||||
<Textarea
|
||||
style={{ backgroundColor: 'white' }}
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.about}
|
||||
required
|
||||
/>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="learnings">
|
||||
<FormLabelAndMessage
|
||||
label="Learnings"
|
||||
message={formik.errors.learnings}
|
||||
/>
|
||||
<Form.Control asChild>
|
||||
<Textarea
|
||||
style={{ backgroundColor: 'white' }}
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.learnings}
|
||||
required
|
||||
/>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="tags">
|
||||
<FormLabelAndMessage
|
||||
label="Tags"
|
||||
message={formik.errors.tags}
|
||||
/>
|
||||
<Form.Control asChild>
|
||||
<Textarea
|
||||
style={{ backgroundColor: 'white' }}
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.tags}
|
||||
required
|
||||
/>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="thumbnail">
|
||||
<FormLabelAndMessage label="Thumbnail" />
|
||||
<Form.Control asChild>
|
||||
<ThumbnailUpdate />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField className="flex items-center h-10" name="public">
|
||||
<div className="flex my-auto items-center">
|
||||
<label
|
||||
className="text-black text-[15px] leading-none pr-[15px]"
|
||||
htmlFor="public-course"
|
||||
>
|
||||
Public Course
|
||||
</label>
|
||||
<Switch.Root
|
||||
className="w-[42px] h-[25px] bg-neutral-200 rounded-full relative data-[state=checked]:bg-neutral-500 outline-none cursor-default"
|
||||
id="public-course"
|
||||
onCheckedChange={(checked) =>
|
||||
formik.setFieldValue('public', checked)
|
||||
}
|
||||
checked={formik.values.public === 'true'}
|
||||
>
|
||||
<Switch.Thumb className="block w-[21px] h-[21px] bg-white rounded-full shadow-[0_2px_2px] shadow-neutral-300 transition-transform duration-100 translate-x-0.5 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
||||
</Switch.Root>
|
||||
</div>
|
||||
</FormField>
|
||||
</FormLayout>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditCourseGeneral
|
||||
|
|
|
|||
|
|
@ -1,79 +1,100 @@
|
|||
import { useCourse } from '@components/Contexts/CourseContext';
|
||||
import { useOrg } from '@components/Contexts/OrgContext';
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
import { updateCourseThumbnail } from '@services/courses/courses';
|
||||
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
|
||||
import { ArrowBigUpDash, UploadCloud } from 'lucide-react';
|
||||
import { useCourse } from '@components/Contexts/CourseContext'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import { getAPIUrl } from '@services/config/config'
|
||||
import { updateCourseThumbnail } from '@services/courses/courses'
|
||||
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
|
||||
import { ArrowBigUpDash, UploadCloud } from 'lucide-react'
|
||||
import React from 'react'
|
||||
import { mutate } from 'swr';
|
||||
import { mutate } from 'swr'
|
||||
|
||||
function ThumbnailUpdate() {
|
||||
const course = useCourse() as any;
|
||||
const org = useOrg() as any;
|
||||
const [localThumbnail, setLocalThumbnail] = React.useState(null) as any;
|
||||
const [isLoading, setIsLoading] = React.useState(false) as any;
|
||||
const [error, setError] = React.useState('') as any;
|
||||
const course = useCourse() as any
|
||||
const org = useOrg() as any
|
||||
const [localThumbnail, setLocalThumbnail] = React.useState(null) as any
|
||||
const [isLoading, setIsLoading] = React.useState(false) as any
|
||||
const [error, setError] = React.useState('') as any
|
||||
|
||||
|
||||
const handleFileChange = async (event: any) => {
|
||||
const file = event.target.files[0];
|
||||
setLocalThumbnail(file);
|
||||
setIsLoading(true);
|
||||
const res = await updateCourseThumbnail(course.courseStructure.course_uuid, file)
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
||||
// wait for 1 second to show loading animation
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
if (res.success === false) {
|
||||
setError(res.HTTPmessage);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
setError('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='w-auto bg-gray-50 rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow'>
|
||||
<div className='flex flex-col justify-center items-center h-full'>
|
||||
<div className='flex flex-col justify-center items-center'>
|
||||
<div className='flex flex-col justify-center items-center'>
|
||||
{error && (
|
||||
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-2 transition-all shadow-sm">
|
||||
<div className="text-sm font-semibold">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
{localThumbnail ? (
|
||||
<img src={URL.createObjectURL(localThumbnail)} className={`${isLoading ? 'animate-pulse' : ''} shadow w-[200px] h-[100px] rounded-md`} />
|
||||
|
||||
) : (
|
||||
<img src={`${getCourseThumbnailMediaDirectory(org?.org_uuid, course.courseStructure.course_uuid, course.courseStructure.thumbnail_image)}`} className='shadow w-[200px] h-[100px] rounded-md' />
|
||||
)}
|
||||
|
||||
</div>
|
||||
{isLoading ? (<div className='flex justify-center items-center'>
|
||||
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
|
||||
<div
|
||||
className='font-bold animate-pulse antialiased items-center bg-green-200 text-gray text-sm rounded-md px-4 py-2 mt-4 flex'
|
||||
>
|
||||
<ArrowBigUpDash size={16} className='mr-2' />
|
||||
<span>Uploading</span>
|
||||
</div>
|
||||
</div>) : (
|
||||
|
||||
<div className='flex justify-center items-center'>
|
||||
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
|
||||
<button
|
||||
className='font-bold antialiased items-center text-gray text-sm rounded-md px-4 mt-6 flex'
|
||||
onClick={() => document.getElementById('fileInput')?.click()}
|
||||
>
|
||||
<UploadCloud size={16} className='mr-2' />
|
||||
<span>Change Thumbnail</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
const handleFileChange = async (event: any) => {
|
||||
const file = event.target.files[0]
|
||||
setLocalThumbnail(file)
|
||||
setIsLoading(true)
|
||||
const res = await updateCourseThumbnail(
|
||||
course.courseStructure.course_uuid,
|
||||
file
|
||||
)
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||
// wait for 1 second to show loading animation
|
||||
await new Promise((r) => setTimeout(r, 1500))
|
||||
if (res.success === false) {
|
||||
setError(res.HTTPmessage)
|
||||
} else {
|
||||
setIsLoading(false)
|
||||
setError('')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-auto bg-gray-50 rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow">
|
||||
<div className="flex flex-col justify-center items-center h-full">
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
{error && (
|
||||
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-2 transition-all shadow-sm">
|
||||
<div className="text-sm font-semibold">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
{localThumbnail ? (
|
||||
<img
|
||||
src={URL.createObjectURL(localThumbnail)}
|
||||
className={`${
|
||||
isLoading ? 'animate-pulse' : ''
|
||||
} shadow w-[200px] h-[100px] rounded-md`}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={`${getCourseThumbnailMediaDirectory(
|
||||
org?.org_uuid,
|
||||
course.courseStructure.course_uuid,
|
||||
course.courseStructure.thumbnail_image
|
||||
)}`}
|
||||
className="shadow w-[200px] h-[100px] rounded-md"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center">
|
||||
<input
|
||||
type="file"
|
||||
id="fileInput"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<div className="font-bold animate-pulse antialiased items-center bg-green-200 text-gray text-sm rounded-md px-4 py-2 mt-4 flex">
|
||||
<ArrowBigUpDash size={16} className="mr-2" />
|
||||
<span>Uploading</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-center items-center">
|
||||
<input
|
||||
type="file"
|
||||
id="fileInput"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<button
|
||||
className="font-bold antialiased items-center text-gray text-sm rounded-md px-4 mt-6 flex"
|
||||
onClick={() => document.getElementById('fileInput')?.click()}
|
||||
>
|
||||
<UploadCloud size={16} className="mr-2" />
|
||||
<span>Change Thumbnail</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThumbnailUpdate
|
||||
export default ThumbnailUpdate
|
||||
|
|
|
|||
|
|
@ -1,93 +1,116 @@
|
|||
import { useCourse } from '@components/Contexts/CourseContext';
|
||||
import NewActivityModal from '@components/Objects/Modals/Activities/Create/NewActivity';
|
||||
import Modal from '@components/StyledElements/Modal/Modal';
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
import { createActivity, createExternalVideoActivity, createFileActivity } from '@services/courses/activities';
|
||||
import { getOrganizationContextInfoWithoutCredentials } from '@services/organizations/orgs';
|
||||
import { revalidateTags } from '@services/utils/ts/requests';
|
||||
import { useCourse } from '@components/Contexts/CourseContext'
|
||||
import NewActivityModal from '@components/Objects/Modals/Activities/Create/NewActivity'
|
||||
import Modal from '@components/StyledElements/Modal/Modal'
|
||||
import { getAPIUrl } from '@services/config/config'
|
||||
import {
|
||||
createActivity,
|
||||
createExternalVideoActivity,
|
||||
createFileActivity,
|
||||
} from '@services/courses/activities'
|
||||
import { getOrganizationContextInfoWithoutCredentials } from '@services/organizations/orgs'
|
||||
import { revalidateTags } from '@services/utils/ts/requests'
|
||||
import { Layers } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { useEffect } from 'react'
|
||||
import { mutate } from 'swr';
|
||||
import { mutate } from 'swr'
|
||||
|
||||
type NewActivityButtonProps = {
|
||||
chapterId: string,
|
||||
orgslug: string
|
||||
chapterId: string
|
||||
orgslug: string
|
||||
}
|
||||
|
||||
function NewActivityButton(props: NewActivityButtonProps) {
|
||||
const [newActivityModal, setNewActivityModal] = React.useState(false);
|
||||
const router = useRouter();
|
||||
const course = useCourse() as any;
|
||||
const [newActivityModal, setNewActivityModal] = React.useState(false)
|
||||
const router = useRouter()
|
||||
const course = useCourse() as any
|
||||
|
||||
const openNewActivityModal = async (chapterId: any) => {
|
||||
setNewActivityModal(true);
|
||||
};
|
||||
const openNewActivityModal = async (chapterId: any) => {
|
||||
setNewActivityModal(true)
|
||||
}
|
||||
|
||||
const closeNewActivityModal = async () => {
|
||||
setNewActivityModal(false);
|
||||
};
|
||||
const closeNewActivityModal = async () => {
|
||||
setNewActivityModal(false)
|
||||
}
|
||||
|
||||
// Submit new activity
|
||||
const submitActivity = async (activity: any) => {
|
||||
let org = await getOrganizationContextInfoWithoutCredentials(props.orgslug, { revalidate: 1800 });
|
||||
await createActivity(activity, props.chapterId, org.org_id);
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
||||
setNewActivityModal(false);
|
||||
await revalidateTags(['courses'], props.orgslug);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Submit File Upload
|
||||
const submitFileActivity = async (file: any, type: any, activity: any, chapterId: string) => {
|
||||
await createFileActivity(file, type, activity, chapterId);
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
||||
setNewActivityModal(false);
|
||||
await revalidateTags(['courses'], props.orgslug);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
// Submit YouTube Video Upload
|
||||
const submitExternalVideo = async (external_video_data: any, activity: any, chapterId: string) => {
|
||||
await createExternalVideoActivity(external_video_data, activity, props.chapterId);
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
||||
setNewActivityModal(false);
|
||||
await revalidateTags(['courses'], props.orgslug);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
useEffect(() => { }
|
||||
, [course])
|
||||
|
||||
return (
|
||||
<div className='flex justify-center'>
|
||||
<Modal
|
||||
isDialogOpen={newActivityModal}
|
||||
onOpenChange={setNewActivityModal}
|
||||
minHeight="no-min"
|
||||
addDefCloseButton={false}
|
||||
dialogContent={<NewActivityModal
|
||||
closeModal={closeNewActivityModal}
|
||||
submitFileActivity={submitFileActivity}
|
||||
submitExternalVideo={submitExternalVideo}
|
||||
submitActivity={submitActivity}
|
||||
chapterId={props.chapterId}
|
||||
course={course}
|
||||
></NewActivityModal>}
|
||||
dialogTitle="Create Activity"
|
||||
dialogDescription="Choose between types of activities to add to the course"
|
||||
|
||||
/>
|
||||
<div onClick={() => {
|
||||
openNewActivityModal(props.chapterId)
|
||||
}} className="flex w-44 h-10 space-x-2 items-center py-2 my-3 rounded-xl justify-center text-white bg-black hover:cursor-pointer">
|
||||
<Layers className="" size={17} />
|
||||
<div className="text-sm mx-auto my-auto items-center font-bold">Add Activity</div>
|
||||
</div>
|
||||
</div>
|
||||
// Submit new activity
|
||||
const submitActivity = async (activity: any) => {
|
||||
let org = await getOrganizationContextInfoWithoutCredentials(
|
||||
props.orgslug,
|
||||
{ revalidate: 1800 }
|
||||
)
|
||||
await createActivity(activity, props.chapterId, org.org_id)
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||
setNewActivityModal(false)
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
// Submit File Upload
|
||||
const submitFileActivity = async (
|
||||
file: any,
|
||||
type: any,
|
||||
activity: any,
|
||||
chapterId: string
|
||||
) => {
|
||||
await createFileActivity(file, type, activity, chapterId)
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||
setNewActivityModal(false)
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
// Submit YouTube Video Upload
|
||||
const submitExternalVideo = async (
|
||||
external_video_data: any,
|
||||
activity: any,
|
||||
chapterId: string
|
||||
) => {
|
||||
await createExternalVideoActivity(
|
||||
external_video_data,
|
||||
activity,
|
||||
props.chapterId
|
||||
)
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||
setNewActivityModal(false)
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
useEffect(() => {}, [course])
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<Modal
|
||||
isDialogOpen={newActivityModal}
|
||||
onOpenChange={setNewActivityModal}
|
||||
minHeight="no-min"
|
||||
addDefCloseButton={false}
|
||||
dialogContent={
|
||||
<NewActivityModal
|
||||
closeModal={closeNewActivityModal}
|
||||
submitFileActivity={submitFileActivity}
|
||||
submitExternalVideo={submitExternalVideo}
|
||||
submitActivity={submitActivity}
|
||||
chapterId={props.chapterId}
|
||||
course={course}
|
||||
></NewActivityModal>
|
||||
}
|
||||
dialogTitle="Create Activity"
|
||||
dialogDescription="Choose between types of activities to add to the course"
|
||||
/>
|
||||
<div
|
||||
onClick={() => {
|
||||
openNewActivityModal(props.chapterId)
|
||||
}}
|
||||
className="flex w-44 h-10 space-x-2 items-center py-2 my-3 rounded-xl justify-center text-white bg-black hover:cursor-pointer"
|
||||
>
|
||||
<Layers className="" size={17} />
|
||||
<div className="text-sm mx-auto my-auto items-center font-bold">
|
||||
Add Activity
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewActivityButton
|
||||
export default NewActivityButton
|
||||
|
|
|
|||
|
|
@ -2,7 +2,16 @@ import ConfirmationModal from '@components/StyledElements/ConfirmationModal/Conf
|
|||
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
|
||||
import { deleteActivity, updateActivity } from '@services/courses/activities'
|
||||
import { revalidateTags } from '@services/utils/ts/requests'
|
||||
import { Eye, File, MoreVertical, Pencil, Save, Sparkles, Video, X } from 'lucide-react'
|
||||
import {
|
||||
Eye,
|
||||
File,
|
||||
MoreVertical,
|
||||
Pencil,
|
||||
Save,
|
||||
Sparkles,
|
||||
Video,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React from 'react'
|
||||
|
|
@ -10,124 +19,210 @@ import { Draggable } from 'react-beautiful-dnd'
|
|||
import { mutate } from 'swr'
|
||||
|
||||
type ActivitiyElementProps = {
|
||||
orgslug: string,
|
||||
activity: any,
|
||||
activityIndex: any,
|
||||
course_uuid: string
|
||||
orgslug: string
|
||||
activity: any
|
||||
activityIndex: any
|
||||
course_uuid: string
|
||||
}
|
||||
|
||||
interface ModifiedActivityInterface {
|
||||
activityId: string;
|
||||
activityName: string;
|
||||
activityId: string
|
||||
activityName: string
|
||||
}
|
||||
|
||||
function ActivityElement(props: ActivitiyElementProps) {
|
||||
const router = useRouter();
|
||||
const [modifiedActivity, setModifiedActivity] = React.useState<ModifiedActivityInterface | undefined>(undefined);
|
||||
const [selectedActivity, setSelectedActivity] = React.useState<string | undefined>(undefined);
|
||||
const activityUUID = props.activity.activity_uuid;
|
||||
const router = useRouter()
|
||||
const [modifiedActivity, setModifiedActivity] = React.useState<
|
||||
ModifiedActivityInterface | undefined
|
||||
>(undefined)
|
||||
const [selectedActivity, setSelectedActivity] = React.useState<
|
||||
string | undefined
|
||||
>(undefined)
|
||||
const activityUUID = props.activity.activity_uuid
|
||||
|
||||
async function deleteActivityUI() {
|
||||
await deleteActivity(props.activity.activity_uuid);
|
||||
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`);
|
||||
await revalidateTags(['courses'], props.orgslug);
|
||||
router.refresh();
|
||||
async function deleteActivityUI() {
|
||||
await deleteActivity(props.activity.activity_uuid)
|
||||
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
async function updateActivityName(activityId: string) {
|
||||
if (
|
||||
modifiedActivity?.activityId === activityId &&
|
||||
selectedActivity !== undefined
|
||||
) {
|
||||
setSelectedActivity(undefined)
|
||||
let modifiedActivityCopy = {
|
||||
name: modifiedActivity.activityName,
|
||||
description: '',
|
||||
type: props.activity.type,
|
||||
content: props.activity.content,
|
||||
}
|
||||
|
||||
await updateActivity(modifiedActivityCopy, activityUUID)
|
||||
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
router.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
async function updateActivityName(activityId: string) {
|
||||
if ((modifiedActivity?.activityId === activityId) && selectedActivity !== undefined) {
|
||||
setSelectedActivity(undefined);
|
||||
let modifiedActivityCopy = {
|
||||
name: modifiedActivity.activityName,
|
||||
description: '',
|
||||
type: props.activity.type,
|
||||
content: props.activity.content,
|
||||
}
|
||||
return (
|
||||
<Draggable
|
||||
key={props.activity.activity_uuid}
|
||||
draggableId={props.activity.activity_uuid}
|
||||
index={props.activityIndex}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className="flex flex-row py-2 my-2 w-full rounded-md bg-gray-50 text-gray-500 hover:bg-gray-100 hover:scale-102 hover:shadow space-x-1 items-center ring-1 ring-inset ring-gray-400/10 shadow-sm transition-all delay-100 duration-75 ease-linear"
|
||||
key={props.activity.id}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{/* Activity Type Icon */}
|
||||
<ActivityTypeIndicator activityType={props.activity.activity_type} />
|
||||
|
||||
await updateActivity(modifiedActivityCopy, activityUUID)
|
||||
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`);
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
router.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Draggable key={props.activity.activity_uuid} draggableId={props.activity.activity_uuid} index={props.activityIndex}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className="flex flex-row py-2 my-2 w-full rounded-md bg-gray-50 text-gray-500 hover:bg-gray-100 hover:scale-102 hover:shadow space-x-1 items-center ring-1 ring-inset ring-gray-400/10 shadow-sm transition-all delay-100 duration-75 ease-linear"
|
||||
key={props.activity.id}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
{/* Centered Activity Name */}
|
||||
<div className="grow items-center space-x-2 flex mx-auto justify-center">
|
||||
{selectedActivity === props.activity.id ? (
|
||||
<div className="chapter-modification-zone text-[7px] text-gray-600 shadow-inner bg-gray-200/60 py-1 px-4 rounded-lg space-x-3">
|
||||
<input
|
||||
type="text"
|
||||
className="bg-transparent outline-none text-xs text-gray-500"
|
||||
placeholder="Activity name"
|
||||
value={
|
||||
modifiedActivity
|
||||
? modifiedActivity?.activityName
|
||||
: props.activity.name
|
||||
}
|
||||
onChange={(e) =>
|
||||
setModifiedActivity({
|
||||
activityId: props.activity.id,
|
||||
activityName: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<button
|
||||
onClick={() => updateActivityName(props.activity.id)}
|
||||
className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900"
|
||||
>
|
||||
|
||||
{/* Activity Type Icon */}
|
||||
<ActivityTypeIndicator activityType={props.activity.activity_type} />
|
||||
|
||||
{/* Centered Activity Name */}
|
||||
<div className="grow items-center space-x-2 flex mx-auto justify-center">
|
||||
{selectedActivity === props.activity.id ?
|
||||
(<div className="chapter-modification-zone text-[7px] text-gray-600 shadow-inner bg-gray-200/60 py-1 px-4 rounded-lg space-x-3">
|
||||
<input type="text" className="bg-transparent outline-none text-xs text-gray-500" placeholder="Activity name" value={modifiedActivity ? modifiedActivity?.activityName : props.activity.name} onChange={(e) => setModifiedActivity({ activityId: props.activity.id, activityName: e.target.value })} />
|
||||
<button onClick={() => updateActivityName(props.activity.id)} className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900">
|
||||
<Save size={11} onClick={() => updateActivityName(props.activity.id)} />
|
||||
</button>
|
||||
</div>) : (<p className="first-letter:uppercase"> {props.activity.name} </p>)}
|
||||
<Pencil onClick={() => setSelectedActivity(props.activity.id)}
|
||||
size={12} className="text-neutral-400 hover:cursor-pointer" />
|
||||
</div>
|
||||
{/* Edit and View Button */}
|
||||
<div className="flex flex-row space-x-2">
|
||||
{props.activity.activity_type === "TYPE_DYNAMIC" && <>
|
||||
<Link
|
||||
href={getUriWithOrg(props.orgslug, "") + `/course/${props.course_uuid.replace("course_", "")}/activity/${props.activity.activity_uuid.replace("activity_", "")}/edit`}
|
||||
className=" hover:cursor-pointer p-1 px-3 bg-sky-700 rounded-md items-center"
|
||||
rel="noopener noreferrer">
|
||||
<div className="text-sky-100 font-bold text-xs" >Edit </div>
|
||||
</Link>
|
||||
</>}
|
||||
<Link
|
||||
href={getUriWithOrg(props.orgslug, "") + `/course/${props.course_uuid.replace("course_", "")}/activity/${props.activity.activity_uuid.replace("activity_", "")}`}
|
||||
className=" hover:cursor-pointer p-1 px-3 bg-gray-200 rounded-md"
|
||||
rel="noopener noreferrer">
|
||||
<Eye strokeWidth={2} size={15} className="text-gray-600" />
|
||||
</Link>
|
||||
</div>
|
||||
{/* Delete Button */}
|
||||
<div className="flex flex-row pr-3 space-x-1 items-center">
|
||||
<MoreVertical size={15} className="text-gray-300" />
|
||||
<ConfirmationModal
|
||||
confirmationMessage="Are you sure you want to delete this activity ?"
|
||||
confirmationButtonText="Delete Activity"
|
||||
dialogTitle={"Delete " + props.activity.name + " ?"}
|
||||
dialogTrigger={
|
||||
<div
|
||||
className=" hover:cursor-pointer p-1 px-5 bg-red-600 rounded-md"
|
||||
rel="noopener noreferrer">
|
||||
<X size={15} className="text-rose-200 font-bold" />
|
||||
</div>}
|
||||
functionToExecute={() => deleteActivityUI()}
|
||||
status='warning'
|
||||
></ConfirmationModal></div>
|
||||
</div>
|
||||
<Save
|
||||
size={11}
|
||||
onClick={() => updateActivityName(props.activity.id)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="first-letter:uppercase"> {props.activity.name} </p>
|
||||
)}
|
||||
</Draggable>
|
||||
)
|
||||
<Pencil
|
||||
onClick={() => setSelectedActivity(props.activity.id)}
|
||||
size={12}
|
||||
className="text-neutral-400 hover:cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
{/* Edit and View Button */}
|
||||
<div className="flex flex-row space-x-2">
|
||||
{props.activity.activity_type === 'TYPE_DYNAMIC' && (
|
||||
<>
|
||||
<Link
|
||||
href={
|
||||
getUriWithOrg(props.orgslug, '') +
|
||||
`/course/${props.course_uuid.replace(
|
||||
'course_',
|
||||
''
|
||||
)}/activity/${props.activity.activity_uuid.replace(
|
||||
'activity_',
|
||||
''
|
||||
)}/edit`
|
||||
}
|
||||
className=" hover:cursor-pointer p-1 px-3 bg-sky-700 rounded-md items-center"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="text-sky-100 font-bold text-xs">Edit </div>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<Link
|
||||
href={
|
||||
getUriWithOrg(props.orgslug, '') +
|
||||
`/course/${props.course_uuid.replace(
|
||||
'course_',
|
||||
''
|
||||
)}/activity/${props.activity.activity_uuid.replace(
|
||||
'activity_',
|
||||
''
|
||||
)}`
|
||||
}
|
||||
className=" hover:cursor-pointer p-1 px-3 bg-gray-200 rounded-md"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Eye strokeWidth={2} size={15} className="text-gray-600" />
|
||||
</Link>
|
||||
</div>
|
||||
{/* Delete Button */}
|
||||
<div className="flex flex-row pr-3 space-x-1 items-center">
|
||||
<MoreVertical size={15} className="text-gray-300" />
|
||||
<ConfirmationModal
|
||||
confirmationMessage="Are you sure you want to delete this activity ?"
|
||||
confirmationButtonText="Delete Activity"
|
||||
dialogTitle={'Delete ' + props.activity.name + ' ?'}
|
||||
dialogTrigger={
|
||||
<div
|
||||
className=" hover:cursor-pointer p-1 px-5 bg-red-600 rounded-md"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<X size={15} className="text-rose-200 font-bold" />
|
||||
</div>
|
||||
}
|
||||
functionToExecute={() => deleteActivityUI()}
|
||||
status="warning"
|
||||
></ConfirmationModal>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const ActivityTypeIndicator = (props: { activityType: string }) => {
|
||||
return (
|
||||
<div className="px-3 text-gray-300 space-x-1 w-28" >
|
||||
|
||||
|
||||
{props.activityType === "TYPE_VIDEO" && <>
|
||||
<div className="flex space-x-2 items-center"><Video size={16} /> <div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full mx-auto justify-center align-middle">Video</div> </div></>}
|
||||
{props.activityType === "TYPE_DOCUMENT" && <><div className="flex space-x-2 items-center"><div className="w-[30px]"><File size={16} /> </div><div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full">Document</div> </div></>}
|
||||
{props.activityType === "TYPE_DYNAMIC" && <><div className="flex space-x-2 items-center"><Sparkles size={16} /> <div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full">Dynamic</div> </div></>}
|
||||
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="px-3 text-gray-300 space-x-1 w-28">
|
||||
{props.activityType === 'TYPE_VIDEO' && (
|
||||
<>
|
||||
<div className="flex space-x-2 items-center">
|
||||
<Video size={16} />{' '}
|
||||
<div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full mx-auto justify-center align-middle">
|
||||
Video
|
||||
</div>{' '}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{props.activityType === 'TYPE_DOCUMENT' && (
|
||||
<>
|
||||
<div className="flex space-x-2 items-center">
|
||||
<div className="w-[30px]">
|
||||
<File size={16} />{' '}
|
||||
</div>
|
||||
<div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full">
|
||||
Document
|
||||
</div>{' '}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{props.activityType === 'TYPE_DYNAMIC' && (
|
||||
<>
|
||||
<div className="flex space-x-2 items-center">
|
||||
<Sparkles size={16} />{' '}
|
||||
<div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full">
|
||||
Dynamic
|
||||
</div>{' '}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ActivityElement
|
||||
export default ActivityElement
|
||||
|
|
|
|||
|
|
@ -1,129 +1,185 @@
|
|||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
|
||||
import { Hexagon, MoreHorizontal, MoreVertical, Pencil, Save, X } from 'lucide-react';
|
||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
|
||||
import {
|
||||
Hexagon,
|
||||
MoreHorizontal,
|
||||
MoreVertical,
|
||||
Pencil,
|
||||
Save,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import React from 'react'
|
||||
import { Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
import ActivityElement from './ActivityElement';
|
||||
import NewActivityButton from '../Buttons/NewActivityButton';
|
||||
import { deleteChapter, updateChapter } from '@services/courses/chapters';
|
||||
import { revalidateTags } from '@services/utils/ts/requests';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
import { mutate } from 'swr';
|
||||
import { Draggable, Droppable } from 'react-beautiful-dnd'
|
||||
import ActivityElement from './ActivityElement'
|
||||
import NewActivityButton from '../Buttons/NewActivityButton'
|
||||
import { deleteChapter, updateChapter } from '@services/courses/chapters'
|
||||
import { revalidateTags } from '@services/utils/ts/requests'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { getAPIUrl } from '@services/config/config'
|
||||
import { mutate } from 'swr'
|
||||
|
||||
type ChapterElementProps = {
|
||||
chapter: any,
|
||||
chapterIndex: number,
|
||||
orgslug: string
|
||||
course_uuid: string
|
||||
chapter: any
|
||||
chapterIndex: number
|
||||
orgslug: string
|
||||
course_uuid: string
|
||||
}
|
||||
|
||||
interface ModifiedChapterInterface {
|
||||
chapterId: string;
|
||||
chapterName: string;
|
||||
chapterId: string
|
||||
chapterName: string
|
||||
}
|
||||
|
||||
function ChapterElement(props: ChapterElementProps) {
|
||||
const activities = props.chapter.activities || [];
|
||||
const [modifiedChapter, setModifiedChapter] = React.useState<ModifiedChapterInterface | undefined>(undefined);
|
||||
const [selectedChapter, setSelectedChapter] = React.useState<string | undefined>(undefined);
|
||||
const activities = props.chapter.activities || []
|
||||
const [modifiedChapter, setModifiedChapter] = React.useState<
|
||||
ModifiedChapterInterface | undefined
|
||||
>(undefined)
|
||||
const [selectedChapter, setSelectedChapter] = React.useState<
|
||||
string | undefined
|
||||
>(undefined)
|
||||
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
|
||||
const deleteChapterUI = async () => {
|
||||
await deleteChapter(props.chapter.id);
|
||||
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`);
|
||||
await revalidateTags(['courses'], props.orgslug);
|
||||
router.refresh();
|
||||
};
|
||||
const deleteChapterUI = async () => {
|
||||
await deleteChapter(props.chapter.id)
|
||||
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
async function updateChapterName(chapterId: string) {
|
||||
if (modifiedChapter?.chapterId === chapterId) {
|
||||
setSelectedChapter(undefined);
|
||||
let modifiedChapterCopy = {
|
||||
name: modifiedChapter.chapterName,
|
||||
}
|
||||
await updateChapter(chapterId, modifiedChapterCopy)
|
||||
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`);
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
router.refresh();
|
||||
}
|
||||
async function updateChapterName(chapterId: string) {
|
||||
if (modifiedChapter?.chapterId === chapterId) {
|
||||
setSelectedChapter(undefined)
|
||||
let modifiedChapterCopy = {
|
||||
name: modifiedChapter.chapterName,
|
||||
}
|
||||
await updateChapter(chapterId, modifiedChapterCopy)
|
||||
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
router.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={props.chapter.chapter_uuid}
|
||||
draggableId={props.chapter.chapter_uuid}
|
||||
index={props.chapterIndex}
|
||||
return (
|
||||
<Draggable
|
||||
key={props.chapter.chapter_uuid}
|
||||
draggableId={props.chapter.chapter_uuid}
|
||||
index={props.chapterIndex}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 pt-6"
|
||||
key={props.chapter.chapter_uuid}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<div className="flex font-bold text-md items-center space-x-2 pb-3">
|
||||
<div className="flex grow text-lg space-x-3 items-center rounded-md ">
|
||||
<div className="bg-neutral-100 rounded-md p-2">
|
||||
<Hexagon
|
||||
strokeWidth={3}
|
||||
size={16}
|
||||
className="text-neutral-600 "
|
||||
/>
|
||||
</div>
|
||||
<div className="flex space-x-2 items-center">
|
||||
{selectedChapter === props.chapter.id ? (
|
||||
<div className="chapter-modification-zone bg-neutral-100 py-1 px-4 rounded-lg space-x-3">
|
||||
<input
|
||||
type="text"
|
||||
className="bg-transparent outline-none text-sm text-neutral-700"
|
||||
placeholder="Chapter name"
|
||||
value={
|
||||
modifiedChapter
|
||||
? modifiedChapter?.chapterName
|
||||
: props.chapter.name
|
||||
}
|
||||
onChange={(e) =>
|
||||
setModifiedChapter({
|
||||
chapterId: props.chapter.id,
|
||||
chapterName: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<button
|
||||
onClick={() => updateChapterName(props.chapter.id)}
|
||||
className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900"
|
||||
>
|
||||
<Save
|
||||
size={15}
|
||||
onClick={() => updateChapterName(props.chapter.id)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-neutral-700 first-letter:uppercase">
|
||||
{props.chapter.name}
|
||||
</p>
|
||||
)}
|
||||
<Pencil
|
||||
size={15}
|
||||
onClick={() => setSelectedChapter(props.chapter.id)}
|
||||
className="text-neutral-600 hover:cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<MoreVertical size={15} className="text-gray-300" />
|
||||
<ConfirmationModal
|
||||
confirmationButtonText="Delete Chapter"
|
||||
confirmationMessage="Are you sure you want to delete this chapter?"
|
||||
dialogTitle={'Delete ' + props.chapter.name + ' ?'}
|
||||
dialogTrigger={
|
||||
<div
|
||||
className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 pt-6"
|
||||
key={props.chapter.chapter_uuid}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
className=" hover:cursor-pointer p-1 px-4 bg-red-600 rounded-md shadow flex space-x-1 items-center text-rose-100 text-sm"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="flex font-bold text-md items-center space-x-2 pb-3" >
|
||||
<div className="flex grow text-lg space-x-3 items-center rounded-md ">
|
||||
<div className="bg-neutral-100 rounded-md p-2">
|
||||
<Hexagon strokeWidth={3} size={16} className="text-neutral-600 " />
|
||||
</div>
|
||||
<div className="flex space-x-2 items-center">
|
||||
{selectedChapter === props.chapter.id ?
|
||||
(<div className="chapter-modification-zone bg-neutral-100 py-1 px-4 rounded-lg space-x-3">
|
||||
<input type="text" className="bg-transparent outline-none text-sm text-neutral-700" placeholder="Chapter name" value={modifiedChapter ? modifiedChapter?.chapterName : props.chapter.name} onChange={(e) => setModifiedChapter({ chapterId: props.chapter.id, chapterName: e.target.value })} />
|
||||
<button onClick={() => updateChapterName(props.chapter.id)} className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900">
|
||||
<Save size={15} onClick={() => updateChapterName(props.chapter.id)} />
|
||||
</button>
|
||||
</div>) : (<p className="text-neutral-700 first-letter:uppercase">{props.chapter.name}</p>)}
|
||||
<Pencil size={15} onClick={() => setSelectedChapter(props.chapter.id)} className="text-neutral-600 hover:cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
<MoreVertical size={15} className="text-gray-300" />
|
||||
<ConfirmationModal
|
||||
confirmationButtonText="Delete Chapter"
|
||||
confirmationMessage="Are you sure you want to delete this chapter?"
|
||||
dialogTitle={"Delete " + props.chapter.name + " ?"}
|
||||
dialogTrigger={
|
||||
<div
|
||||
className=" hover:cursor-pointer p-1 px-4 bg-red-600 rounded-md shadow flex space-x-1 items-center text-rose-100 text-sm"
|
||||
rel="noopener noreferrer">
|
||||
<X size={15} className="text-rose-200 font-bold" />
|
||||
<p>Delete Chapter</p>
|
||||
</div>}
|
||||
functionToExecute={() => deleteChapterUI()}
|
||||
status='warning'
|
||||
></ConfirmationModal>
|
||||
</div>
|
||||
<Droppable key={props.chapter.chapter_uuid} droppableId={props.chapter.chapter_uuid} type="activity">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
<div className="flex flex-col">
|
||||
{activities.map((activity: any, index: any) => {
|
||||
return (
|
||||
<div key={index} className="flex items-center ">
|
||||
<ActivityElement
|
||||
orgslug={props.orgslug}
|
||||
course_uuid={props.course_uuid}
|
||||
activityIndex={index}
|
||||
activity={activity} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)}
|
||||
</Droppable>
|
||||
<NewActivityButton orgslug={props.orgslug} chapterId={props.chapter.id} />
|
||||
<div className='h-6'>
|
||||
<div className='flex items-center'><MoreHorizontal size={19} className="text-gray-300 mx-auto" /></div>
|
||||
</div>
|
||||
<X size={15} className="text-rose-200 font-bold" />
|
||||
<p>Delete Chapter</p>
|
||||
</div>
|
||||
}
|
||||
functionToExecute={() => deleteChapterUI()}
|
||||
status="warning"
|
||||
></ConfirmationModal>
|
||||
</div>
|
||||
<Droppable
|
||||
key={props.chapter.chapter_uuid}
|
||||
droppableId={props.chapter.chapter_uuid}
|
||||
type="activity"
|
||||
>
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
<div className="flex flex-col">
|
||||
{activities.map((activity: any, index: any) => {
|
||||
return (
|
||||
<div key={index} className="flex items-center ">
|
||||
<ActivityElement
|
||||
orgslug={props.orgslug}
|
||||
course_uuid={props.course_uuid}
|
||||
activityIndex={index}
|
||||
activity={activity}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
)
|
||||
</Droppable>
|
||||
<NewActivityButton
|
||||
orgslug={props.orgslug}
|
||||
chapterId={props.chapter.id}
|
||||
/>
|
||||
<div className="h-6">
|
||||
<div className="flex items-center">
|
||||
<MoreHorizontal size={19} className="text-gray-300 mx-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChapterElement
|
||||
export default ChapterElement
|
||||
|
|
|
|||
|
|
@ -1,149 +1,184 @@
|
|||
'use client';
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
import { revalidateTags } from '@services/utils/ts/requests';
|
||||
'use client'
|
||||
import { getAPIUrl } from '@services/config/config'
|
||||
import { revalidateTags } from '@services/utils/ts/requests'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
import { mutate } from 'swr';
|
||||
import ChapterElement from './DraggableElements/ChapterElement';
|
||||
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
||||
import { createChapter } from '@services/courses/chapters';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCourse, useCourseDispatch } from '@components/Contexts/CourseContext';
|
||||
import { Hexagon } from 'lucide-react';
|
||||
import Modal from '@components/StyledElements/Modal/Modal';
|
||||
import NewChapterModal from '@components/Objects/Modals/Chapters/NewChapter';
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd'
|
||||
import { mutate } from 'swr'
|
||||
import ChapterElement from './DraggableElements/ChapterElement'
|
||||
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||
import { createChapter } from '@services/courses/chapters'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import {
|
||||
useCourse,
|
||||
useCourseDispatch,
|
||||
} from '@components/Contexts/CourseContext'
|
||||
import { Hexagon } from 'lucide-react'
|
||||
import Modal from '@components/StyledElements/Modal/Modal'
|
||||
import NewChapterModal from '@components/Objects/Modals/Chapters/NewChapter'
|
||||
|
||||
type EditCourseStructureProps = {
|
||||
orgslug: string,
|
||||
course_uuid?: string,
|
||||
orgslug: string
|
||||
course_uuid?: string
|
||||
}
|
||||
|
||||
export type OrderPayload = {
|
||||
chapter_order_by_ids: [
|
||||
export type OrderPayload =
|
||||
| {
|
||||
chapter_order_by_ids: [
|
||||
{
|
||||
chapter_id: string,
|
||||
activities_order_by_ids: [
|
||||
{
|
||||
activity_id: string
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
} | undefined
|
||||
chapter_id: string
|
||||
activities_order_by_ids: [
|
||||
{
|
||||
activity_id: string
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
| undefined
|
||||
|
||||
const EditCourseStructure = (props: EditCourseStructureProps) => {
|
||||
const router = useRouter();
|
||||
// Check window availability
|
||||
const [winReady, setwinReady] = useState(false);
|
||||
const router = useRouter()
|
||||
// Check window availability
|
||||
const [winReady, setwinReady] = useState(false)
|
||||
|
||||
const dispatchCourse = useCourseDispatch() as any;
|
||||
const dispatchCourse = useCourseDispatch() as any
|
||||
|
||||
const [order, setOrder] = useState<OrderPayload>();
|
||||
const course = useCourse() as any;
|
||||
const course_structure = course ? course.courseStructure : {};
|
||||
const course_uuid = course ? course.courseStructure.course_uuid : '';
|
||||
const [order, setOrder] = useState<OrderPayload>()
|
||||
const course = useCourse() as any
|
||||
const course_structure = course ? course.courseStructure : {}
|
||||
const course_uuid = course ? course.courseStructure.course_uuid : ''
|
||||
|
||||
// New Chapter creation
|
||||
const [newChapterModal, setNewChapterModal] = useState(false);
|
||||
// New Chapter creation
|
||||
const [newChapterModal, setNewChapterModal] = useState(false)
|
||||
|
||||
const closeNewChapterModal = async () => {
|
||||
setNewChapterModal(false);
|
||||
};
|
||||
|
||||
// Submit new chapter
|
||||
const submitChapter = async (chapter: any) => {
|
||||
await createChapter(chapter);
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
||||
await revalidateTags(['courses'], props.orgslug);
|
||||
router.refresh();
|
||||
setNewChapterModal(false);
|
||||
};
|
||||
|
||||
const updateStructure = (result: any) => {
|
||||
const { destination, source, draggableId, type } = result;
|
||||
if (!destination) return;
|
||||
if (destination.droppableId === source.droppableId && destination.index === source.index) return;
|
||||
if (type === 'chapter') {
|
||||
const newChapterOrder = Array.from(course_structure.chapters);
|
||||
newChapterOrder.splice(source.index, 1);
|
||||
newChapterOrder.splice(destination.index, 0, course_structure.chapters[source.index]);
|
||||
dispatchCourse({ type: 'setCourseStructure', payload: { ...course_structure, chapters: newChapterOrder } })
|
||||
dispatchCourse({ type: 'setIsNotSaved' })
|
||||
}
|
||||
if (type === 'activity') {
|
||||
const newChapterOrder = Array.from(course_structure.chapters);
|
||||
const sourceChapter = newChapterOrder.find((chapter: any) => chapter.chapter_uuid === source.droppableId) as any;
|
||||
const destinationChapter = newChapterOrder.find((chapter: any) => chapter.chapter_uuid === destination.droppableId) ? newChapterOrder.find((chapter: any) => chapter.chapter_uuid === destination.droppableId) : sourceChapter;
|
||||
const activity = sourceChapter.activities.find((activity: any) => activity.activity_uuid === draggableId);
|
||||
sourceChapter.activities.splice(source.index, 1);
|
||||
destinationChapter.activities.splice(destination.index, 0, activity);
|
||||
dispatchCourse({ type: 'setCourseStructure', payload: { ...course_structure, chapters: newChapterOrder } })
|
||||
dispatchCourse({ type: 'setIsNotSaved' })
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setwinReady(true);
|
||||
|
||||
}, [props.course_uuid, course_structure, course]);
|
||||
|
||||
|
||||
if (!course) return <PageLoading></PageLoading>
|
||||
|
||||
return (
|
||||
<div className='flex flex-col'>
|
||||
<div className="h-6"></div>
|
||||
{winReady ?
|
||||
<DragDropContext onDragEnd={updateStructure}>
|
||||
<Droppable type='chapter' droppableId='chapters'>
|
||||
{(provided) => (
|
||||
<div
|
||||
className='space-y-4'
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}>
|
||||
{course_structure.chapters && course_structure.chapters.map((chapter: any, index: any) => {
|
||||
return (
|
||||
|
||||
<ChapterElement
|
||||
key={chapter.chapter_uuid}
|
||||
chapterIndex={index}
|
||||
orgslug={props.orgslug}
|
||||
course_uuid={course_uuid}
|
||||
chapter={chapter} />
|
||||
)
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
|
||||
{/* New Chapter Modal */}
|
||||
<Modal
|
||||
isDialogOpen={newChapterModal}
|
||||
onOpenChange={setNewChapterModal}
|
||||
minHeight="sm"
|
||||
dialogContent={<NewChapterModal
|
||||
course={course ? course.courseStructure : null}
|
||||
closeModal={closeNewChapterModal}
|
||||
submitChapter={submitChapter}
|
||||
></NewChapterModal>}
|
||||
dialogTitle="Create chapter"
|
||||
dialogDescription="Add a new chapter to the course"
|
||||
dialogTrigger={
|
||||
<div className="w-44 my-16 py-5 max-w-screen-2xl mx-auto bg-cyan-800 text-white rounded-xl shadow-sm px-6 items-center flex flex-row h-10">
|
||||
<div className='mx-auto flex space-x-2 items-center hover:cursor-pointer'>
|
||||
<Hexagon strokeWidth={3} size={16} className="text-white text-sm " />
|
||||
<div className='font-bold text-sm'>Add Chapter</div></div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</DragDropContext>
|
||||
|
||||
: <></>}
|
||||
</div>
|
||||
const closeNewChapterModal = async () => {
|
||||
setNewChapterModal(false)
|
||||
}
|
||||
|
||||
// Submit new chapter
|
||||
const submitChapter = async (chapter: any) => {
|
||||
await createChapter(chapter)
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
router.refresh()
|
||||
setNewChapterModal(false)
|
||||
}
|
||||
|
||||
const updateStructure = (result: any) => {
|
||||
const { destination, source, draggableId, type } = result
|
||||
if (!destination) return
|
||||
if (
|
||||
destination.droppableId === source.droppableId &&
|
||||
destination.index === source.index
|
||||
)
|
||||
return
|
||||
if (type === 'chapter') {
|
||||
const newChapterOrder = Array.from(course_structure.chapters)
|
||||
newChapterOrder.splice(source.index, 1)
|
||||
newChapterOrder.splice(
|
||||
destination.index,
|
||||
0,
|
||||
course_structure.chapters[source.index]
|
||||
)
|
||||
dispatchCourse({
|
||||
type: 'setCourseStructure',
|
||||
payload: { ...course_structure, chapters: newChapterOrder },
|
||||
})
|
||||
dispatchCourse({ type: 'setIsNotSaved' })
|
||||
}
|
||||
if (type === 'activity') {
|
||||
const newChapterOrder = Array.from(course_structure.chapters)
|
||||
const sourceChapter = newChapterOrder.find(
|
||||
(chapter: any) => chapter.chapter_uuid === source.droppableId
|
||||
) as any
|
||||
const destinationChapter = newChapterOrder.find(
|
||||
(chapter: any) => chapter.chapter_uuid === destination.droppableId
|
||||
)
|
||||
? newChapterOrder.find(
|
||||
(chapter: any) => chapter.chapter_uuid === destination.droppableId
|
||||
)
|
||||
: sourceChapter
|
||||
const activity = sourceChapter.activities.find(
|
||||
(activity: any) => activity.activity_uuid === draggableId
|
||||
)
|
||||
sourceChapter.activities.splice(source.index, 1)
|
||||
destinationChapter.activities.splice(destination.index, 0, activity)
|
||||
dispatchCourse({
|
||||
type: 'setCourseStructure',
|
||||
payload: { ...course_structure, chapters: newChapterOrder },
|
||||
})
|
||||
dispatchCourse({ type: 'setIsNotSaved' })
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setwinReady(true)
|
||||
}, [props.course_uuid, course_structure, course])
|
||||
|
||||
if (!course) return <PageLoading></PageLoading>
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="h-6"></div>
|
||||
{winReady ? (
|
||||
<DragDropContext onDragEnd={updateStructure}>
|
||||
<Droppable type="chapter" droppableId="chapters">
|
||||
{(provided) => (
|
||||
<div
|
||||
className="space-y-4"
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{course_structure.chapters &&
|
||||
course_structure.chapters.map((chapter: any, index: any) => {
|
||||
return (
|
||||
<ChapterElement
|
||||
key={chapter.chapter_uuid}
|
||||
chapterIndex={index}
|
||||
orgslug={props.orgslug}
|
||||
course_uuid={course_uuid}
|
||||
chapter={chapter}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
|
||||
{/* New Chapter Modal */}
|
||||
<Modal
|
||||
isDialogOpen={newChapterModal}
|
||||
onOpenChange={setNewChapterModal}
|
||||
minHeight="sm"
|
||||
dialogContent={
|
||||
<NewChapterModal
|
||||
course={course ? course.courseStructure : null}
|
||||
closeModal={closeNewChapterModal}
|
||||
submitChapter={submitChapter}
|
||||
></NewChapterModal>
|
||||
}
|
||||
dialogTitle="Create chapter"
|
||||
dialogDescription="Add a new chapter to the course"
|
||||
dialogTrigger={
|
||||
<div className="w-44 my-16 py-5 max-w-screen-2xl mx-auto bg-cyan-800 text-white rounded-xl shadow-sm px-6 items-center flex flex-row h-10">
|
||||
<div className="mx-auto flex space-x-2 items-center hover:cursor-pointer">
|
||||
<Hexagon
|
||||
strokeWidth={3}
|
||||
size={16}
|
||||
className="text-white text-sm "
|
||||
/>
|
||||
<div className="font-bold text-sm">Add Chapter</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</DragDropContext>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditCourseStructure
|
||||
export default EditCourseStructure
|
||||
|
|
|
|||
|
|
@ -1,77 +1,75 @@
|
|||
"use client";
|
||||
'use client'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { updateOrganization, uploadOrganizationLogo } from '@services/settings/org';
|
||||
import { UploadCloud } from 'lucide-react';
|
||||
import { revalidateTags } from '@services/utils/ts/requests';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useOrg } from '@components/Contexts/OrgContext';
|
||||
import { Field, Form, Formik } from 'formik'
|
||||
import {
|
||||
updateOrganization,
|
||||
uploadOrganizationLogo,
|
||||
} from '@services/settings/org'
|
||||
import { UploadCloud } from 'lucide-react'
|
||||
import { revalidateTags } from '@services/utils/ts/requests'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
|
||||
interface OrganizationValues {
|
||||
name: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
logo: string;
|
||||
email: string;
|
||||
name: string
|
||||
description: string
|
||||
slug: string
|
||||
logo: string
|
||||
email: string
|
||||
}
|
||||
|
||||
function OrgEditGeneral(props: any) {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const router = useRouter();
|
||||
const org = useOrg() as any;
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
const router = useRouter()
|
||||
const org = useOrg() as any
|
||||
// ...
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files && event.target.files.length > 0) {
|
||||
const file = event.target.files[0];
|
||||
setSelectedFile(file);
|
||||
const file = event.target.files[0]
|
||||
setSelectedFile(file)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const uploadLogo = async () => {
|
||||
if (selectedFile) {
|
||||
let org_id = org.id;
|
||||
await uploadOrganizationLogo(org_id, selectedFile);
|
||||
setSelectedFile(null); // Reset the selected file
|
||||
await revalidateTags(['organizations'], org.slug);
|
||||
router.refresh();
|
||||
|
||||
let org_id = org.id
|
||||
await uploadOrganizationLogo(org_id, selectedFile)
|
||||
setSelectedFile(null) // Reset the selected file
|
||||
await revalidateTags(['organizations'], org.slug)
|
||||
router.refresh()
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
let orgValues: OrganizationValues = {
|
||||
name: org?.name,
|
||||
description: org?.description,
|
||||
slug: org?.slug,
|
||||
logo: org?.logo,
|
||||
email: org?.email
|
||||
email: org?.email,
|
||||
}
|
||||
|
||||
const updateOrg = async (values: OrganizationValues) => {
|
||||
let org_id = org.id;
|
||||
await updateOrganization(org_id, values);
|
||||
let org_id = org.id
|
||||
await updateOrganization(org_id, values)
|
||||
|
||||
// Mutate the org
|
||||
await revalidateTags(['organizations'], org.slug);
|
||||
router.refresh();
|
||||
await revalidateTags(['organizations'], org.slug)
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
}
|
||||
, [org])
|
||||
useEffect(() => {}, [org])
|
||||
|
||||
return (
|
||||
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'>
|
||||
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5">
|
||||
<Formik
|
||||
enableReinitialize
|
||||
enableReinitialize
|
||||
initialValues={orgValues}
|
||||
onSubmit={(values, { setSubmitting }) => {
|
||||
setTimeout(() => {
|
||||
setSubmitting(false);
|
||||
setSubmitting(false)
|
||||
updateOrg(values)
|
||||
}, 400);
|
||||
}, 400)
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
|
|
@ -115,7 +113,6 @@ function OrgEditGeneral(props: any) {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<label className="block mb-2 font-bold" htmlFor="slug">
|
||||
Slug
|
||||
</label>
|
||||
|
|
@ -143,11 +140,10 @@ function OrgEditGeneral(props: any) {
|
|||
Submit
|
||||
</button>
|
||||
</Form>
|
||||
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrgEditGeneral
|
||||
export default OrgEditGeneral
|
||||
|
|
|
|||
|
|
@ -4,30 +4,68 @@ import Link from 'next/link'
|
|||
import React from 'react'
|
||||
|
||||
type BreadCrumbsProps = {
|
||||
type: 'courses' | 'user' | 'users' | 'org' | 'orgusers'
|
||||
last_breadcrumb?: string
|
||||
type: 'courses' | 'user' | 'users' | 'org' | 'orgusers'
|
||||
last_breadcrumb?: string
|
||||
}
|
||||
|
||||
function BreadCrumbs(props: BreadCrumbsProps) {
|
||||
const course = useCourse() as any;
|
||||
const course = useCourse() as any
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='h-7'></div>
|
||||
<div className='text-gray-400 tracking-tight font-medium text-sm flex space-x-1'>
|
||||
<div className='flex items-center space-x-1'>
|
||||
{props.type == 'courses' ? <div className='flex space-x-2 items-center'> <Book className='text-gray' size={14}></Book><Link href='/dash/courses'>Courses</Link></div> : ''}
|
||||
{props.type == 'user' ? <div className='flex space-x-2 items-center'> <User className='text-gray' size={14}></User><Link href='/dash/user-account/settings/general'>Account Settings</Link></div> : ''}
|
||||
{props.type == 'orgusers' ? <div className='flex space-x-2 items-center'> <Users className='text-gray' size={14}></Users><Link href='/dash/users/settings/users'>Organization users</Link></div> : ''}
|
||||
|
||||
{props.type == 'org' ? <div className='flex space-x-2 items-center'> <School className='text-gray' size={14}></School><Link href='/dash/users'>Organization Settings</Link></div> : ''}
|
||||
<div className='flex items-center space-x-1 first-letter:uppercase'>
|
||||
{props.last_breadcrumb ? <ChevronRight size={17} /> : ''}
|
||||
<div className='first-letter:uppercase'> {props.last_breadcrumb}</div>
|
||||
</div></div></div>
|
||||
return (
|
||||
<div>
|
||||
<div className="h-7"></div>
|
||||
<div className="text-gray-400 tracking-tight font-medium text-sm flex space-x-1">
|
||||
<div className="flex items-center space-x-1">
|
||||
{props.type == 'courses' ? (
|
||||
<div className="flex space-x-2 items-center">
|
||||
{' '}
|
||||
<Book className="text-gray" size={14}></Book>
|
||||
<Link href="/dash/courses">Courses</Link>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{props.type == 'user' ? (
|
||||
<div className="flex space-x-2 items-center">
|
||||
{' '}
|
||||
<User className="text-gray" size={14}></User>
|
||||
<Link href="/dash/user-account/settings/general">
|
||||
Account Settings
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{props.type == 'orgusers' ? (
|
||||
<div className="flex space-x-2 items-center">
|
||||
{' '}
|
||||
<Users className="text-gray" size={14}></Users>
|
||||
<Link href="/dash/users/settings/users">Organization users</Link>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
|
||||
{props.type == 'org' ? (
|
||||
<div className="flex space-x-2 items-center">
|
||||
{' '}
|
||||
<School className="text-gray" size={14}></School>
|
||||
<Link href="/dash/users">Organization Settings</Link>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<div className="flex items-center space-x-1 first-letter:uppercase">
|
||||
{props.last_breadcrumb ? <ChevronRight size={17} /> : ''}
|
||||
<div className="first-letter:uppercase">
|
||||
{' '}
|
||||
{props.last_breadcrumb}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BreadCrumbs
|
||||
export default BreadCrumbs
|
||||
|
|
|
|||
|
|
@ -1,43 +1,66 @@
|
|||
import { useCourse } from "@components/Contexts/CourseContext";
|
||||
import { useEffect } from "react";
|
||||
import BreadCrumbs from "./BreadCrumbs";
|
||||
import SaveState from "./SaveState";
|
||||
import { CourseOverviewParams } from "app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page";
|
||||
import { getUriWithOrg } from "@services/config/config";
|
||||
import { useOrg } from "@components/Contexts/OrgContext";
|
||||
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import EmptyThumbnailImage from '../../../public/empty_thumbnail.png';
|
||||
import { useCourse } from '@components/Contexts/CourseContext'
|
||||
import { useEffect } from 'react'
|
||||
import BreadCrumbs from './BreadCrumbs'
|
||||
import SaveState from './SaveState'
|
||||
import { CourseOverviewParams } from 'app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page'
|
||||
import { getUriWithOrg } from '@services/config/config'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import EmptyThumbnailImage from '../../../public/empty_thumbnail.png'
|
||||
|
||||
export function CourseOverviewTop({ params }: { params: CourseOverviewParams }) {
|
||||
const course = useCourse() as any;
|
||||
const org = useOrg() as any;
|
||||
export function CourseOverviewTop({
|
||||
params,
|
||||
}: {
|
||||
params: CourseOverviewParams
|
||||
}) {
|
||||
const course = useCourse() as any
|
||||
const org = useOrg() as any
|
||||
|
||||
useEffect(() => { }
|
||||
, [course, org])
|
||||
useEffect(() => {}, [course, org])
|
||||
|
||||
return (
|
||||
<>
|
||||
<BreadCrumbs type='courses' last_breadcrumb={course.courseStructure.name} ></BreadCrumbs>
|
||||
<div className='flex'>
|
||||
<div className='flex py-5 grow items-center'>
|
||||
<Link href={getUriWithOrg(org?.slug, "") + `/course/${params.courseuuid}`}>
|
||||
{course?.courseStructure?.thumbnail_image ?
|
||||
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(org?.org_uuid, "course_" + params.courseuuid, course.courseStructure.thumbnail_image)}`} alt="" />
|
||||
:
|
||||
<Image width={100} className="h-[57px] rounded-md drop-shadow-md" src={EmptyThumbnailImage} alt="" />}
|
||||
</Link>
|
||||
<div className="flex flex-col course_metadata justify-center pl-5">
|
||||
<div className='text-gray-400 font-semibold text-sm'>Course</div>
|
||||
<div className='text-black font-bold text-xl -mt-1 first-letter:uppercase'>{course.courseStructure.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<SaveState orgslug={params.orgslug} />
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<BreadCrumbs
|
||||
type="courses"
|
||||
last_breadcrumb={course.courseStructure.name}
|
||||
></BreadCrumbs>
|
||||
<div className="flex">
|
||||
<div className="flex py-5 grow items-center">
|
||||
<Link
|
||||
href={getUriWithOrg(org?.slug, '') + `/course/${params.courseuuid}`}
|
||||
>
|
||||
{course?.courseStructure?.thumbnail_image ? (
|
||||
<img
|
||||
className="w-[100px] h-[57px] rounded-md drop-shadow-md"
|
||||
src={`${getCourseThumbnailMediaDirectory(
|
||||
org?.org_uuid,
|
||||
'course_' + params.courseuuid,
|
||||
course.courseStructure.thumbnail_image
|
||||
)}`}
|
||||
alt=""
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
width={100}
|
||||
className="h-[57px] rounded-md drop-shadow-md"
|
||||
src={EmptyThumbnailImage}
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
<div className="flex flex-col course_metadata justify-center pl-5">
|
||||
<div className="text-gray-400 font-semibold text-sm">Course</div>
|
||||
<div className="text-black font-bold text-xl -mt-1 first-letter:uppercase">
|
||||
{course.courseStructure.name}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<SaveState orgslug={params.orgslug} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,107 +1,172 @@
|
|||
'use client';
|
||||
import { useOrg } from '@components/Contexts/OrgContext';
|
||||
import { useSession } from '@components/Contexts/SessionContext';
|
||||
'use client'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import { useSession } from '@components/Contexts/SessionContext'
|
||||
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
|
||||
import LearnHouseDashboardLogo from '@public/dashLogo.png';
|
||||
import { logout } from '@services/auth/auth';
|
||||
import LearnHouseDashboardLogo from '@public/dashLogo.png'
|
||||
import { logout } from '@services/auth/auth'
|
||||
import { BookCopy, Home, LogOut, School, Settings, Users } from 'lucide-react'
|
||||
import Image from 'next/image';
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { useEffect } from 'react'
|
||||
import UserAvatar from '../../Objects/UserAvatar';
|
||||
import AdminAuthorization from '@components/Security/AdminAuthorization';
|
||||
import UserAvatar from '../../Objects/UserAvatar'
|
||||
import AdminAuthorization from '@components/Security/AdminAuthorization'
|
||||
|
||||
function LeftMenu() {
|
||||
const org = useOrg() as any;
|
||||
const session = useSession() as any;
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const route = useRouter();
|
||||
const org = useOrg() as any
|
||||
const session = useSession() as any
|
||||
const [loading, setLoading] = React.useState(true)
|
||||
const route = useRouter()
|
||||
|
||||
function waitForEverythingToLoad() {
|
||||
if (org && session) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
function waitForEverythingToLoad() {
|
||||
if (org && session) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function logOutUI() {
|
||||
const res = await logout();
|
||||
if (res) {
|
||||
route.push('/login');
|
||||
}
|
||||
|
||||
async function logOutUI() {
|
||||
const res = await logout()
|
||||
if (res) {
|
||||
route.push('/login')
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (waitForEverythingToLoad()) {
|
||||
setLoading(false);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (waitForEverythingToLoad()) {
|
||||
setLoading(false)
|
||||
}
|
||||
, [loading])
|
||||
}, [loading])
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ background: "linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(271.56% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgb(20 19 19)" }}
|
||||
className='flex flex-col w-[90px] bg-black h-screen text-white shadow-xl'>
|
||||
<div className='flex flex-col h-full'>
|
||||
<div className='flex h-20 mt-6'>
|
||||
<Link className='flex flex-col items-center mx-auto space-y-3' href={"/"}>
|
||||
<ToolTip content={'Back to Home'} slateBlack sideOffset={8} side='right' >
|
||||
<Image alt="Learnhouse logo" width={40} src={LearnHouseDashboardLogo} />
|
||||
</ToolTip>
|
||||
<ToolTip content={'Your Organization'} slateBlack sideOffset={8} side='right' >
|
||||
<div className='py-1 px-3 bg-black/40 opacity-40 rounded-md text-[10px] justify-center text-center'>{org?.name}</div>
|
||||
</ToolTip>
|
||||
</Link>
|
||||
</div>
|
||||
<div className='flex grow flex-col justify-center space-y-5 items-center mx-auto'>
|
||||
{/* <ToolTip content={"Back to " + org?.name + "'s Home"} slateBlack sideOffset={8} side='right' >
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(271.56% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgb(20 19 19)',
|
||||
}}
|
||||
className="flex flex-col w-[90px] bg-black h-screen text-white shadow-xl"
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex h-20 mt-6">
|
||||
<Link
|
||||
className="flex flex-col items-center mx-auto space-y-3"
|
||||
href={'/'}
|
||||
>
|
||||
<ToolTip
|
||||
content={'Back to Home'}
|
||||
slateBlack
|
||||
sideOffset={8}
|
||||
side="right"
|
||||
>
|
||||
<Image
|
||||
alt="Learnhouse logo"
|
||||
width={40}
|
||||
src={LearnHouseDashboardLogo}
|
||||
/>
|
||||
</ToolTip>
|
||||
<ToolTip
|
||||
content={'Your Organization'}
|
||||
slateBlack
|
||||
sideOffset={8}
|
||||
side="right"
|
||||
>
|
||||
<div className="py-1 px-3 bg-black/40 opacity-40 rounded-md text-[10px] justify-center text-center">
|
||||
{org?.name}
|
||||
</div>
|
||||
</ToolTip>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex grow flex-col justify-center space-y-5 items-center mx-auto">
|
||||
{/* <ToolTip content={"Back to " + org?.name + "'s Home"} slateBlack sideOffset={8} side='right' >
|
||||
<Link className='bg-white text-black hover:text-white rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/`} ><ArrowLeft className='hover:text-white' size={18} /></Link>
|
||||
</ToolTip> */}
|
||||
<AdminAuthorization authorizationMode="component">
|
||||
<ToolTip content={"Home"} slateBlack sideOffset={8} side='right' >
|
||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash`} ><Home size={18} /></Link>
|
||||
</ToolTip>
|
||||
<ToolTip content={"Courses"} slateBlack sideOffset={8} side='right' >
|
||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/courses`} ><BookCopy size={18} /></Link>
|
||||
</ToolTip>
|
||||
<ToolTip content={"Users"} slateBlack sideOffset={8} side='right' >
|
||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/users/settings/users`} ><Users size={18} /></Link>
|
||||
</ToolTip>
|
||||
<ToolTip content={"Organization"} slateBlack sideOffset={8} side='right' >
|
||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/org/settings/general`} ><School size={18} /></Link>
|
||||
</ToolTip>
|
||||
</AdminAuthorization>
|
||||
</div>
|
||||
<div className='flex flex-col mx-auto pb-7 space-y-2'>
|
||||
|
||||
<div className="flex items-center flex-col space-y-2">
|
||||
<ToolTip content={'@' + session.user.username} slateBlack sideOffset={8} side='right' >
|
||||
<div className='mx-auto'>
|
||||
<UserAvatar border='border-4' width={35} />
|
||||
</div>
|
||||
</ToolTip>
|
||||
<div className='flex items-center flex-col space-y-1'>
|
||||
<ToolTip content={session.user.username + "'s Settings"} slateBlack sideOffset={8} side='right' >
|
||||
<Link href={'/dash/user-account/settings/general'} className='py-3'>
|
||||
<Settings className='mx-auto text-neutral-400 cursor-pointer' size={18} />
|
||||
</Link>
|
||||
</ToolTip>
|
||||
<ToolTip content={'Logout'} slateBlack sideOffset={8} side='right' >
|
||||
<LogOut onClick={() => logOutUI()} className='mx-auto text-neutral-400 cursor-pointer' size={14} />
|
||||
</ToolTip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<AdminAuthorization authorizationMode="component">
|
||||
<ToolTip content={'Home'} slateBlack sideOffset={8} side="right">
|
||||
<Link
|
||||
className="bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear"
|
||||
href={`/dash`}
|
||||
>
|
||||
<Home size={18} />
|
||||
</Link>
|
||||
</ToolTip>
|
||||
<ToolTip content={'Courses'} slateBlack sideOffset={8} side="right">
|
||||
<Link
|
||||
className="bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear"
|
||||
href={`/dash/courses`}
|
||||
>
|
||||
<BookCopy size={18} />
|
||||
</Link>
|
||||
</ToolTip>
|
||||
<ToolTip content={'Users'} slateBlack sideOffset={8} side="right">
|
||||
<Link
|
||||
className="bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear"
|
||||
href={`/dash/users/settings/users`}
|
||||
>
|
||||
<Users size={18} />
|
||||
</Link>
|
||||
</ToolTip>
|
||||
<ToolTip
|
||||
content={'Organization'}
|
||||
slateBlack
|
||||
sideOffset={8}
|
||||
side="right"
|
||||
>
|
||||
<Link
|
||||
className="bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear"
|
||||
href={`/dash/org/settings/general`}
|
||||
>
|
||||
<School size={18} />
|
||||
</Link>
|
||||
</ToolTip>
|
||||
</AdminAuthorization>
|
||||
</div>
|
||||
)
|
||||
<div className="flex flex-col mx-auto pb-7 space-y-2">
|
||||
<div className="flex items-center flex-col space-y-2">
|
||||
<ToolTip
|
||||
content={'@' + session.user.username}
|
||||
slateBlack
|
||||
sideOffset={8}
|
||||
side="right"
|
||||
>
|
||||
<div className="mx-auto">
|
||||
<UserAvatar border="border-4" width={35} />
|
||||
</div>
|
||||
</ToolTip>
|
||||
<div className="flex items-center flex-col space-y-1">
|
||||
<ToolTip
|
||||
content={session.user.username + "'s Settings"}
|
||||
slateBlack
|
||||
sideOffset={8}
|
||||
side="right"
|
||||
>
|
||||
<Link
|
||||
href={'/dash/user-account/settings/general'}
|
||||
className="py-3"
|
||||
>
|
||||
<Settings
|
||||
className="mx-auto text-neutral-400 cursor-pointer"
|
||||
size={18}
|
||||
/>
|
||||
</Link>
|
||||
</ToolTip>
|
||||
<ToolTip
|
||||
content={'Logout'}
|
||||
slateBlack
|
||||
sideOffset={8}
|
||||
side="right"
|
||||
>
|
||||
<LogOut
|
||||
onClick={() => logOutUI()}
|
||||
className="mx-auto text-neutral-400 cursor-pointer"
|
||||
size={14}
|
||||
/>
|
||||
</ToolTip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LeftMenu
|
||||
|
||||
|
|
|
|||
|
|
@ -1,114 +1,127 @@
|
|||
'use client';
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
import { updateCourseOrderStructure } from '@services/courses/chapters';
|
||||
import { revalidateTags } from '@services/utils/ts/requests';
|
||||
import { useCourse, useCourseDispatch } from '@components/Contexts/CourseContext'
|
||||
'use client'
|
||||
import { getAPIUrl } from '@services/config/config'
|
||||
import { updateCourseOrderStructure } from '@services/courses/chapters'
|
||||
import { revalidateTags } from '@services/utils/ts/requests'
|
||||
import {
|
||||
useCourse,
|
||||
useCourseDispatch,
|
||||
} from '@components/Contexts/CourseContext'
|
||||
import { Check, SaveAllIcon, Timer } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { useEffect } from 'react'
|
||||
import { mutate } from 'swr';
|
||||
import { updateCourse } from '@services/courses/courses';
|
||||
import { mutate } from 'swr'
|
||||
import { updateCourse } from '@services/courses/courses'
|
||||
|
||||
function SaveState(props: { orgslug: string }) {
|
||||
const course = useCourse() as any;
|
||||
const router = useRouter();
|
||||
const saved = course ? course.isSaved : true;
|
||||
const dispatchCourse = useCourseDispatch() as any;
|
||||
const course_structure = course.courseStructure;
|
||||
|
||||
const saveCourseState = async () => {
|
||||
// Course order
|
||||
if (saved) return;
|
||||
await changeOrderBackend();
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
||||
// Course metadata
|
||||
await changeMetadataBackend();
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
dispatchCourse({ type: 'setIsSaved' })
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Course Order
|
||||
const changeOrderBackend = async () => {
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
||||
await updateCourseOrderStructure(course.courseStructure.course_uuid, course.courseOrder);
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
router.refresh();
|
||||
dispatchCourse({ type: 'setIsSaved' })
|
||||
}
|
||||
const course = useCourse() as any
|
||||
const router = useRouter()
|
||||
const saved = course ? course.isSaved : true
|
||||
const dispatchCourse = useCourseDispatch() as any
|
||||
const course_structure = course.courseStructure
|
||||
|
||||
const saveCourseState = async () => {
|
||||
// Course order
|
||||
if (saved) return
|
||||
await changeOrderBackend()
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||
// Course metadata
|
||||
const changeMetadataBackend = async () => {
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
||||
await updateCourse(course.courseStructure.course_uuid, course.courseStructure);
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
router.refresh();
|
||||
dispatchCourse({ type: 'setIsSaved' })
|
||||
}
|
||||
|
||||
|
||||
|
||||
const handleCourseOrder = (course_structure: any) => {
|
||||
const chapters = course_structure.chapters;
|
||||
const chapter_order_by_ids = chapters.map((chapter: any) => {
|
||||
return {
|
||||
chapter_id: chapter.id,
|
||||
activities_order_by_ids: chapter.activities.map((activity: any) => {
|
||||
return {
|
||||
activity_id: activity.id
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
dispatchCourse({ type: 'setCourseOrder', payload: { chapter_order_by_ids: chapter_order_by_ids } })
|
||||
dispatchCourse({ type: 'setIsNotSaved' })
|
||||
}
|
||||
|
||||
const initOrderPayload = () => {
|
||||
if (course_structure && course_structure.chapters) {
|
||||
handleCourseOrder(course_structure);
|
||||
dispatchCourse({ type: 'setIsSaved' })
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const changeOrderPayload = () => {
|
||||
if (course_structure && course_structure.chapters) {
|
||||
handleCourseOrder(course_structure);
|
||||
dispatchCourse({ type: 'setIsNotSaved' })
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (course_structure?.chapters) {
|
||||
initOrderPayload();
|
||||
}
|
||||
if (course_structure?.chapters && !saved) {
|
||||
changeOrderPayload();
|
||||
}
|
||||
}, [course_structure]); // This effect depends on the `course_structure` variable
|
||||
|
||||
return (
|
||||
<div className='flex space-x-4'>
|
||||
{saved ? <></> : <div className='text-gray-600 flex space-x-2 items-center antialiased'>
|
||||
<Timer size={15} />
|
||||
<div>
|
||||
Unsaved changes
|
||||
</div>
|
||||
|
||||
</div>}
|
||||
<div className={`px-4 py-2 rounded-lg drop-shadow-md cursor-pointer flex space-x-2 items-center font-bold antialiased transition-all ease-linear ` + (saved ? 'bg-gray-600 text-white' : 'bg-black text-white border hover:bg-gray-900 ')
|
||||
} onClick={saveCourseState}>
|
||||
|
||||
{saved ? <Check size={20} /> : <SaveAllIcon size={20} />}
|
||||
{saved ? <div className=''>Saved</div> : <div className=''>Save</div>}
|
||||
</div>
|
||||
</div>
|
||||
await changeMetadataBackend()
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
dispatchCourse({ type: 'setIsSaved' })
|
||||
}
|
||||
|
||||
//
|
||||
// Course Order
|
||||
const changeOrderBackend = async () => {
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||
await updateCourseOrderStructure(
|
||||
course.courseStructure.course_uuid,
|
||||
course.courseOrder
|
||||
)
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
router.refresh()
|
||||
dispatchCourse({ type: 'setIsSaved' })
|
||||
}
|
||||
|
||||
// Course metadata
|
||||
const changeMetadataBackend = async () => {
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||
await updateCourse(
|
||||
course.courseStructure.course_uuid,
|
||||
course.courseStructure
|
||||
)
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
router.refresh()
|
||||
dispatchCourse({ type: 'setIsSaved' })
|
||||
}
|
||||
|
||||
const handleCourseOrder = (course_structure: any) => {
|
||||
const chapters = course_structure.chapters
|
||||
const chapter_order_by_ids = chapters.map((chapter: any) => {
|
||||
return {
|
||||
chapter_id: chapter.id,
|
||||
activities_order_by_ids: chapter.activities.map((activity: any) => {
|
||||
return {
|
||||
activity_id: activity.id,
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
dispatchCourse({
|
||||
type: 'setCourseOrder',
|
||||
payload: { chapter_order_by_ids: chapter_order_by_ids },
|
||||
})
|
||||
dispatchCourse({ type: 'setIsNotSaved' })
|
||||
}
|
||||
|
||||
const initOrderPayload = () => {
|
||||
if (course_structure && course_structure.chapters) {
|
||||
handleCourseOrder(course_structure)
|
||||
dispatchCourse({ type: 'setIsSaved' })
|
||||
}
|
||||
}
|
||||
|
||||
const changeOrderPayload = () => {
|
||||
if (course_structure && course_structure.chapters) {
|
||||
handleCourseOrder(course_structure)
|
||||
dispatchCourse({ type: 'setIsNotSaved' })
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (course_structure?.chapters) {
|
||||
initOrderPayload()
|
||||
}
|
||||
if (course_structure?.chapters && !saved) {
|
||||
changeOrderPayload()
|
||||
}
|
||||
}, [course_structure]) // This effect depends on the `course_structure` variable
|
||||
|
||||
return (
|
||||
<div className="flex space-x-4">
|
||||
{saved ? (
|
||||
<></>
|
||||
) : (
|
||||
<div className="text-gray-600 flex space-x-2 items-center antialiased">
|
||||
<Timer size={15} />
|
||||
<div>Unsaved changes</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={
|
||||
`px-4 py-2 rounded-lg drop-shadow-md cursor-pointer flex space-x-2 items-center font-bold antialiased transition-all ease-linear ` +
|
||||
(saved
|
||||
? 'bg-gray-600 text-white'
|
||||
: 'bg-black text-white border hover:bg-gray-900 ')
|
||||
}
|
||||
onClick={saveCourseState}
|
||||
>
|
||||
{saved ? <Check size={20} /> : <SaveAllIcon size={20} />}
|
||||
{saved ? <div className="">Saved</div> : <div className="">Save</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SaveState
|
||||
export default SaveState
|
||||
|
|
|
|||
|
|
@ -1,40 +1,44 @@
|
|||
import { updateProfile } from '@services/settings/profile';
|
||||
import { updateProfile } from '@services/settings/profile'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import { useSession } from '@components/Contexts/SessionContext';
|
||||
import { ArrowBigUpDash, Check, FileWarning, Info, UploadCloud } from 'lucide-react';
|
||||
import UserAvatar from '@components/Objects/UserAvatar';
|
||||
import { updateUserAvatar } from '@services/users/users';
|
||||
import { Formik, Form, Field } from 'formik'
|
||||
import { useSession } from '@components/Contexts/SessionContext'
|
||||
import {
|
||||
ArrowBigUpDash,
|
||||
Check,
|
||||
FileWarning,
|
||||
Info,
|
||||
UploadCloud,
|
||||
} from 'lucide-react'
|
||||
import UserAvatar from '@components/Objects/UserAvatar'
|
||||
import { updateUserAvatar } from '@services/users/users'
|
||||
|
||||
function UserEditGeneral() {
|
||||
const session = useSession() as any;
|
||||
const [localAvatar, setLocalAvatar] = React.useState(null) as any;
|
||||
const [isLoading, setIsLoading] = React.useState(false) as any;
|
||||
const [error, setError] = React.useState() as any;
|
||||
const [success, setSuccess] = React.useState('') as any;
|
||||
const session = useSession() as any
|
||||
const [localAvatar, setLocalAvatar] = React.useState(null) as any
|
||||
const [isLoading, setIsLoading] = React.useState(false) as any
|
||||
const [error, setError] = React.useState() as any
|
||||
const [success, setSuccess] = React.useState('') as any
|
||||
|
||||
const handleFileChange = async (event: any) => {
|
||||
const file = event.target.files[0];
|
||||
setLocalAvatar(file);
|
||||
setIsLoading(true);
|
||||
const file = event.target.files[0]
|
||||
setLocalAvatar(file)
|
||||
setIsLoading(true)
|
||||
const res = await updateUserAvatar(session.user.user_uuid, file)
|
||||
// wait for 1 second to show loading animation
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
await new Promise((r) => setTimeout(r, 1500))
|
||||
if (res.success === false) {
|
||||
setError(res.HTTPmessage);
|
||||
setError(res.HTTPmessage)
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
setError('');
|
||||
setSuccess('Avatar Updated');
|
||||
setIsLoading(false)
|
||||
setError('')
|
||||
setSuccess('Avatar Updated')
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
}
|
||||
, [session, session.user])
|
||||
|
||||
useEffect(() => {}, [session, session.user])
|
||||
|
||||
return (
|
||||
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'>
|
||||
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5">
|
||||
{session.user && (
|
||||
<Formik
|
||||
enableReinitialize
|
||||
|
|
@ -47,17 +51,14 @@ function UserEditGeneral() {
|
|||
}}
|
||||
onSubmit={(values, { setSubmitting }) => {
|
||||
setTimeout(() => {
|
||||
|
||||
setSubmitting(false);
|
||||
setSubmitting(false)
|
||||
updateProfile(values, session.user.id)
|
||||
}, 400);
|
||||
}, 400)
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<div className='flex space-x-8'>
|
||||
|
||||
<div className="flex space-x-8">
|
||||
<Form className="max-w-md">
|
||||
|
||||
<label className="block mb-2 font-bold" htmlFor="email">
|
||||
Email
|
||||
</label>
|
||||
|
|
@ -104,7 +105,6 @@ function UserEditGeneral() {
|
|||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
type="bio"
|
||||
name="bio"
|
||||
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
|
|
@ -114,63 +114,77 @@ function UserEditGeneral() {
|
|||
Submit
|
||||
</button>
|
||||
</Form>
|
||||
<div className='flex flex-col grow justify-center align-middle space-y-3'>
|
||||
<label className="flex mx-auto mb-2 font-bold " >
|
||||
Avatar
|
||||
</label>
|
||||
<div className="flex flex-col grow justify-center align-middle space-y-3">
|
||||
<label className="flex mx-auto mb-2 font-bold ">Avatar</label>
|
||||
{error && (
|
||||
<div className="flex justify-center mx-auto bg-red-200 rounded-md text-red-950 space-x-1 px-4 items-center p-2 transition-all shadow-sm">
|
||||
<FileWarning size={16} className='mr-2' />
|
||||
<div className="text-sm font-semibold first-letter:uppercase">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="flex justify-center mx-auto bg-green-200 rounded-md text-green-950 space-x-1 px-4 items-center p-2 transition-all shadow-sm">
|
||||
<Check size={16} className='mr-2' />
|
||||
<div className="text-sm font-semibold first-letter:uppercase">{success}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-center mx-auto bg-red-200 rounded-md text-red-950 space-x-1 px-4 items-center p-2 transition-all shadow-sm">
|
||||
<FileWarning size={16} className="mr-2" />
|
||||
<div className="text-sm font-semibold first-letter:uppercase">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="flex justify-center mx-auto bg-green-200 rounded-md text-green-950 space-x-1 px-4 items-center p-2 transition-all shadow-sm">
|
||||
<Check size={16} className="mr-2" />
|
||||
<div className="text-sm font-semibold first-letter:uppercase">
|
||||
{success}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col space-y-3">
|
||||
|
||||
<div className='w-auto bg-gray-50 rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow mx-20'>
|
||||
|
||||
<div className='flex flex-col justify-center items-center mt-10'>
|
||||
|
||||
<div className="w-auto bg-gray-50 rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow mx-20">
|
||||
<div className="flex flex-col justify-center items-center mt-10">
|
||||
{localAvatar ? (
|
||||
<UserAvatar border='border-8' width={100} avatar_url={URL.createObjectURL(localAvatar)} />
|
||||
|
||||
<UserAvatar
|
||||
border="border-8"
|
||||
width={100}
|
||||
avatar_url={URL.createObjectURL(localAvatar)}
|
||||
/>
|
||||
) : (
|
||||
<UserAvatar border='border-8' width={100} />
|
||||
<UserAvatar border="border-8" width={100} />
|
||||
)}
|
||||
</div>
|
||||
{isLoading ? (<div className='flex justify-center items-center'>
|
||||
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
|
||||
<div
|
||||
className='font-bold animate-pulse antialiased items-center bg-green-200 text-gray text-sm rounded-md px-4 py-2 mt-4 flex'
|
||||
>
|
||||
<ArrowBigUpDash size={16} className='mr-2' />
|
||||
<span>Uploading</span>
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center">
|
||||
<input
|
||||
type="file"
|
||||
id="fileInput"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<div className="font-bold animate-pulse antialiased items-center bg-green-200 text-gray text-sm rounded-md px-4 py-2 mt-4 flex">
|
||||
<ArrowBigUpDash size={16} className="mr-2" />
|
||||
<span>Uploading</span>
|
||||
</div>
|
||||
</div>) : (
|
||||
<div className='flex justify-center items-center'>
|
||||
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
|
||||
<button
|
||||
className='font-bold antialiased items-center text-gray text-sm rounded-md px-4 py-2 mt-4 flex'
|
||||
onClick={() => document.getElementById('fileInput')?.click()}
|
||||
>
|
||||
<UploadCloud size={16} className='mr-2' />
|
||||
<span>Change Thumbnail</span>
|
||||
</button>
|
||||
</div> )}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-center items-center">
|
||||
<input
|
||||
type="file"
|
||||
id="fileInput"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<button
|
||||
className="font-bold antialiased items-center text-gray text-sm rounded-md px-4 py-2 mt-4 flex"
|
||||
onClick={() =>
|
||||
document.getElementById('fileInput')?.click()
|
||||
}
|
||||
>
|
||||
<UploadCloud size={16} className="mr-2" />
|
||||
<span>Change Thumbnail</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex text-xs space-x-2 items-center text-gray-500 justify-center'>
|
||||
<Info size={13} /><p>Recommended size 100x100</p>
|
||||
<div className="flex text-xs space-x-2 items-center text-gray-500 justify-center">
|
||||
<Info size={13} />
|
||||
<p>Recommended size 100x100</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
)}
|
||||
</Formik>
|
||||
)}
|
||||
|
|
@ -178,4 +192,4 @@ function UserEditGeneral() {
|
|||
)
|
||||
}
|
||||
|
||||
export default UserEditGeneral
|
||||
export default UserEditGeneral
|
||||
|
|
|
|||
|
|
@ -1,66 +1,62 @@
|
|||
import { useSession } from '@components/Contexts/SessionContext';
|
||||
import { updatePassword } from '@services/settings/password';
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import { useSession } from '@components/Contexts/SessionContext'
|
||||
import { updatePassword } from '@services/settings/password'
|
||||
import { Formik, Form, Field } from 'formik'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
function UserEditPassword() {
|
||||
const session = useSession() as any;
|
||||
const session = useSession() as any
|
||||
|
||||
const updatePasswordUI = async (values: any) => {
|
||||
let user_id = session.user.user_id;
|
||||
await updatePassword(user_id, values)
|
||||
}
|
||||
const updatePasswordUI = async (values: any) => {
|
||||
let user_id = session.user.user_id
|
||||
await updatePassword(user_id, values)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
}
|
||||
, [session])
|
||||
useEffect(() => {}, [session])
|
||||
|
||||
return (
|
||||
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5">
|
||||
<Formik
|
||||
initialValues={{ old_password: '', new_password: '' }}
|
||||
enableReinitialize
|
||||
onSubmit={(values, { setSubmitting }) => {
|
||||
setTimeout(() => {
|
||||
setSubmitting(false)
|
||||
updatePasswordUI(values)
|
||||
}, 400)
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form className="max-w-md">
|
||||
<label className="block mb-2 font-bold" htmlFor="old_password">
|
||||
Old Password
|
||||
</label>
|
||||
<Field
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
type="password"
|
||||
name="old_password"
|
||||
/>
|
||||
|
||||
return (
|
||||
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'>
|
||||
<Formik
|
||||
initialValues={{ old_password: '', new_password: '' }}
|
||||
enableReinitialize
|
||||
onSubmit={(values, { setSubmitting }) => {
|
||||
setTimeout(() => {
|
||||
setSubmitting(false);
|
||||
updatePasswordUI(values)
|
||||
}, 400);
|
||||
}}
|
||||
<label className="block mb-2 font-bold" htmlFor="new_password">
|
||||
New Password
|
||||
</label>
|
||||
<Field
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
type="password"
|
||||
name="new_password"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="px-6 py-3 text-white bg-black rounded-lg shadow-md hover:bg-black focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form className="max-w-md">
|
||||
<label className="block mb-2 font-bold" htmlFor="old_password">
|
||||
Old Password
|
||||
</label>
|
||||
<Field
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
type="password"
|
||||
name="old_password"
|
||||
/>
|
||||
|
||||
<label className="block mb-2 font-bold" htmlFor="new_password">
|
||||
New Password
|
||||
</label>
|
||||
<Field
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
type="password"
|
||||
name="new_password"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="px-6 py-3 text-white bg-black rounded-lg shadow-md hover:bg-black focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</Form>
|
||||
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
)
|
||||
Submit
|
||||
</button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserEditPassword
|
||||
export default UserEditPassword
|
||||
|
|
|
|||
|
|
@ -1,175 +1,236 @@
|
|||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
|
||||
import { getAPIUrl, getUriWithOrg } from '@services/config/config';
|
||||
import { swrFetcher } from '@services/utils/ts/requests';
|
||||
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
|
||||
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
|
||||
import { swrFetcher } from '@services/utils/ts/requests'
|
||||
import { Globe, Shield, X } from 'lucide-react'
|
||||
import Link from 'next/link';
|
||||
import Link from 'next/link'
|
||||
import React, { useEffect } from 'react'
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import dayjs from 'dayjs';
|
||||
import { changeSignupMechanism, createInviteCode, deleteInviteCode } from '@services/organizations/invites';
|
||||
import Toast from '@components/StyledElements/Toast/Toast';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import useSWR, { mutate } from 'swr'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
changeSignupMechanism,
|
||||
createInviteCode,
|
||||
deleteInviteCode,
|
||||
} from '@services/organizations/invites'
|
||||
import Toast from '@components/StyledElements/Toast/Toast'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
function OrgAccess() {
|
||||
const org = useOrg() as any;
|
||||
const { data: invites } = useSWR(org ? `${getAPIUrl()}orgs/${org?.id}/invites` : null, swrFetcher);
|
||||
const [isLoading, setIsLoading] = React.useState(false)
|
||||
const [joinMethod, setJoinMethod] = React.useState('closed')
|
||||
const router = useRouter()
|
||||
const org = useOrg() as any
|
||||
const { data: invites } = useSWR(
|
||||
org ? `${getAPIUrl()}orgs/${org?.id}/invites` : null,
|
||||
swrFetcher
|
||||
)
|
||||
const [isLoading, setIsLoading] = React.useState(false)
|
||||
const [joinMethod, setJoinMethod] = React.useState('closed')
|
||||
const router = useRouter()
|
||||
|
||||
async function getOrgJoinMethod() {
|
||||
if (org) {
|
||||
if (org.config.config.GeneralConfig.users.signup_mechanism == 'open') {
|
||||
setJoinMethod('open')
|
||||
}
|
||||
else {
|
||||
setJoinMethod('inviteOnly')
|
||||
}
|
||||
}
|
||||
async function getOrgJoinMethod() {
|
||||
if (org) {
|
||||
if (org.config.config.GeneralConfig.users.signup_mechanism == 'open') {
|
||||
setJoinMethod('open')
|
||||
} else {
|
||||
setJoinMethod('inviteOnly')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createInvite() {
|
||||
let res = await createInviteCode(org.id)
|
||||
if (res.status == 200) {
|
||||
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
|
||||
}
|
||||
else {
|
||||
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||
}
|
||||
|
||||
async function createInvite() {
|
||||
let res = await createInviteCode(org.id)
|
||||
if (res.status == 200) {
|
||||
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
|
||||
} else {
|
||||
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteInvite(invite: any) {
|
||||
let res = await deleteInviteCode(org.id, invite.invite_code_uuid)
|
||||
if (res.status == 200) {
|
||||
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
|
||||
}
|
||||
else {
|
||||
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||
}
|
||||
|
||||
async function deleteInvite(invite: any) {
|
||||
let res = await deleteInviteCode(org.id, invite.invite_code_uuid)
|
||||
if (res.status == 200) {
|
||||
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
|
||||
} else {
|
||||
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||
}
|
||||
}
|
||||
|
||||
async function changeJoinMethod(method: 'open' | 'inviteOnly') {
|
||||
let res = await changeSignupMechanism(org.id, method)
|
||||
if (res.status == 200) {
|
||||
router.refresh()
|
||||
mutate(`${getAPIUrl()}orgs/slug/${org?.slug}`)
|
||||
}
|
||||
else {
|
||||
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||
}
|
||||
async function changeJoinMethod(method: 'open' | 'inviteOnly') {
|
||||
let res = await changeSignupMechanism(org.id, method)
|
||||
if (res.status == 200) {
|
||||
router.refresh()
|
||||
mutate(`${getAPIUrl()}orgs/slug/${org?.slug}`)
|
||||
} else {
|
||||
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (invites && org) {
|
||||
getOrgJoinMethod()
|
||||
setIsLoading(false)
|
||||
}
|
||||
useEffect(() => {
|
||||
if (invites && org) {
|
||||
getOrgJoinMethod()
|
||||
setIsLoading(false)
|
||||
}
|
||||
, [org, invites])
|
||||
|
||||
return (
|
||||
}, [org, invites])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toast></Toast>
|
||||
{!isLoading ? (
|
||||
<>
|
||||
<Toast></Toast>
|
||||
{!isLoading ? (<>
|
||||
<div className="h-6"></div>
|
||||
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4 anit '>
|
||||
<div className='flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3 '>
|
||||
<h1 className='font-bold text-xl text-gray-800'>Join method</h1>
|
||||
<h2 className='text-gray-500 text-md'> Choose how users can join your organization </h2>
|
||||
<div className="h-6"></div>
|
||||
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4 anit ">
|
||||
<div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3 ">
|
||||
<h1 className="font-bold text-xl text-gray-800">Join method</h1>
|
||||
<h2 className="text-gray-500 text-md">
|
||||
{' '}
|
||||
Choose how users can join your organization{' '}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex space-x-2 mx-auto">
|
||||
<ConfirmationModal
|
||||
confirmationButtonText="Change to open "
|
||||
confirmationMessage="Are you sure you want to change the signup mechanism to open ? This will allow users to join your organization freely."
|
||||
dialogTitle={'Change to open ?'}
|
||||
dialogTrigger={
|
||||
<div className="w-full h-[160px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all">
|
||||
{joinMethod == 'open' ? (
|
||||
<div className="bg-green-200 text-green-600 font-bold w-fit my-3 mx-3 absolute text-sm px-3 py-1 rounded-lg">
|
||||
Active
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-col space-y-1 justify-center items-center h-full">
|
||||
<Globe className="text-slate-400" size={40}></Globe>
|
||||
<div className="text-2xl text-slate-700 font-bold">
|
||||
Open
|
||||
</div>
|
||||
<div className="text-gray-400 text-center">
|
||||
Users can join freely from the signup page
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex space-x-2 mx-auto'>
|
||||
<ConfirmationModal
|
||||
confirmationButtonText='Change to open '
|
||||
confirmationMessage='Are you sure you want to change the signup mechanism to open ? This will allow users to join your organization freely.'
|
||||
dialogTitle={'Change to open ?'}
|
||||
</div>
|
||||
}
|
||||
functionToExecute={() => {
|
||||
changeJoinMethod('open')
|
||||
}}
|
||||
status="info"
|
||||
></ConfirmationModal>
|
||||
<ConfirmationModal
|
||||
confirmationButtonText="Change to closed "
|
||||
confirmationMessage="Are you sure you want to change the signup mechanism to closed ? This will allow users to join your organization only by invitation."
|
||||
dialogTitle={'Change to closed ?'}
|
||||
dialogTrigger={
|
||||
<div className="w-full h-[160px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all">
|
||||
{joinMethod == 'inviteOnly' ? (
|
||||
<div className="bg-green-200 text-green-600 font-bold w-fit my-3 mx-3 absolute text-sm px-3 py-1 rounded-lg">
|
||||
Active
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-col space-y-1 justify-center items-center h-full">
|
||||
<Shield className="text-slate-400" size={40}></Shield>
|
||||
<div className="text-2xl text-slate-700 font-bold">
|
||||
Closed
|
||||
</div>
|
||||
<div className="text-gray-400 text-center">
|
||||
Users can join only by invitation
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
functionToExecute={() => {
|
||||
changeJoinMethod('inviteOnly')
|
||||
}}
|
||||
status="info"
|
||||
></ConfirmationModal>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
joinMethod == 'open'
|
||||
? 'opacity-20 pointer-events-none'
|
||||
: 'pointer-events-auto'
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mt-3 mb-3 ">
|
||||
<h1 className="font-bold text-xl text-gray-800">
|
||||
Invite codes
|
||||
</h1>
|
||||
<h2 className="text-gray-500 text-md">
|
||||
Invite codes can be copied and used to join your organization{' '}
|
||||
</h2>
|
||||
</div>
|
||||
<table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden">
|
||||
<thead className="bg-gray-100 text-gray-500 rounded-xl uppercase">
|
||||
<tr className="font-bolder text-sm">
|
||||
<th className="py-3 px-4">Code</th>
|
||||
<th className="py-3 px-4">Signup link</th>
|
||||
<th className="py-3 px-4">Expiration date</th>
|
||||
<th className="py-3 px-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<>
|
||||
<tbody className="mt-5 bg-white rounded-md">
|
||||
{invites?.map((invite: any) => (
|
||||
<tr
|
||||
key={invite.invite_code_uuid}
|
||||
className="border-b border-gray-100 text-sm"
|
||||
>
|
||||
<td className="py-3 px-4">{invite.invite_code}</td>
|
||||
<td className="py-3 px-4 ">
|
||||
<Link
|
||||
className="outline bg-gray-50 text-gray-600 px-2 py-1 rounded-md outline-gray-300 outline-dashed outline-1"
|
||||
target="_blank"
|
||||
href={getUriWithOrg(
|
||||
org?.slug,
|
||||
`/signup?inviteCode=${invite.invite_code}`
|
||||
)}
|
||||
>
|
||||
{getUriWithOrg(
|
||||
org?.slug,
|
||||
`/signup?inviteCode=${invite.invite_code}`
|
||||
)}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
{dayjs(invite.expiration_date)
|
||||
.add(1, 'year')
|
||||
.format('DD/MM/YYYY')}{' '}
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<ConfirmationModal
|
||||
confirmationButtonText="Delete Code"
|
||||
confirmationMessage="Are you sure you want remove this invite code ?"
|
||||
dialogTitle={'Delete code ?'}
|
||||
dialogTrigger={
|
||||
<div className='w-full h-[160px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all'>
|
||||
{joinMethod == 'open' ? <div className='bg-green-200 text-green-600 font-bold w-fit my-3 mx-3 absolute text-sm px-3 py-1 rounded-lg'>Active</div> : null}
|
||||
<div className='flex flex-col space-y-1 justify-center items-center h-full'>
|
||||
<Globe className='text-slate-400' size={40}></Globe>
|
||||
<div className='text-2xl text-slate-700 font-bold'>Open</div>
|
||||
<div className='text-gray-400 text-center'>Users can join freely from the signup page</div>
|
||||
</div>
|
||||
</div>}
|
||||
functionToExecute={() => { changeJoinMethod('open') }}
|
||||
status='info'
|
||||
></ConfirmationModal>
|
||||
<ConfirmationModal
|
||||
confirmationButtonText='Change to closed '
|
||||
confirmationMessage='Are you sure you want to change the signup mechanism to closed ? This will allow users to join your organization only by invitation.'
|
||||
dialogTitle={'Change to closed ?'}
|
||||
dialogTrigger={
|
||||
<div className='w-full h-[160px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all'>
|
||||
{joinMethod == 'inviteOnly' ? <div className='bg-green-200 text-green-600 font-bold w-fit my-3 mx-3 absolute text-sm px-3 py-1 rounded-lg'>Active</div> : null}
|
||||
<div className='flex flex-col space-y-1 justify-center items-center h-full'>
|
||||
<Shield className='text-slate-400' size={40}></Shield>
|
||||
<div className='text-2xl text-slate-700 font-bold'>Closed</div>
|
||||
<div className='text-gray-400 text-center'>Users can join only by invitation</div>
|
||||
</div>
|
||||
</div>}
|
||||
functionToExecute={() => { changeJoinMethod('inviteOnly') }}
|
||||
status='info'
|
||||
></ConfirmationModal>
|
||||
|
||||
</div>
|
||||
<div className={joinMethod == 'open' ? 'opacity-20 pointer-events-none' : 'pointer-events-auto'}>
|
||||
<div className='flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mt-3 mb-3 '>
|
||||
<h1 className='font-bold text-xl text-gray-800'>Invite codes</h1>
|
||||
<h2 className='text-gray-500 text-md'>Invite codes can be copied and used to join your organization </h2>
|
||||
</div>
|
||||
<table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden">
|
||||
<thead className='bg-gray-100 text-gray-500 rounded-xl uppercase'>
|
||||
<tr className='font-bolder text-sm'>
|
||||
<th className='py-3 px-4'>Code</th>
|
||||
<th className='py-3 px-4'>Signup link</th>
|
||||
<th className='py-3 px-4'>Expiration date</th>
|
||||
<th className='py-3 px-4'>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<>
|
||||
<tbody className='mt-5 bg-white rounded-md' >
|
||||
{invites?.map((invite: any) => (
|
||||
<tr key={invite.invite_code_uuid} className='border-b border-gray-100 text-sm'>
|
||||
<td className='py-3 px-4'>{invite.invite_code}</td>
|
||||
<td className='py-3 px-4 '>
|
||||
<Link className='outline bg-gray-50 text-gray-600 px-2 py-1 rounded-md outline-gray-300 outline-dashed outline-1' target='_blank' href={getUriWithOrg(org?.slug, `/signup?inviteCode=${invite.invite_code}`)}>
|
||||
{getUriWithOrg(org?.slug, `/signup?inviteCode=${invite.invite_code}`)}
|
||||
</Link>
|
||||
</td>
|
||||
<td className='py-3 px-4'>{dayjs(invite.expiration_date).add(1, 'year').format('DD/MM/YYYY')} </td>
|
||||
<td className='py-3 px-4'>
|
||||
<ConfirmationModal
|
||||
confirmationButtonText='Delete Code'
|
||||
confirmationMessage='Are you sure you want remove this invite code ?'
|
||||
dialogTitle={'Delete code ?'}
|
||||
dialogTrigger={
|
||||
<button className='mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100'>
|
||||
<X className='w-4 h-4' />
|
||||
<span> Delete code</span>
|
||||
</button>}
|
||||
functionToExecute={() => { deleteInvite(invite) }}
|
||||
status='warning'
|
||||
></ConfirmationModal>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</>
|
||||
</table>
|
||||
<button onClick={() => createInvite()} className='mt-3 mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-sm text-green-100'>
|
||||
<Shield className='w-4 h-4' />
|
||||
<span> Create invite code</span>
|
||||
</button>
|
||||
</div>
|
||||
</div></>) : <PageLoading />}
|
||||
<button className="mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100">
|
||||
<X className="w-4 h-4" />
|
||||
<span> Delete code</span>
|
||||
</button>
|
||||
}
|
||||
functionToExecute={() => {
|
||||
deleteInvite(invite)
|
||||
}}
|
||||
status="warning"
|
||||
></ConfirmationModal>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</>
|
||||
</table>
|
||||
<button
|
||||
onClick={() => createInvite()}
|
||||
className="mt-3 mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-sm text-green-100"
|
||||
>
|
||||
<Shield className="w-4 h-4" />
|
||||
<span> Create invite code</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<PageLoading />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrgAccess
|
||||
export default OrgAccess
|
||||
|
|
|
|||
|
|
@ -1,120 +1,144 @@
|
|||
import { useOrg } from '@components/Contexts/OrgContext';
|
||||
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
||||
import RolesUpdate from '@components/Objects/Modals/Dash/OrgUsers/RolesUpdate';
|
||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
|
||||
import Modal from '@components/StyledElements/Modal/Modal';
|
||||
import Toast from '@components/StyledElements/Toast/Toast';
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
import { removeUserFromOrg } from '@services/organizations/orgs';
|
||||
import { swrFetcher } from '@services/utils/ts/requests';
|
||||
import { KeyRound, LogOut } from 'lucide-react';
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||
import RolesUpdate from '@components/Objects/Modals/Dash/OrgUsers/RolesUpdate'
|
||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
|
||||
import Modal from '@components/StyledElements/Modal/Modal'
|
||||
import Toast from '@components/StyledElements/Toast/Toast'
|
||||
import { getAPIUrl } from '@services/config/config'
|
||||
import { removeUserFromOrg } from '@services/organizations/orgs'
|
||||
import { swrFetcher } from '@services/utils/ts/requests'
|
||||
import { KeyRound, LogOut } from 'lucide-react'
|
||||
import React, { useEffect } from 'react'
|
||||
import toast from 'react-hot-toast';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import toast from 'react-hot-toast'
|
||||
import useSWR, { mutate } from 'swr'
|
||||
|
||||
function OrgUsers() {
|
||||
const org = useOrg() as any;
|
||||
const { data: orgUsers } = useSWR(org ? `${getAPIUrl()}orgs/${org?.id}/users` : null, swrFetcher);
|
||||
const [rolesModal, setRolesModal] = React.useState(false);
|
||||
const [selectedUser, setSelectedUser] = React.useState(null) as any;
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const org = useOrg() as any
|
||||
const { data: orgUsers } = useSWR(
|
||||
org ? `${getAPIUrl()}orgs/${org?.id}/users` : null,
|
||||
swrFetcher
|
||||
)
|
||||
const [rolesModal, setRolesModal] = React.useState(false)
|
||||
const [selectedUser, setSelectedUser] = React.useState(null) as any
|
||||
const [isLoading, setIsLoading] = React.useState(true)
|
||||
|
||||
const handleRolesModal = (user_uuid: any) => {
|
||||
setSelectedUser(user_uuid);
|
||||
setRolesModal(!rolesModal);
|
||||
const handleRolesModal = (user_uuid: any) => {
|
||||
setSelectedUser(user_uuid)
|
||||
setRolesModal(!rolesModal)
|
||||
}
|
||||
|
||||
const handleRemoveUser = async (user_id: any) => {
|
||||
const res = await removeUserFromOrg(org.id, user_id)
|
||||
if (res.status === 200) {
|
||||
await mutate(`${getAPIUrl()}orgs/${org.id}/users`)
|
||||
} else {
|
||||
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveUser = async (user_id: any) => {
|
||||
const res = await removeUserFromOrg(org.id, user_id);
|
||||
if (res.status === 200) {
|
||||
await mutate(`${getAPIUrl()}orgs/${org.id}/users`);
|
||||
}
|
||||
else {
|
||||
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||
}
|
||||
useEffect(() => {
|
||||
if (orgUsers) {
|
||||
setIsLoading(false)
|
||||
console.log(orgUsers)
|
||||
}
|
||||
}, [org, orgUsers])
|
||||
|
||||
useEffect(() => {
|
||||
if (orgUsers) {
|
||||
setIsLoading(false)
|
||||
console.log(orgUsers)
|
||||
}
|
||||
}, [org, orgUsers])
|
||||
|
||||
return (
|
||||
return (
|
||||
<div>
|
||||
{isLoading ? (
|
||||
<div>
|
||||
{isLoading ? <div><PageLoading /></div> :
|
||||
|
||||
<>
|
||||
<Toast></Toast>
|
||||
<div className="h-6"></div>
|
||||
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4 '>
|
||||
<div className='flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3 '>
|
||||
<h1 className='font-bold text-xl text-gray-800'>Active users</h1>
|
||||
<h2 className='text-gray-500 text-md'> Manage your organization users, assign roles and permissions </h2>
|
||||
</div>
|
||||
<table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden">
|
||||
<thead className='bg-gray-100 text-gray-500 rounded-xl uppercase'>
|
||||
<tr className='font-bolder text-sm'>
|
||||
<th className='py-3 px-4'>User</th>
|
||||
<th className='py-3 px-4'>Role</th>
|
||||
<th className='py-3 px-4'>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<>
|
||||
<tbody className='mt-5 bg-white rounded-md' >
|
||||
{orgUsers?.map((user: any) => (
|
||||
<tr key={user.user.id} className='border-b border-gray-200 border-dashed'>
|
||||
<td className='py-3 px-4 flex space-x-2 items-center'>
|
||||
<span>{user.user.first_name + ' ' + user.user.last_name}</span>
|
||||
<span className='text-xs bg-neutral-100 p-1 px-2 rounded-full text-neutral-400 font-semibold'>@{user.user.username}</span>
|
||||
</td>
|
||||
<td className='py-3 px-4'>{user.role.name}</td>
|
||||
<td className='py-3 px-4 flex space-x-2 items-end'>
|
||||
<Modal
|
||||
isDialogOpen={rolesModal && selectedUser === user.user.user_uuid}
|
||||
onOpenChange={() => handleRolesModal(user.user.user_uuid)}
|
||||
minHeight="no-min"
|
||||
dialogContent={
|
||||
<RolesUpdate
|
||||
alreadyAssignedRole={user.role.role_uuid}
|
||||
setRolesModal={setRolesModal}
|
||||
user={user} />
|
||||
}
|
||||
dialogTitle="Update Role"
|
||||
dialogDescription={"Update @" + user.user.username + "'s role"}
|
||||
dialogTrigger={
|
||||
<button className='flex space-x-2 hover:cursor-pointer p-1 px-3 bg-yellow-700 rounded-md font-bold items-center text-sm text-yellow-100'>
|
||||
<KeyRound className='w-4 h-4' />
|
||||
<span> Edit Role</span>
|
||||
</button>}
|
||||
/>
|
||||
|
||||
|
||||
<ConfirmationModal
|
||||
confirmationButtonText='Remove User'
|
||||
confirmationMessage='Are you sure you want remove this user from the organization?'
|
||||
dialogTitle={'Delete ' + user.user.username + ' ?'}
|
||||
dialogTrigger={
|
||||
<button className='mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100'>
|
||||
<LogOut className='w-4 h-4' />
|
||||
<span> Remove from organization</span>
|
||||
</button>}
|
||||
functionToExecute={() => { handleRemoveUser(user.user.id) }}
|
||||
status='warning'
|
||||
></ConfirmationModal>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
))}
|
||||
</tbody>
|
||||
</>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
<PageLoading />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<Toast></Toast>
|
||||
<div className="h-6"></div>
|
||||
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4 ">
|
||||
<div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3 ">
|
||||
<h1 className="font-bold text-xl text-gray-800">Active users</h1>
|
||||
<h2 className="text-gray-500 text-md">
|
||||
{' '}
|
||||
Manage your organization users, assign roles and permissions{' '}
|
||||
</h2>
|
||||
</div>
|
||||
<table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden">
|
||||
<thead className="bg-gray-100 text-gray-500 rounded-xl uppercase">
|
||||
<tr className="font-bolder text-sm">
|
||||
<th className="py-3 px-4">User</th>
|
||||
<th className="py-3 px-4">Role</th>
|
||||
<th className="py-3 px-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<>
|
||||
<tbody className="mt-5 bg-white rounded-md">
|
||||
{orgUsers?.map((user: any) => (
|
||||
<tr
|
||||
key={user.user.id}
|
||||
className="border-b border-gray-200 border-dashed"
|
||||
>
|
||||
<td className="py-3 px-4 flex space-x-2 items-center">
|
||||
<span>
|
||||
{user.user.first_name + ' ' + user.user.last_name}
|
||||
</span>
|
||||
<span className="text-xs bg-neutral-100 p-1 px-2 rounded-full text-neutral-400 font-semibold">
|
||||
@{user.user.username}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">{user.role.name}</td>
|
||||
<td className="py-3 px-4 flex space-x-2 items-end">
|
||||
<Modal
|
||||
isDialogOpen={
|
||||
rolesModal && selectedUser === user.user.user_uuid
|
||||
}
|
||||
onOpenChange={() =>
|
||||
handleRolesModal(user.user.user_uuid)
|
||||
}
|
||||
minHeight="no-min"
|
||||
dialogContent={
|
||||
<RolesUpdate
|
||||
alreadyAssignedRole={user.role.role_uuid}
|
||||
setRolesModal={setRolesModal}
|
||||
user={user}
|
||||
/>
|
||||
}
|
||||
dialogTitle="Update Role"
|
||||
dialogDescription={
|
||||
'Update @' + user.user.username + "'s role"
|
||||
}
|
||||
dialogTrigger={
|
||||
<button className="flex space-x-2 hover:cursor-pointer p-1 px-3 bg-yellow-700 rounded-md font-bold items-center text-sm text-yellow-100">
|
||||
<KeyRound className="w-4 h-4" />
|
||||
<span> Edit Role</span>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
confirmationButtonText="Remove User"
|
||||
confirmationMessage="Are you sure you want remove this user from the organization?"
|
||||
dialogTitle={'Delete ' + user.user.username + ' ?'}
|
||||
dialogTrigger={
|
||||
<button className="mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100">
|
||||
<LogOut className="w-4 h-4" />
|
||||
<span> Remove from organization</span>
|
||||
</button>
|
||||
}
|
||||
functionToExecute={() => {
|
||||
handleRemoveUser(user.user.id)
|
||||
}}
|
||||
status="warning"
|
||||
></ConfirmationModal>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrgUsers
|
||||
export default OrgUsers
|
||||
|
|
|
|||
|
|
@ -1,327 +1,477 @@
|
|||
import { useSession } from '@components/Contexts/SessionContext'
|
||||
import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai';
|
||||
import { AlertTriangle, BadgeInfo, NotebookTabs } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
sendActivityAIChatMessage,
|
||||
startActivityAIChatSession,
|
||||
} from '@services/ai/ai'
|
||||
import { AlertTriangle, BadgeInfo, NotebookTabs } from 'lucide-react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { FlaskConical, MessageCircle, X } from 'lucide-react'
|
||||
import Image from 'next/image';
|
||||
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
||||
import learnhouseAI_logo_black from "public/learnhouse_ai_black_logo.png";
|
||||
import Image from 'next/image'
|
||||
import learnhouseAI_icon from 'public/learnhouse_ai_simple.png'
|
||||
import learnhouseAI_logo_black from 'public/learnhouse_ai_black_logo.png'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { AIChatBotStateTypes, useAIChatBot, useAIChatBotDispatch } from '@components/Contexts/AI/AIChatBotContext';
|
||||
import useGetAIFeatures from '../../../AI/Hooks/useGetAIFeatures';
|
||||
import UserAvatar from '@components/Objects/UserAvatar';
|
||||
|
||||
import {
|
||||
AIChatBotStateTypes,
|
||||
useAIChatBot,
|
||||
useAIChatBotDispatch,
|
||||
} from '@components/Contexts/AI/AIChatBotContext'
|
||||
import useGetAIFeatures from '../../../AI/Hooks/useGetAIFeatures'
|
||||
import UserAvatar from '@components/Objects/UserAvatar'
|
||||
|
||||
type AIActivityAskProps = {
|
||||
activity: any;
|
||||
activity: any
|
||||
}
|
||||
|
||||
|
||||
function AIActivityAsk(props: AIActivityAskProps) {
|
||||
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'activity_ask' });
|
||||
const [isButtonAvailable, setIsButtonAvailable] = React.useState(false);
|
||||
const dispatchAIChatBot = useAIChatBotDispatch() as any;
|
||||
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'activity_ask' })
|
||||
const [isButtonAvailable, setIsButtonAvailable] = React.useState(false)
|
||||
const dispatchAIChatBot = useAIChatBotDispatch() as any
|
||||
|
||||
useEffect(() => {
|
||||
if (is_ai_feature_enabled) {
|
||||
setIsButtonAvailable(true);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (is_ai_feature_enabled) {
|
||||
setIsButtonAvailable(true)
|
||||
}
|
||||
, [is_ai_feature_enabled]);
|
||||
}, [is_ai_feature_enabled])
|
||||
|
||||
return (
|
||||
<>
|
||||
{isButtonAvailable && (
|
||||
<div >
|
||||
<ActivityChatMessageBox activity={props.activity} />
|
||||
<div
|
||||
onClick={() => dispatchAIChatBot({ type: 'setIsModalOpen' })}
|
||||
style={{
|
||||
background: 'conic-gradient(from 32deg at 53.75% 50%, rgb(35, 40, 93) 4deg, rgba(20, 0, 52, 0.95) 59deg, rgba(164, 45, 238, 0.88) 281deg)',
|
||||
}}
|
||||
className="rounded-full px-5 drop-shadow-md flex items-center space-x-1.5 p-2.5 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out hover:scale-105">
|
||||
{" "}
|
||||
<i>
|
||||
<Image className='outline outline-1 outline-neutral-200/20 rounded-md' width={20} src={learnhouseAI_icon} alt="" />
|
||||
</i>{" "}
|
||||
<i className="not-italic text-xs font-bold">Ask AI</i>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
)
|
||||
return (
|
||||
<>
|
||||
{isButtonAvailable && (
|
||||
<div>
|
||||
<ActivityChatMessageBox activity={props.activity} />
|
||||
<div
|
||||
onClick={() => dispatchAIChatBot({ type: 'setIsModalOpen' })}
|
||||
style={{
|
||||
background:
|
||||
'conic-gradient(from 32deg at 53.75% 50%, rgb(35, 40, 93) 4deg, rgba(20, 0, 52, 0.95) 59deg, rgba(164, 45, 238, 0.88) 281deg)',
|
||||
}}
|
||||
className="rounded-full px-5 drop-shadow-md flex items-center space-x-1.5 p-2.5 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out hover:scale-105"
|
||||
>
|
||||
{' '}
|
||||
<i>
|
||||
<Image
|
||||
className="outline outline-1 outline-neutral-200/20 rounded-md"
|
||||
width={20}
|
||||
src={learnhouseAI_icon}
|
||||
alt=""
|
||||
/>
|
||||
</i>{' '}
|
||||
<i className="not-italic text-xs font-bold">Ask AI</i>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export type AIMessage = {
|
||||
sender: string;
|
||||
message: any;
|
||||
type: 'ai' | 'user';
|
||||
sender: string
|
||||
message: any
|
||||
type: 'ai' | 'user'
|
||||
}
|
||||
|
||||
type ActivityChatMessageBoxProps = {
|
||||
activity: any;
|
||||
activity: any
|
||||
}
|
||||
|
||||
function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) {
|
||||
const session = useSession() as any;
|
||||
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes;
|
||||
const dispatchAIChatBot = useAIChatBotDispatch() as any;
|
||||
const session = useSession() as any
|
||||
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes
|
||||
const dispatchAIChatBot = useAIChatBotDispatch() as any
|
||||
|
||||
// TODO : come up with a better way to handle this
|
||||
const inputClass = aiChatBotState.isWaitingForResponse
|
||||
? 'ring-1 ring-inset ring-white/10 bg-gray-950/40 w-full rounded-lg outline-none px-4 py-2 text-white text-sm placeholder:text-white/30 opacity-30 '
|
||||
: 'ring-1 ring-inset ring-white/10 bg-gray-950/40 w-full rounded-lg outline-none px-4 py-2 text-white text-sm placeholder:text-white/30';
|
||||
// TODO : come up with a better way to handle this
|
||||
const inputClass = aiChatBotState.isWaitingForResponse
|
||||
? 'ring-1 ring-inset ring-white/10 bg-gray-950/40 w-full rounded-lg outline-none px-4 py-2 text-white text-sm placeholder:text-white/30 opacity-30 '
|
||||
: 'ring-1 ring-inset ring-white/10 bg-gray-950/40 w-full rounded-lg outline-none px-4 py-2 text-white text-sm placeholder:text-white/30'
|
||||
|
||||
useEffect(() => {
|
||||
if (aiChatBotState.isModalOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = 'unset';
|
||||
}
|
||||
}, [aiChatBotState.isModalOpen]);
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (event.key === 'Enter') {
|
||||
// Perform the sending action here
|
||||
sendMessage(event.currentTarget.value);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (aiChatBotState.isModalOpen) {
|
||||
document.body.style.overflow = 'hidden'
|
||||
} else {
|
||||
document.body.style.overflow = 'unset'
|
||||
}
|
||||
}, [aiChatBotState.isModalOpen])
|
||||
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: event.currentTarget.value });
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (event.key === 'Enter') {
|
||||
// Perform the sending action here
|
||||
sendMessage(event.currentTarget.value)
|
||||
}
|
||||
}
|
||||
|
||||
const sendMessage = async (message: string) => {
|
||||
if (aiChatBotState.aichat_uuid) {
|
||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } });
|
||||
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' });
|
||||
const response = await sendActivityAIChatMessage(message, aiChatBotState.aichat_uuid, props.activity.activity_uuid)
|
||||
if (response.success == false) {
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
||||
await dispatchAIChatBot({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } });
|
||||
return;
|
||||
}
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
await dispatchAIChatBot({
|
||||
type: 'setChatInputValue',
|
||||
payload: event.currentTarget.value,
|
||||
})
|
||||
}
|
||||
|
||||
} else {
|
||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } });
|
||||
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' });
|
||||
const response = await startActivityAIChatSession(message, props.activity.activity_uuid)
|
||||
if (response.success == false) {
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
||||
await dispatchAIChatBot({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } });
|
||||
return;
|
||||
}
|
||||
await dispatchAIChatBot({ type: 'setAichat_uuid', payload: response.data.aichat_uuid });
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
|
||||
}
|
||||
const sendMessage = async (message: string) => {
|
||||
if (aiChatBotState.aichat_uuid) {
|
||||
await dispatchAIChatBot({
|
||||
type: 'addMessage',
|
||||
payload: { sender: 'user', message: message, type: 'user' },
|
||||
})
|
||||
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' })
|
||||
const response = await sendActivityAIChatMessage(
|
||||
message,
|
||||
aiChatBotState.aichat_uuid,
|
||||
props.activity.activity_uuid
|
||||
)
|
||||
if (response.success == false) {
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
|
||||
await dispatchAIChatBot({
|
||||
type: 'setError',
|
||||
payload: {
|
||||
isError: true,
|
||||
status: response.status,
|
||||
error_message: response.data.detail,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
|
||||
await dispatchAIChatBot({
|
||||
type: 'addMessage',
|
||||
payload: { sender: 'ai', message: response.data.message, type: 'ai' },
|
||||
})
|
||||
} else {
|
||||
await dispatchAIChatBot({
|
||||
type: 'addMessage',
|
||||
payload: { sender: 'user', message: message, type: 'user' },
|
||||
})
|
||||
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' })
|
||||
const response = await startActivityAIChatSession(
|
||||
message,
|
||||
props.activity.activity_uuid
|
||||
)
|
||||
if (response.success == false) {
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
|
||||
await dispatchAIChatBot({
|
||||
type: 'setError',
|
||||
payload: {
|
||||
isError: true,
|
||||
status: response.status,
|
||||
error_message: response.data.detail,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
await dispatchAIChatBot({
|
||||
type: 'setAichat_uuid',
|
||||
payload: response.data.aichat_uuid,
|
||||
})
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
|
||||
await dispatchAIChatBot({
|
||||
type: 'addMessage',
|
||||
payload: { sender: 'ai', message: response.data.message, type: 'ai' },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
dispatchAIChatBot({ type: 'setIsModalClose' });
|
||||
function closeModal() {
|
||||
dispatchAIChatBot({ type: 'setIsModalClose' })
|
||||
}
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (messagesEndRef.current) {
|
||||
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
}, [aiChatBotState.messages, session])
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (messagesEndRef.current) {
|
||||
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
}, [aiChatBotState.messages, session]);
|
||||
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{aiChatBotState.isModalOpen && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ y: 20, opacity: 0.3, filter: 'blur(5px)' }}
|
||||
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
||||
exit={{ y: 50, opacity: 0, filter: 'blur(25px)' }}
|
||||
transition={{ type: "spring", bounce: 0.35, duration: 1.7, mass: 0.2, velocity: 2 }}
|
||||
className='fixed top-0 left-0 w-full h-full z-50 flex justify-center items-center '
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
pointerEvents: 'auto',
|
||||
background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(105.16% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgb(2 1 25 / 98%)'
|
||||
}}
|
||||
className="bg-black z-50 rounded-2xl max-w-screen-2xl w-10/12 my-10 mx-auto h-[350px] fixed bottom-0 left-1/2 transform -translate-x-1/2 shadow-lg ring-1 ring-inset ring-white/10 text-white p-4 flex-col-reverse backdrop-blur-md">
|
||||
<div className='flex flex-row-reverse pb-3 justify-between items-center'>
|
||||
<div className='flex space-x-2 items-center'>
|
||||
|
||||
<X size={20} className='text-white/50 hover:cursor-pointer bg-white/10 p-1 rounded-full items-center' onClick={closeModal} />
|
||||
|
||||
</div>
|
||||
<div className={`flex space-x-2 items-center -ml-[100px] ${aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''}`}>
|
||||
<Image className={`outline outline-1 outline-neutral-200/20 rounded-lg ${aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''}`} width={24} src={learnhouseAI_icon} alt="" />
|
||||
<span className='text-sm font-semibold text-white/70'> AI</span>
|
||||
</div>
|
||||
<div className='bg-white/5 text-white/40 py-0.5 px-3 flex space-x-1 rounded-full items-center'>
|
||||
<FlaskConical size={14} />
|
||||
<span className='text-xs font-semibold antialiased '>Experimental</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className={`w-100 h-0.5 bg-white/5 rounded-full mx-auto mb-3 ${aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''}`}></div>
|
||||
{aiChatBotState.messages.length > 0 && !aiChatBotState.error.isError ? (
|
||||
<div className='flex-col h-[237px] w-full space-y-4 overflow-scroll scrollbar-w-2 scrollbar scrollbar-thumb-white/20 scrollbar-thumb-rounded-full scrollbar-track-rounded-full'>
|
||||
{aiChatBotState.messages.map((message: AIMessage, index: number) => {
|
||||
return (
|
||||
<AIMessage key={index} message={message} animated={message.sender == 'ai' ? true : false} />
|
||||
)
|
||||
})}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
) : (
|
||||
<AIMessagePlaceHolder sendMessage={sendMessage} activity_uuid={props.activity.activity_uuid} />
|
||||
)}
|
||||
{aiChatBotState.error.isError && (
|
||||
<div className='flex items-center h-[237px]'>
|
||||
<div className='flex flex-col mx-auto w-[600px] space-y-2 p-5 rounded-lg bg-red-500/20 outline outline-1 outline-red-500'>
|
||||
<AlertTriangle size={20} className='text-red-500' />
|
||||
<div className='flex flex-col'>
|
||||
<h3 className='font-semibold text-red-200'>Something wrong happened</h3>
|
||||
<span className='text-red-100 text-sm '>{aiChatBotState.error.error_message}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
<div className='flex space-x-2 items-center'>
|
||||
<div className=''>
|
||||
<UserAvatar rounded='rounded-lg' border='border-2' width={35} />
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<input onKeyDown={handleKeyDown} onChange={handleChange} disabled={aiChatBotState.isWaitingForResponse} value={aiChatBotState.chatInputValue} placeholder='Ask AI About this Lecture' type="text" className={inputClass} name="" id="" />
|
||||
|
||||
</div>
|
||||
<div className=''>
|
||||
<MessageCircle size={20} className='text-white/50 hover:cursor-pointer' onClick={() => sendMessage(aiChatBotState.chatInputValue)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{aiChatBotState.isModalOpen && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ y: 20, opacity: 0.3, filter: 'blur(5px)' }}
|
||||
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
||||
exit={{ y: 50, opacity: 0, filter: 'blur(25px)' }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
bounce: 0.35,
|
||||
duration: 1.7,
|
||||
mass: 0.2,
|
||||
velocity: 2,
|
||||
}}
|
||||
className="fixed top-0 left-0 w-full h-full z-50 flex justify-center items-center "
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
pointerEvents: 'auto',
|
||||
background:
|
||||
'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(105.16% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgb(2 1 25 / 98%)',
|
||||
}}
|
||||
className="bg-black z-50 rounded-2xl max-w-screen-2xl w-10/12 my-10 mx-auto h-[350px] fixed bottom-0 left-1/2 transform -translate-x-1/2 shadow-lg ring-1 ring-inset ring-white/10 text-white p-4 flex-col-reverse backdrop-blur-md"
|
||||
>
|
||||
<div className="flex flex-row-reverse pb-3 justify-between items-center">
|
||||
<div className="flex space-x-2 items-center">
|
||||
<X
|
||||
size={20}
|
||||
className="text-white/50 hover:cursor-pointer bg-white/10 p-1 rounded-full items-center"
|
||||
onClick={closeModal}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`flex space-x-2 items-center -ml-[100px] ${
|
||||
aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
className={`outline outline-1 outline-neutral-200/20 rounded-lg ${
|
||||
aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''
|
||||
}`}
|
||||
width={24}
|
||||
src={learnhouseAI_icon}
|
||||
alt=""
|
||||
/>
|
||||
<span className="text-sm font-semibold text-white/70">
|
||||
{' '}
|
||||
AI
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-white/5 text-white/40 py-0.5 px-3 flex space-x-1 rounded-full items-center">
|
||||
<FlaskConical size={14} />
|
||||
<span className="text-xs font-semibold antialiased ">
|
||||
Experimental
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-100 h-0.5 bg-white/5 rounded-full mx-auto mb-3 ${
|
||||
aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''
|
||||
}`}
|
||||
></div>
|
||||
{aiChatBotState.messages.length > 0 &&
|
||||
!aiChatBotState.error.isError ? (
|
||||
<div className="flex-col h-[237px] w-full space-y-4 overflow-scroll scrollbar-w-2 scrollbar scrollbar-thumb-white/20 scrollbar-thumb-rounded-full scrollbar-track-rounded-full">
|
||||
{aiChatBotState.messages.map(
|
||||
(message: AIMessage, index: number) => {
|
||||
return (
|
||||
<AIMessage
|
||||
key={index}
|
||||
message={message}
|
||||
animated={message.sender == 'ai' ? true : false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
) : (
|
||||
<AIMessagePlaceHolder
|
||||
sendMessage={sendMessage}
|
||||
activity_uuid={props.activity.activity_uuid}
|
||||
/>
|
||||
)}
|
||||
{aiChatBotState.error.isError && (
|
||||
<div className="flex items-center h-[237px]">
|
||||
<div className="flex flex-col mx-auto w-[600px] space-y-2 p-5 rounded-lg bg-red-500/20 outline outline-1 outline-red-500">
|
||||
<AlertTriangle size={20} className="text-red-500" />
|
||||
<div className="flex flex-col">
|
||||
<h3 className="font-semibold text-red-200">
|
||||
Something wrong happened
|
||||
</h3>
|
||||
<span className="text-red-100 text-sm ">
|
||||
{aiChatBotState.error.error_message}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex space-x-2 items-center">
|
||||
<div className="">
|
||||
<UserAvatar
|
||||
rounded="rounded-lg"
|
||||
border="border-2"
|
||||
width={35}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<input
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleChange}
|
||||
disabled={aiChatBotState.isWaitingForResponse}
|
||||
value={aiChatBotState.chatInputValue}
|
||||
placeholder="Ask AI About this Lecture"
|
||||
type="text"
|
||||
className={inputClass}
|
||||
name=""
|
||||
id=""
|
||||
/>
|
||||
</div>
|
||||
<div className="">
|
||||
<MessageCircle
|
||||
size={20}
|
||||
className="text-white/50 hover:cursor-pointer"
|
||||
onClick={() => sendMessage(aiChatBotState.chatInputValue)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
type AIMessageProps = {
|
||||
message: AIMessage;
|
||||
animated: boolean;
|
||||
message: AIMessage
|
||||
animated: boolean
|
||||
}
|
||||
|
||||
function AIMessage(props: AIMessageProps) {
|
||||
const session = useSession() as any;
|
||||
const session = useSession() as any
|
||||
|
||||
const words = props.message.message.split(' ');
|
||||
const words = props.message.message.split(' ')
|
||||
|
||||
return (
|
||||
<div className="flex space-x-2 w-full antialiased font-medium">
|
||||
<div className="">
|
||||
{props.message.sender == 'ai' ? (
|
||||
<UserAvatar
|
||||
rounded="rounded-lg"
|
||||
border="border-2"
|
||||
predefined_avatar="ai"
|
||||
width={35}
|
||||
/>
|
||||
) : (
|
||||
<UserAvatar rounded="rounded-lg" border="border-2" width={35} />
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<p
|
||||
className="w-full rounded-lg outline-none px-2 py-1 text-white text-md placeholder:text-white/30"
|
||||
id=""
|
||||
>
|
||||
<AnimatePresence>
|
||||
{words.map((word: string, i: number) => (
|
||||
<motion.span
|
||||
key={i}
|
||||
initial={
|
||||
props.animated ? { opacity: 0, y: -10 } : { opacity: 1, y: 0 }
|
||||
}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={
|
||||
props.animated ? { opacity: 0, y: 10 } : { opacity: 1, y: 0 }
|
||||
}
|
||||
transition={props.animated ? { delay: i * 0.1 } : {}}
|
||||
>
|
||||
{word + ' '}
|
||||
</motion.span>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const AIMessagePlaceHolder = (props: {
|
||||
activity_uuid: string
|
||||
sendMessage: any
|
||||
}) => {
|
||||
const session = useSession() as any
|
||||
const [feedbackModal, setFeedbackModal] = React.useState(false)
|
||||
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes
|
||||
|
||||
if (!aiChatBotState.error.isError) {
|
||||
return (
|
||||
<div className='flex space-x-2 w-full antialiased font-medium'>
|
||||
<div className=''>
|
||||
{props.message.sender == 'ai' ? (
|
||||
<UserAvatar rounded='rounded-lg' border='border-2' predefined_avatar='ai' width={35} />
|
||||
) : (
|
||||
<UserAvatar rounded='rounded-lg' border='border-2' width={35} />
|
||||
)}
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<p className='w-full rounded-lg outline-none px-2 py-1 text-white text-md placeholder:text-white/30' id="">
|
||||
<AnimatePresence>
|
||||
{words.map((word: string, i: number) => (
|
||||
<motion.span
|
||||
key={i}
|
||||
initial={props.animated ? { opacity: 0, y: -10 } : { opacity: 1, y: 0 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={props.animated ? { opacity: 0, y: 10 } : { opacity: 1, y: 0 }}
|
||||
transition={props.animated ? { delay: i * 0.1 } : {}}
|
||||
>
|
||||
{word + ' '}
|
||||
</motion.span>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-col h-[237px] w-full">
|
||||
<div className="flex flex-col text-center justify-center pt-12">
|
||||
<motion.div
|
||||
initial={{ y: 20, opacity: 0, filter: 'blur(5px)' }}
|
||||
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
||||
exit={{ y: 50, opacity: 0 }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
bounce: 0.35,
|
||||
duration: 1.7,
|
||||
mass: 0.2,
|
||||
velocity: 2,
|
||||
delay: 0.17,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
width={100}
|
||||
className="mx-auto"
|
||||
src={learnhouseAI_logo_black}
|
||||
alt=""
|
||||
/>
|
||||
<p className="pt-3 text-2xl font-semibold text-white/70 flex justify-center space-x-2 items-center">
|
||||
<span className="items-center">Hello</span>
|
||||
<span className="capitalize flex space-x-2 items-center">
|
||||
<UserAvatar rounded="rounded-lg" border="border-2" width={35} />
|
||||
<span>{session.user.username},</span>
|
||||
</span>
|
||||
<span>how can we help today ?</span>
|
||||
</p>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ y: 20, opacity: 0, filter: 'blur(5px)' }}
|
||||
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
||||
exit={{ y: 50, opacity: 0 }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
bounce: 0.35,
|
||||
duration: 1.7,
|
||||
mass: 0.2,
|
||||
velocity: 2,
|
||||
delay: 0.27,
|
||||
}}
|
||||
className="questions flex space-x-3 mx-auto pt-6 flex-wrap justify-center"
|
||||
>
|
||||
<AIChatPredefinedQuestion
|
||||
sendMessage={props.sendMessage}
|
||||
label="about"
|
||||
/>
|
||||
<AIChatPredefinedQuestion
|
||||
sendMessage={props.sendMessage}
|
||||
label="flashcards"
|
||||
/>
|
||||
<AIChatPredefinedQuestion
|
||||
sendMessage={props.sendMessage}
|
||||
label="examples"
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const AIMessagePlaceHolder = (props: { activity_uuid: string, sendMessage: any }) => {
|
||||
const session = useSession() as any;
|
||||
const [feedbackModal, setFeedbackModal] = React.useState(false);
|
||||
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes;
|
||||
|
||||
if (!aiChatBotState.error.isError) {
|
||||
return <div className='flex-col h-[237px] w-full'>
|
||||
<div className='flex flex-col text-center justify-center pt-12'>
|
||||
<motion.div
|
||||
initial={{ y: 20, opacity: 0, filter: 'blur(5px)' }}
|
||||
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
||||
exit={{ y: 50, opacity: 0, }}
|
||||
transition={{ type: "spring", bounce: 0.35, duration: 1.7, mass: 0.2, velocity: 2, delay: 0.17 }}
|
||||
|
||||
>
|
||||
|
||||
<Image width={100} className='mx-auto' src={learnhouseAI_logo_black} alt="" />
|
||||
<p className='pt-3 text-2xl font-semibold text-white/70 flex justify-center space-x-2 items-center'>
|
||||
<span className='items-center'>Hello</span>
|
||||
<span className='capitalize flex space-x-2 items-center'>
|
||||
<UserAvatar rounded='rounded-lg' border='border-2' width={35} />
|
||||
<span>{session.user.username},</span>
|
||||
</span>
|
||||
<span>how can we help today ?</span>
|
||||
</p>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ y: 20, opacity: 0, filter: 'blur(5px)' }}
|
||||
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
||||
exit={{ y: 50, opacity: 0, }}
|
||||
transition={{ type: "spring", bounce: 0.35, duration: 1.7, mass: 0.2, velocity: 2, delay: 0.27 }}
|
||||
|
||||
className='questions flex space-x-3 mx-auto pt-6 flex-wrap justify-center'
|
||||
>
|
||||
<AIChatPredefinedQuestion sendMessage={props.sendMessage} label='about' />
|
||||
<AIChatPredefinedQuestion sendMessage={props.sendMessage} label='flashcards' />
|
||||
<AIChatPredefinedQuestion sendMessage={props.sendMessage} label='examples' />
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
const AIChatPredefinedQuestion = (props: {
|
||||
sendMessage: any
|
||||
label: string
|
||||
}) => {
|
||||
function getQuestion(label: string) {
|
||||
if (label === 'about') {
|
||||
return `What is this Activity about ?`
|
||||
} else if (label === 'flashcards') {
|
||||
return `Generate flashcards about this Activity`
|
||||
} else if (label === 'examples') {
|
||||
return `Explain this Activity in practical examples`
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => props.sendMessage(getQuestion(props.label))}
|
||||
className="flex space-x-1.5 items-center bg-white/5 cursor-pointer px-4 py-1.5 rounded-xl outline outline-1 outline-neutral-100/10 text-xs font-semibold text-white/40 hover:text-white/60 hover:bg-white/10 hover:outline-neutral-200/40 delay-75 ease-linear transition-all"
|
||||
>
|
||||
{props.label === 'about' && <BadgeInfo size={15} />}
|
||||
{props.label === 'flashcards' && <NotebookTabs size={15} />}
|
||||
{props.label === 'examples' && <div className="text-white/50">Ex</div>}
|
||||
<span>{getQuestion(props.label)}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const AIChatPredefinedQuestion = (props: { sendMessage: any, label: string }) => {
|
||||
function getQuestion(label: string) {
|
||||
if (label === 'about') {
|
||||
return `What is this Activity about ?`
|
||||
} else if (label === 'flashcards') {
|
||||
return `Generate flashcards about this Activity`
|
||||
} else if (label === 'examples') {
|
||||
return `Explain this Activity in practical examples`
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div onClick={() => props.sendMessage(getQuestion(props.label))} className='flex space-x-1.5 items-center bg-white/5 cursor-pointer px-4 py-1.5 rounded-xl outline outline-1 outline-neutral-100/10 text-xs font-semibold text-white/40 hover:text-white/60 hover:bg-white/10 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'>
|
||||
{props.label === 'about' && <BadgeInfo size={15} />}
|
||||
{props.label === 'flashcards' && <NotebookTabs size={15} />}
|
||||
{props.label === 'examples' && <div className='text-white/50'>Ex</div>}
|
||||
<span>{getQuestion(props.label)}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default AIActivityAsk
|
||||
export default AIActivityAsk
|
||||
|
|
|
|||
|
|
@ -1,24 +1,34 @@
|
|||
import { useOrg } from "@components/Contexts/OrgContext";
|
||||
import { getActivityMediaDirectory } from "@services/media/media";
|
||||
import React from "react";
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import { getActivityMediaDirectory } from '@services/media/media'
|
||||
import React from 'react'
|
||||
|
||||
function DocumentPdfActivity({ activity, course }: { activity: any; course: any }) {
|
||||
const org = useOrg() as any;
|
||||
function DocumentPdfActivity({
|
||||
activity,
|
||||
course,
|
||||
}: {
|
||||
activity: any
|
||||
course: any
|
||||
}) {
|
||||
const org = useOrg() as any
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log(activity);
|
||||
}, [activity, org]);
|
||||
console.log(activity)
|
||||
}, [activity, org])
|
||||
|
||||
return (
|
||||
<div className="m-8 bg-zinc-900 rounded-md mt-14">
|
||||
<iframe
|
||||
className="rounded-lg w-full h-[900px]"
|
||||
src={getActivityMediaDirectory(org?.org_uuid, course?.course_uuid, activity.activity_uuid, activity.content.filename, 'documentpdf')}
|
||||
src={getActivityMediaDirectory(
|
||||
org?.org_uuid,
|
||||
course?.course_uuid,
|
||||
activity.activity_uuid,
|
||||
activity.content.filename,
|
||||
'documentpdf'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default DocumentPdfActivity;
|
||||
|
||||
|
||||
export default DocumentPdfActivity
|
||||
|
|
|
|||
|
|
@ -1,131 +1,220 @@
|
|||
import React from 'react'
|
||||
import { Editor } from '@tiptap/core';
|
||||
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
||||
import Image from 'next/image';
|
||||
import { BookOpen, FormInput, Languages, MoreVertical } from 'lucide-react';
|
||||
import { BubbleMenu } from '@tiptap/react';
|
||||
import ToolTip from '@components/StyledElements/Tooltip/Tooltip';
|
||||
import { AIChatBotStateTypes, useAIChatBot, useAIChatBotDispatch } from '@components/Contexts/AI/AIChatBotContext';
|
||||
import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai';
|
||||
import useGetAIFeatures from '../../../../AI/Hooks/useGetAIFeatures';
|
||||
|
||||
|
||||
import { Editor } from '@tiptap/core'
|
||||
import learnhouseAI_icon from 'public/learnhouse_ai_simple.png'
|
||||
import Image from 'next/image'
|
||||
import { BookOpen, FormInput, Languages, MoreVertical } from 'lucide-react'
|
||||
import { BubbleMenu } from '@tiptap/react'
|
||||
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
|
||||
import {
|
||||
AIChatBotStateTypes,
|
||||
useAIChatBot,
|
||||
useAIChatBotDispatch,
|
||||
} from '@components/Contexts/AI/AIChatBotContext'
|
||||
import {
|
||||
sendActivityAIChatMessage,
|
||||
startActivityAIChatSession,
|
||||
} from '@services/ai/ai'
|
||||
import useGetAIFeatures from '../../../../AI/Hooks/useGetAIFeatures'
|
||||
|
||||
type AICanvaToolkitProps = {
|
||||
editor: Editor,
|
||||
activity: any
|
||||
editor: Editor
|
||||
activity: any
|
||||
}
|
||||
|
||||
function AICanvaToolkit(props: AICanvaToolkitProps) {
|
||||
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'activity_ask' });
|
||||
const [isBubbleMenuAvailable, setIsButtonAvailable] = React.useState(false);
|
||||
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'activity_ask' })
|
||||
const [isBubbleMenuAvailable, setIsButtonAvailable] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (is_ai_feature_enabled) {
|
||||
setIsButtonAvailable(true);
|
||||
}
|
||||
}, [is_ai_feature_enabled])
|
||||
React.useEffect(() => {
|
||||
if (is_ai_feature_enabled) {
|
||||
setIsButtonAvailable(true)
|
||||
}
|
||||
}, [is_ai_feature_enabled])
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{isBubbleMenuAvailable && <BubbleMenu className="w-fit" tippyOptions={{ duration: 100 }} editor={props.editor}>
|
||||
<div style={{ background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(105.16% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgba(2, 1, 25, 0.98)' }}
|
||||
className='py-1 h-10 px-2 w-max text-white rounded-xl shadow-md cursor-pointer flex items-center space-x-2 antialiased'
|
||||
>
|
||||
<div className='flex w-full space-x-2 font-bold text-white/80'><Image className='outline outline-1 outline-neutral-200/10 rounded-lg' width={24} src={learnhouseAI_icon} alt="" /> <div>AI</div> </div>
|
||||
<div>
|
||||
<MoreVertical className='text-white/50' size={12} />
|
||||
</div>
|
||||
<div className='flex space-x-2'>
|
||||
<AIActionButton editor={props.editor} activity={props.activity} label='Explain' />
|
||||
<AIActionButton editor={props.editor} activity={props.activity} label='Summarize' />
|
||||
<AIActionButton editor={props.editor} activity={props.activity} label='Translate' />
|
||||
<AIActionButton editor={props.editor} activity={props.activity} label='Examples' />
|
||||
</div>
|
||||
</div>
|
||||
</BubbleMenu>}
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
{isBubbleMenuAvailable && (
|
||||
<BubbleMenu
|
||||
className="w-fit"
|
||||
tippyOptions={{ duration: 100 }}
|
||||
editor={props.editor}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(105.16% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgba(2, 1, 25, 0.98)',
|
||||
}}
|
||||
className="py-1 h-10 px-2 w-max text-white rounded-xl shadow-md cursor-pointer flex items-center space-x-2 antialiased"
|
||||
>
|
||||
<div className="flex w-full space-x-2 font-bold text-white/80">
|
||||
<Image
|
||||
className="outline outline-1 outline-neutral-200/10 rounded-lg"
|
||||
width={24}
|
||||
src={learnhouseAI_icon}
|
||||
alt=""
|
||||
/>{' '}
|
||||
<div>AI</div>{' '}
|
||||
</div>
|
||||
<div>
|
||||
<MoreVertical className="text-white/50" size={12} />
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<AIActionButton
|
||||
editor={props.editor}
|
||||
activity={props.activity}
|
||||
label="Explain"
|
||||
/>
|
||||
<AIActionButton
|
||||
editor={props.editor}
|
||||
activity={props.activity}
|
||||
label="Summarize"
|
||||
/>
|
||||
<AIActionButton
|
||||
editor={props.editor}
|
||||
activity={props.activity}
|
||||
label="Translate"
|
||||
/>
|
||||
<AIActionButton
|
||||
editor={props.editor}
|
||||
activity={props.activity}
|
||||
label="Examples"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BubbleMenu>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function AIActionButton(props: { editor: Editor, label: string, activity: any }) {
|
||||
const dispatchAIChatBot = useAIChatBotDispatch() as any;
|
||||
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes;
|
||||
function AIActionButton(props: {
|
||||
editor: Editor
|
||||
label: string
|
||||
activity: any
|
||||
}) {
|
||||
const dispatchAIChatBot = useAIChatBotDispatch() as any
|
||||
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes
|
||||
|
||||
async function handleAction(label: string) {
|
||||
const selection = getTipTapEditorSelectedText();
|
||||
const prompt = getPrompt(label, selection);
|
||||
dispatchAIChatBot({ type: 'setIsModalOpen' });
|
||||
await sendMessage(prompt);
|
||||
async function handleAction(label: string) {
|
||||
const selection = getTipTapEditorSelectedText()
|
||||
const prompt = getPrompt(label, selection)
|
||||
dispatchAIChatBot({ type: 'setIsModalOpen' })
|
||||
await sendMessage(prompt)
|
||||
}
|
||||
|
||||
const getTipTapEditorSelectedText = () => {
|
||||
const selection = props.editor.state.selection
|
||||
const from = selection.from
|
||||
const to = selection.to
|
||||
const text = props.editor.state.doc.textBetween(from, to)
|
||||
return text
|
||||
}
|
||||
|
||||
const getPrompt = (label: string, selection: string) => {
|
||||
if (label === 'Explain') {
|
||||
return `Explain this part of the course "${selection}" keep this course context in mind.`
|
||||
} else if (label === 'Summarize') {
|
||||
return `Summarize this "${selection}" with the course context in mind.`
|
||||
} else if (label === 'Translate') {
|
||||
return `Translate "${selection}" to another language.`
|
||||
} else {
|
||||
return `Give examples to understand "${selection}" better, if possible give context in the course.`
|
||||
}
|
||||
}
|
||||
|
||||
const getTipTapEditorSelectedText = () => {
|
||||
const selection = props.editor.state.selection;
|
||||
const from = selection.from;
|
||||
const to = selection.to;
|
||||
const text = props.editor.state.doc.textBetween(from, to);
|
||||
return text;
|
||||
const sendMessage = async (message: string) => {
|
||||
if (aiChatBotState.aichat_uuid) {
|
||||
await dispatchAIChatBot({
|
||||
type: 'addMessage',
|
||||
payload: { sender: 'user', message: message, type: 'user' },
|
||||
})
|
||||
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' })
|
||||
const response = await sendActivityAIChatMessage(
|
||||
message,
|
||||
aiChatBotState.aichat_uuid,
|
||||
props.activity.activity_uuid
|
||||
)
|
||||
if (response.success == false) {
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
|
||||
await dispatchAIChatBot({
|
||||
type: 'setError',
|
||||
payload: {
|
||||
isError: true,
|
||||
status: response.status,
|
||||
error_message: response.data.detail,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
|
||||
await dispatchAIChatBot({
|
||||
type: 'addMessage',
|
||||
payload: { sender: 'ai', message: response.data.message, type: 'ai' },
|
||||
})
|
||||
} else {
|
||||
await dispatchAIChatBot({
|
||||
type: 'addMessage',
|
||||
payload: { sender: 'user', message: message, type: 'user' },
|
||||
})
|
||||
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' })
|
||||
const response = await startActivityAIChatSession(
|
||||
message,
|
||||
props.activity.activity_uuid
|
||||
)
|
||||
if (response.success == false) {
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
|
||||
await dispatchAIChatBot({
|
||||
type: 'setError',
|
||||
payload: {
|
||||
isError: true,
|
||||
status: response.status,
|
||||
error_message: response.data.detail,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
await dispatchAIChatBot({
|
||||
type: 'setAichat_uuid',
|
||||
payload: response.data.aichat_uuid,
|
||||
})
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
|
||||
await dispatchAIChatBot({
|
||||
type: 'addMessage',
|
||||
payload: { sender: 'ai', message: response.data.message, type: 'ai' },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const getPrompt = (label: string, selection: string) => {
|
||||
if (label === 'Explain') {
|
||||
return `Explain this part of the course "${selection}" keep this course context in mind.`
|
||||
} else if (label === 'Summarize') {
|
||||
return `Summarize this "${selection}" with the course context in mind.`
|
||||
} else if (label === 'Translate') {
|
||||
return `Translate "${selection}" to another language.`
|
||||
} else {
|
||||
return `Give examples to understand "${selection}" better, if possible give context in the course.`
|
||||
}
|
||||
}
|
||||
|
||||
const sendMessage = async (message: string) => {
|
||||
if (aiChatBotState.aichat_uuid) {
|
||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } });
|
||||
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' });
|
||||
const response = await sendActivityAIChatMessage(message, aiChatBotState.aichat_uuid, props.activity.activity_uuid)
|
||||
if (response.success == false) {
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
||||
await dispatchAIChatBot({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } });
|
||||
return;
|
||||
}
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
|
||||
|
||||
} else {
|
||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } });
|
||||
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' });
|
||||
const response = await startActivityAIChatSession(message, props.activity.activity_uuid)
|
||||
if (response.success == false) {
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
||||
await dispatchAIChatBot({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } });
|
||||
return;
|
||||
}
|
||||
await dispatchAIChatBot({ type: 'setAichat_uuid', payload: response.data.aichat_uuid });
|
||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
|
||||
}
|
||||
}
|
||||
|
||||
const tooltipLabel = props.label === 'Explain' ? 'Explain a word or a sentence with AI' : props.label === 'Summarize' ? 'Summarize a long paragraph or text with AI' : props.label === 'Translate' ? 'Translate to different languages with AI' : 'Give examples to understand better with AI'
|
||||
return (
|
||||
<div className='flex space-x-2' >
|
||||
<ToolTip sideOffset={10} slateBlack content={tooltipLabel}>
|
||||
<button onClick={() => handleAction(props.label)} className='flex space-x-1.5 items-center bg-white/10 px-2 py-0.5 rounded-md outline outline-1 outline-neutral-200/20 text-sm font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'>
|
||||
{props.label === 'Explain' && <BookOpen size={16} />}
|
||||
{props.label === 'Summarize' && <FormInput size={16} />}
|
||||
{props.label === 'Translate' && <Languages size={16} />}
|
||||
{props.label === 'Examples' && <div className='text-white/50'>Ex</div>}
|
||||
<div>{props.label}</div>
|
||||
</button>
|
||||
</ToolTip>
|
||||
</div>
|
||||
)
|
||||
const tooltipLabel =
|
||||
props.label === 'Explain'
|
||||
? 'Explain a word or a sentence with AI'
|
||||
: props.label === 'Summarize'
|
||||
? 'Summarize a long paragraph or text with AI'
|
||||
: props.label === 'Translate'
|
||||
? 'Translate to different languages with AI'
|
||||
: 'Give examples to understand better with AI'
|
||||
return (
|
||||
<div className="flex space-x-2">
|
||||
<ToolTip sideOffset={10} slateBlack content={tooltipLabel}>
|
||||
<button
|
||||
onClick={() => handleAction(props.label)}
|
||||
className="flex space-x-1.5 items-center bg-white/10 px-2 py-0.5 rounded-md outline outline-1 outline-neutral-200/20 text-sm font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all"
|
||||
>
|
||||
{props.label === 'Explain' && <BookOpen size={16} />}
|
||||
{props.label === 'Summarize' && <FormInput size={16} />}
|
||||
{props.label === 'Translate' && <Languages size={16} />}
|
||||
{props.label === 'Examples' && (
|
||||
<div className="text-white/50">Ex</div>
|
||||
)}
|
||||
<div>{props.label}</div>
|
||||
</button>
|
||||
</ToolTip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AICanvaToolkit
|
||||
export default AICanvaToolkit
|
||||
|
|
|
|||
|
|
@ -1,45 +1,43 @@
|
|||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import styled from "styled-components"
|
||||
import Youtube from "@tiptap/extension-youtube";
|
||||
import { useEditor, EditorContent } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import styled from 'styled-components'
|
||||
import Youtube from '@tiptap/extension-youtube'
|
||||
// Custom Extensions
|
||||
import InfoCallout from "@components/Objects/Editor/Extensions/Callout/Info/InfoCallout";
|
||||
import WarningCallout from "@components/Objects/Editor/Extensions/Callout/Warning/WarningCallout";
|
||||
import ImageBlock from "@components/Objects/Editor/Extensions/Image/ImageBlock";
|
||||
import VideoBlock from "@components/Objects/Editor/Extensions/Video/VideoBlock";
|
||||
import MathEquationBlock from "@components/Objects/Editor/Extensions/MathEquation/MathEquationBlock";
|
||||
import PDFBlock from "@components/Objects/Editor/Extensions/PDF/PDFBlock";
|
||||
import { OrderedList } from "@tiptap/extension-ordered-list";
|
||||
import QuizBlock from "@components/Objects/Editor/Extensions/Quiz/QuizBlock";
|
||||
import InfoCallout from '@components/Objects/Editor/Extensions/Callout/Info/InfoCallout'
|
||||
import WarningCallout from '@components/Objects/Editor/Extensions/Callout/Warning/WarningCallout'
|
||||
import ImageBlock from '@components/Objects/Editor/Extensions/Image/ImageBlock'
|
||||
import VideoBlock from '@components/Objects/Editor/Extensions/Video/VideoBlock'
|
||||
import MathEquationBlock from '@components/Objects/Editor/Extensions/MathEquation/MathEquationBlock'
|
||||
import PDFBlock from '@components/Objects/Editor/Extensions/PDF/PDFBlock'
|
||||
import { OrderedList } from '@tiptap/extension-ordered-list'
|
||||
import QuizBlock from '@components/Objects/Editor/Extensions/Quiz/QuizBlock'
|
||||
|
||||
// Lowlight
|
||||
import { common, createLowlight } from 'lowlight'
|
||||
const lowlight = createLowlight(common)
|
||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
|
||||
import css from 'highlight.js/lib/languages/css'
|
||||
import js from 'highlight.js/lib/languages/javascript'
|
||||
import ts from 'highlight.js/lib/languages/typescript'
|
||||
import html from 'highlight.js/lib/languages/xml'
|
||||
import python from 'highlight.js/lib/languages/python'
|
||||
import java from 'highlight.js/lib/languages/java'
|
||||
import { NoTextInput } from "@components/Objects/Editor/Extensions/NoTextInput/NoTextInput";
|
||||
import EditorOptionsProvider from "@components/Contexts/Editor/EditorContext";
|
||||
import AICanvaToolkit from "./AI/AICanvaToolkit";
|
||||
|
||||
import { NoTextInput } from '@components/Objects/Editor/Extensions/NoTextInput/NoTextInput'
|
||||
import EditorOptionsProvider from '@components/Contexts/Editor/EditorContext'
|
||||
import AICanvaToolkit from './AI/AICanvaToolkit'
|
||||
|
||||
interface Editor {
|
||||
content: string;
|
||||
activity: any;
|
||||
content: string
|
||||
activity: any
|
||||
}
|
||||
|
||||
function Canva(props: Editor) {
|
||||
/**
|
||||
* Important Note : This is a workaround to enable user interaction features to be implemented easily, like text selection, AI features and other planned features, this is set to true but otherwise it should be set to false.
|
||||
* Another workaround is implemented below to disable the editor from being edited by the user by setting the caret-color to transparent and using a custom extension to filter out transactions that add/edit/remove text.
|
||||
* To let the various Custom Extensions know that the editor is not editable, React context (EditorOptionsProvider) will be used instead of props.extension.options.editable.
|
||||
*/
|
||||
const isEditable = true;
|
||||
|
||||
/**
|
||||
* Important Note : This is a workaround to enable user interaction features to be implemented easily, like text selection, AI features and other planned features, this is set to true but otherwise it should be set to false.
|
||||
* Another workaround is implemented below to disable the editor from being edited by the user by setting the caret-color to transparent and using a custom extension to filter out transactions that add/edit/remove text.
|
||||
* To let the various Custom Extensions know that the editor is not editable, React context (EditorOptionsProvider) will be used instead of props.extension.options.editable.
|
||||
*/
|
||||
const isEditable = true
|
||||
|
||||
// Code Block Languages for Lowlight
|
||||
lowlight.register('html', html)
|
||||
|
|
@ -49,7 +47,6 @@ function Canva(props: Editor) {
|
|||
lowlight.register('python', python)
|
||||
lowlight.register('java', java)
|
||||
|
||||
|
||||
const editor: any = useEditor({
|
||||
editable: isEditable,
|
||||
extensions: [
|
||||
|
|
@ -90,62 +87,55 @@ function Canva(props: Editor) {
|
|||
CodeBlockLowlight.configure({
|
||||
lowlight,
|
||||
}),
|
||||
|
||||
],
|
||||
|
||||
content: props.content,
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
<EditorOptionsProvider options={{ isEditable: false }}>
|
||||
<CanvaWrapper>
|
||||
<AICanvaToolkit activity={props.activity} editor={editor} />
|
||||
<EditorContent editor={editor} />
|
||||
</CanvaWrapper>
|
||||
</EditorOptionsProvider>
|
||||
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const CanvaWrapper = styled.div`
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
.bubble-menu {
|
||||
display: flex;
|
||||
background-color: #0D0D0D;
|
||||
padding: 0.2rem;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
background-color: #0d0d0d;
|
||||
padding: 0.2rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background: none;
|
||||
color: #FFF;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
padding: 0 0.2rem;
|
||||
opacity: 0.6;
|
||||
button {
|
||||
border: none;
|
||||
background: none;
|
||||
color: #fff;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
padding: 0 0.2rem;
|
||||
opacity: 0.6;
|
||||
|
||||
&:hover,
|
||||
&.is-active {
|
||||
opacity: 1;
|
||||
&:hover,
|
||||
&.is-active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// disable chrome outline
|
||||
|
||||
.ProseMirror {
|
||||
|
||||
// Workaround to disable editor from being edited by the user.
|
||||
caret-color: transparent;
|
||||
|
||||
h1 {
|
||||
font-size: 30px;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
|
@ -176,13 +166,13 @@ const CanvaWrapper = styled.div`
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
padding-left: 20px;
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
outline-style: none !important;
|
||||
|
|
@ -191,74 +181,72 @@ const CanvaWrapper = styled.div`
|
|||
|
||||
// Code Block
|
||||
pre {
|
||||
background: #0d0d0d;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
font-family: "JetBrainsMono", monospace;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #0d0d0d;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #616161;
|
||||
}
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-regexp,
|
||||
.hljs-link,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class {
|
||||
color: #f98181;
|
||||
}
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-regexp,
|
||||
.hljs-link,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class {
|
||||
color: #f98181;
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-meta,
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params {
|
||||
color: #fbbc88;
|
||||
}
|
||||
.hljs-number,
|
||||
.hljs-meta,
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params {
|
||||
color: #fbbc88;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet {
|
||||
color: #b9f18d;
|
||||
}
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet {
|
||||
color: #b9f18d;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #faf594;
|
||||
}
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #faf594;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: #70cff8;
|
||||
}
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: #70cff8;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: 700;
|
||||
.hljs-strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
`;
|
||||
|
||||
export default Canva;
|
||||
export default Canva
|
||||
|
|
|
|||
|
|
@ -1,68 +1,70 @@
|
|||
import React from "react";
|
||||
import YouTube from 'react-youtube';
|
||||
import { getActivityMediaDirectory } from "@services/media/media";
|
||||
import { useOrg } from "@components/Contexts/OrgContext";
|
||||
import React from 'react'
|
||||
import YouTube from 'react-youtube'
|
||||
import { getActivityMediaDirectory } from '@services/media/media'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
|
||||
function VideoActivity({ activity, course }: { activity: any; course: any }) {
|
||||
const org = useOrg() as any;
|
||||
const [videoId, setVideoId] = React.useState('');
|
||||
const org = useOrg() as any
|
||||
const [videoId, setVideoId] = React.useState('')
|
||||
|
||||
function getYouTubeEmbed(url: any) {
|
||||
// Extract video ID from the YouTube URL
|
||||
var videoId = url.match(/(?:\?v=|\/embed\/|\/\d\/|\/vi\/|\/v\/|https?:\/\/(?:www\.)?youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([^#\&\?\/]+)/)[1];
|
||||
var videoId = url.match(
|
||||
/(?:\?v=|\/embed\/|\/\d\/|\/vi\/|\/v\/|https?:\/\/(?:www\.)?youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([^#\&\?\/]+)/
|
||||
)[1]
|
||||
|
||||
// Create the embed object
|
||||
var embedObject = {
|
||||
videoId: videoId,
|
||||
width: 560,
|
||||
height: 315
|
||||
};
|
||||
height: 315,
|
||||
}
|
||||
|
||||
return embedObject;
|
||||
return embedObject
|
||||
}
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log(activity);
|
||||
}, [activity, org]);
|
||||
console.log(activity)
|
||||
}, [activity, org])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{activity &&
|
||||
{activity && (
|
||||
<>
|
||||
{activity.activity_sub_type === 'SUBTYPE_VIDEO_HOSTED' && (
|
||||
<div className="m-8 bg-zinc-900 rounded-md mt-14">
|
||||
<video className="rounded-lg w-full h-[500px]" controls
|
||||
src={getActivityMediaDirectory(org?.org_uuid, course?.course_uuid, activity.activity_uuid, activity.content?.filename, 'video')}
|
||||
<video
|
||||
className="rounded-lg w-full h-[500px]"
|
||||
controls
|
||||
src={getActivityMediaDirectory(
|
||||
org?.org_uuid,
|
||||
course?.course_uuid,
|
||||
activity.activity_uuid,
|
||||
activity.content?.filename,
|
||||
'video'
|
||||
)}
|
||||
></video>
|
||||
|
||||
</div>
|
||||
)}
|
||||
{activity.activity_sub_type === 'SUBTYPE_VIDEO_YOUTUBE' && (
|
||||
<div>
|
||||
<YouTube
|
||||
className="rounded-md overflow-hidden m-8 bg-zinc-900 mt-14"
|
||||
opts={
|
||||
{
|
||||
width: '1300',
|
||||
height: '500',
|
||||
playerVars: {
|
||||
autoplay: 0,
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
videoId={videoId} />
|
||||
opts={{
|
||||
width: '1300',
|
||||
height: '500',
|
||||
playerVars: {
|
||||
autoplay: 0,
|
||||
},
|
||||
}}
|
||||
videoId={videoId}
|
||||
/>
|
||||
</div>
|
||||
)}</>}
|
||||
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export default VideoActivity;
|
||||
|
||||
|
||||
export default VideoActivity
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,77 +1,79 @@
|
|||
'use client';
|
||||
import React from "react";
|
||||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import learnhouseIcon from "public/learnhouse_icon.png";
|
||||
import { ToolbarButtons } from "./Toolbar/ToolbarButtons";
|
||||
import { motion } from "framer-motion";
|
||||
import Image from "next/image";
|
||||
import styled from "styled-components";
|
||||
import { DividerVerticalIcon, SlashIcon } from "@radix-ui/react-icons";
|
||||
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
||||
import { AIEditorStateTypes, useAIEditor, useAIEditorDispatch } from "@components/Contexts/AI/AIEditorContext";
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useEditor, EditorContent } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import learnhouseIcon from 'public/learnhouse_icon.png'
|
||||
import { ToolbarButtons } from './Toolbar/ToolbarButtons'
|
||||
import { motion } from 'framer-motion'
|
||||
import Image from 'next/image'
|
||||
import styled from 'styled-components'
|
||||
import { DividerVerticalIcon, SlashIcon } from '@radix-ui/react-icons'
|
||||
import learnhouseAI_icon from 'public/learnhouse_ai_simple.png'
|
||||
import {
|
||||
AIEditorStateTypes,
|
||||
useAIEditor,
|
||||
useAIEditorDispatch,
|
||||
} from '@components/Contexts/AI/AIEditorContext'
|
||||
|
||||
// extensions
|
||||
import InfoCallout from "./Extensions/Callout/Info/InfoCallout";
|
||||
import WarningCallout from "./Extensions/Callout/Warning/WarningCallout";
|
||||
import ImageBlock from "./Extensions/Image/ImageBlock";
|
||||
import Youtube from "@tiptap/extension-youtube";
|
||||
import VideoBlock from "./Extensions/Video/VideoBlock";
|
||||
import { Eye } from "lucide-react";
|
||||
import MathEquationBlock from "./Extensions/MathEquation/MathEquationBlock";
|
||||
import PDFBlock from "./Extensions/PDF/PDFBlock";
|
||||
import QuizBlock from "./Extensions/Quiz/QuizBlock";
|
||||
import ToolTip from "@components/StyledElements/Tooltip/Tooltip";
|
||||
import Link from "next/link";
|
||||
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
||||
import { OrderedList } from "@tiptap/extension-ordered-list";
|
||||
|
||||
import InfoCallout from './Extensions/Callout/Info/InfoCallout'
|
||||
import WarningCallout from './Extensions/Callout/Warning/WarningCallout'
|
||||
import ImageBlock from './Extensions/Image/ImageBlock'
|
||||
import Youtube from '@tiptap/extension-youtube'
|
||||
import VideoBlock from './Extensions/Video/VideoBlock'
|
||||
import { Eye } from 'lucide-react'
|
||||
import MathEquationBlock from './Extensions/MathEquation/MathEquationBlock'
|
||||
import PDFBlock from './Extensions/PDF/PDFBlock'
|
||||
import QuizBlock from './Extensions/Quiz/QuizBlock'
|
||||
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
|
||||
import Link from 'next/link'
|
||||
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
|
||||
import { OrderedList } from '@tiptap/extension-ordered-list'
|
||||
|
||||
// Lowlight
|
||||
import { common, createLowlight } from 'lowlight'
|
||||
const lowlight = createLowlight(common)
|
||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
|
||||
import css from 'highlight.js/lib/languages/css'
|
||||
import js from 'highlight.js/lib/languages/javascript'
|
||||
import ts from 'highlight.js/lib/languages/typescript'
|
||||
import html from 'highlight.js/lib/languages/xml'
|
||||
import python from 'highlight.js/lib/languages/python'
|
||||
import java from 'highlight.js/lib/languages/java'
|
||||
import { CourseProvider } from "@components/Contexts/CourseContext";
|
||||
import { useSession } from "@components/Contexts/SessionContext";
|
||||
import AIEditorToolkit from "./AI/AIEditorToolkit";
|
||||
import useGetAIFeatures from "@components/AI/Hooks/useGetAIFeatures";
|
||||
import UserAvatar from "../UserAvatar";
|
||||
|
||||
import { CourseProvider } from '@components/Contexts/CourseContext'
|
||||
import { useSession } from '@components/Contexts/SessionContext'
|
||||
import AIEditorToolkit from './AI/AIEditorToolkit'
|
||||
import useGetAIFeatures from '@components/AI/Hooks/useGetAIFeatures'
|
||||
import UserAvatar from '../UserAvatar'
|
||||
|
||||
interface Editor {
|
||||
content: string;
|
||||
ydoc: any;
|
||||
provider: any;
|
||||
activity: any;
|
||||
course: any;
|
||||
org: any;
|
||||
setContent: (content: string) => void;
|
||||
content: string
|
||||
ydoc: any
|
||||
provider: any
|
||||
activity: any
|
||||
course: any
|
||||
org: any
|
||||
setContent: (content: string) => void
|
||||
}
|
||||
|
||||
function Editor(props: Editor) {
|
||||
const session = useSession() as any;
|
||||
const dispatchAIEditor = useAIEditorDispatch() as any;
|
||||
const aiEditorState = useAIEditor() as AIEditorStateTypes;
|
||||
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'editor' });
|
||||
const [isButtonAvailable, setIsButtonAvailable] = React.useState(false);
|
||||
const session = useSession() as any
|
||||
const dispatchAIEditor = useAIEditorDispatch() as any
|
||||
const aiEditorState = useAIEditor() as AIEditorStateTypes
|
||||
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'editor' })
|
||||
const [isButtonAvailable, setIsButtonAvailable] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (is_ai_feature_enabled) {
|
||||
setIsButtonAvailable(true);
|
||||
setIsButtonAvailable(true)
|
||||
}
|
||||
}, [is_ai_feature_enabled])
|
||||
|
||||
// remove course_ from course_uuid
|
||||
const course_uuid = props.course.course_uuid.substring(7);
|
||||
const course_uuid = props.course.course_uuid.substring(7)
|
||||
|
||||
// remove activity_ from activity_uuid
|
||||
const activity_uuid = props.activity.activity_uuid.substring(9);
|
||||
const activity_uuid = props.activity.activity_uuid.substring(9)
|
||||
|
||||
// Code Block Languages for Lowlight
|
||||
lowlight.register('html', html)
|
||||
|
|
@ -124,7 +126,6 @@ function Editor(props: Editor) {
|
|||
lowlight,
|
||||
}),
|
||||
|
||||
|
||||
// Register the document with Tiptap
|
||||
// Collaboration.configure({
|
||||
// document: props.ydoc,
|
||||
|
|
@ -140,98 +141,153 @@ function Editor(props: Editor) {
|
|||
],
|
||||
|
||||
content: props.content,
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<CourseProvider courseuuid={props.course.course_uuid}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
key="modal"
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 360,
|
||||
damping: 70,
|
||||
delay: 0.02,
|
||||
}}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<EditorTop className="fixed bg-white bg-opacity-95 backdrop-blur backdrop-brightness-125">
|
||||
<EditorDocSection>
|
||||
<EditorInfoWrapper>
|
||||
<Link href="/">
|
||||
<EditorInfoLearnHouseLogo width={25} height={25} src={learnhouseIcon} alt="" />
|
||||
</Link>
|
||||
<Link target="_blank" href={`/course/${course_uuid}`}>
|
||||
<EditorInfoThumbnail src={`${getCourseThumbnailMediaDirectory(props.org?.org_uuid, props.course.course_uuid, props.course.thumbnail_image)}`} alt=""></EditorInfoThumbnail>
|
||||
</Link>
|
||||
<EditorInfoDocName>
|
||||
{" "}
|
||||
<b>{props.course.name}</b> <SlashIcon /> {props.activity.name}{" "}
|
||||
</EditorInfoDocName>
|
||||
</EditorInfoWrapper>
|
||||
<EditorButtonsWrapper>
|
||||
<ToolbarButtons editor={editor} />
|
||||
</EditorButtonsWrapper>
|
||||
</EditorDocSection>
|
||||
<EditorUsersSection className="space-x-2">
|
||||
<div>
|
||||
<div className="transition-all ease-linear text-teal-100 rounded-md hover:cursor-pointer" >
|
||||
{isButtonAvailable && <div
|
||||
onClick={() => dispatchAIEditor({ type: aiEditorState.isModalOpen ? 'setIsModalClose' : 'setIsModalOpen' })}
|
||||
<CourseProvider courseuuid={props.course.course_uuid}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
key="modal"
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 360,
|
||||
damping: 70,
|
||||
delay: 0.02,
|
||||
}}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<EditorTop className="fixed bg-white bg-opacity-95 backdrop-blur backdrop-brightness-125">
|
||||
<EditorDocSection>
|
||||
<EditorInfoWrapper>
|
||||
<Link href="/">
|
||||
<EditorInfoLearnHouseLogo
|
||||
width={25}
|
||||
height={25}
|
||||
src={learnhouseIcon}
|
||||
alt=""
|
||||
/>
|
||||
</Link>
|
||||
<Link target="_blank" href={`/course/${course_uuid}`}>
|
||||
<EditorInfoThumbnail
|
||||
src={`${getCourseThumbnailMediaDirectory(
|
||||
props.org?.org_uuid,
|
||||
props.course.course_uuid,
|
||||
props.course.thumbnail_image
|
||||
)}`}
|
||||
alt=""
|
||||
></EditorInfoThumbnail>
|
||||
</Link>
|
||||
<EditorInfoDocName>
|
||||
{' '}
|
||||
<b>{props.course.name}</b> <SlashIcon /> {props.activity.name}{' '}
|
||||
</EditorInfoDocName>
|
||||
</EditorInfoWrapper>
|
||||
<EditorButtonsWrapper>
|
||||
<ToolbarButtons editor={editor} />
|
||||
</EditorButtonsWrapper>
|
||||
</EditorDocSection>
|
||||
<EditorUsersSection className="space-x-2">
|
||||
<div>
|
||||
<div className="transition-all ease-linear text-teal-100 rounded-md hover:cursor-pointer">
|
||||
{isButtonAvailable && (
|
||||
<div
|
||||
onClick={() =>
|
||||
dispatchAIEditor({
|
||||
type: aiEditorState.isModalOpen
|
||||
? 'setIsModalClose'
|
||||
: 'setIsModalOpen',
|
||||
})
|
||||
}
|
||||
style={{
|
||||
background: 'conic-gradient(from 32deg at 53.75% 50%, rgb(35, 40, 93) 4deg, rgba(20, 0, 52, 0.95) 59deg, rgba(164, 45, 238, 0.88) 281deg)',
|
||||
background:
|
||||
'conic-gradient(from 32deg at 53.75% 50%, rgb(35, 40, 93) 4deg, rgba(20, 0, 52, 0.95) 59deg, rgba(164, 45, 238, 0.88) 281deg)',
|
||||
}}
|
||||
className="rounded-md px-3 py-2 drop-shadow-md flex items-center space-x-1.5 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out hover:scale-105">
|
||||
{" "}
|
||||
className="rounded-md px-3 py-2 drop-shadow-md flex items-center space-x-1.5 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out hover:scale-105"
|
||||
>
|
||||
{' '}
|
||||
<i>
|
||||
<Image className='' width={20} src={learnhouseAI_icon} alt="" />
|
||||
</i>{" "}
|
||||
<Image
|
||||
className=""
|
||||
width={20}
|
||||
src={learnhouseAI_icon}
|
||||
alt=""
|
||||
/>
|
||||
</i>{' '}
|
||||
<i className="not-italic text-xs font-bold">AI Editor</i>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DividerVerticalIcon style={{ marginTop: "auto", marginBottom: "auto", color: "grey", opacity: '0.5' }} />
|
||||
<EditorLeftOptionsSection className="space-x-2 ">
|
||||
<div className="bg-sky-600 hover:bg-sky-700 transition-all ease-linear px-3 py-2 font-black text-sm shadow text-teal-100 rounded-lg hover:cursor-pointer" onClick={() => props.setContent(editor.getJSON())}> Save </div>
|
||||
<ToolTip content="Preview">
|
||||
<Link target="_blank" href={`/course/${course_uuid}/activity/${activity_uuid}`}>
|
||||
<div className="flex bg-neutral-600 hover:bg-neutral-700 transition-all ease-linear h-9 px-3 py-2 font-black justify-center items-center text-sm shadow text-neutral-100 rounded-lg hover:cursor-pointer">
|
||||
<Eye className="mx-auto items-center" size={15} />
|
||||
</div>
|
||||
</Link>
|
||||
</ToolTip>
|
||||
</EditorLeftOptionsSection>
|
||||
<DividerVerticalIcon style={{ marginTop: "auto", marginBottom: "auto", color: "grey", opacity: '0.5' }} />
|
||||
</div>
|
||||
<DividerVerticalIcon
|
||||
style={{
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
color: 'grey',
|
||||
opacity: '0.5',
|
||||
}}
|
||||
/>
|
||||
<EditorLeftOptionsSection className="space-x-2 ">
|
||||
<div
|
||||
className="bg-sky-600 hover:bg-sky-700 transition-all ease-linear px-3 py-2 font-black text-sm shadow text-teal-100 rounded-lg hover:cursor-pointer"
|
||||
onClick={() => props.setContent(editor.getJSON())}
|
||||
>
|
||||
{' '}
|
||||
Save{' '}
|
||||
</div>
|
||||
<ToolTip content="Preview">
|
||||
<Link
|
||||
target="_blank"
|
||||
href={`/course/${course_uuid}/activity/${activity_uuid}`}
|
||||
>
|
||||
<div className="flex bg-neutral-600 hover:bg-neutral-700 transition-all ease-linear h-9 px-3 py-2 font-black justify-center items-center text-sm shadow text-neutral-100 rounded-lg hover:cursor-pointer">
|
||||
<Eye className="mx-auto items-center" size={15} />
|
||||
</div>
|
||||
</Link>
|
||||
</ToolTip>
|
||||
</EditorLeftOptionsSection>
|
||||
<DividerVerticalIcon
|
||||
style={{
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
color: 'grey',
|
||||
opacity: '0.5',
|
||||
}}
|
||||
/>
|
||||
|
||||
<EditorUserProfileWrapper>
|
||||
{!session.isAuthenticated && <span>Loading</span>}
|
||||
{session.isAuthenticated && <UserAvatar width={40} border="border-4" rounded="rounded-full"/>}
|
||||
</EditorUserProfileWrapper>
|
||||
|
||||
</EditorUsersSection>
|
||||
</EditorTop>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.99 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 360,
|
||||
damping: 70,
|
||||
delay: 0.5,
|
||||
}}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<EditorContentWrapper>
|
||||
<AIEditorToolkit activity={props.activity} editor={editor} />
|
||||
<EditorContent editor={editor} />
|
||||
</EditorContentWrapper>
|
||||
</motion.div>
|
||||
</CourseProvider>
|
||||
<EditorUserProfileWrapper>
|
||||
{!session.isAuthenticated && <span>Loading</span>}
|
||||
{session.isAuthenticated && (
|
||||
<UserAvatar
|
||||
width={40}
|
||||
border="border-4"
|
||||
rounded="rounded-full"
|
||||
/>
|
||||
)}
|
||||
</EditorUserProfileWrapper>
|
||||
</EditorUsersSection>
|
||||
</EditorTop>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.99 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 360,
|
||||
damping: 70,
|
||||
delay: 0.5,
|
||||
}}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<EditorContentWrapper>
|
||||
<AIEditorToolkit activity={props.activity} editor={editor} />
|
||||
<EditorContent editor={editor} />
|
||||
</EditorContentWrapper>
|
||||
</motion.div>
|
||||
</CourseProvider>
|
||||
</Page>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const Page = styled.div`
|
||||
|
|
@ -240,12 +296,15 @@ const Page = styled.div`
|
|||
padding-top: 30px;
|
||||
|
||||
// dots background
|
||||
background-image: radial-gradient(#4744446b 1px, transparent 1px), radial-gradient(#4744446b 1px, transparent 1px);
|
||||
background-position: 0 0, 25px 25px;
|
||||
background-image: radial-gradient(#4744446b 1px, transparent 1px),
|
||||
radial-gradient(#4744446b 1px, transparent 1px);
|
||||
background-position:
|
||||
0 0,
|
||||
25px 25px;
|
||||
background-size: 50px 50px;
|
||||
background-attachment: fixed;
|
||||
background-repeat: repeat;
|
||||
`;
|
||||
`
|
||||
|
||||
const EditorTop = styled.div`
|
||||
border-radius: 15px;
|
||||
|
|
@ -259,35 +318,34 @@ const EditorTop = styled.div`
|
|||
position: fixed;
|
||||
z-index: 303;
|
||||
width: -webkit-fill-available;
|
||||
`;
|
||||
`
|
||||
|
||||
// Inside EditorTop
|
||||
const EditorDocSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
`
|
||||
const EditorUsersSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
`
|
||||
|
||||
const EditorLeftOptionsSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
`
|
||||
|
||||
// Inside EditorDocSection
|
||||
const EditorInfoWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 5px;
|
||||
`;
|
||||
const EditorButtonsWrapper = styled.div``;
|
||||
`
|
||||
const EditorButtonsWrapper = styled.div``
|
||||
|
||||
// Inside EditorUsersSection
|
||||
const EditorUserProfileWrapper = styled.div`
|
||||
|
|
@ -295,14 +353,14 @@ const EditorUserProfileWrapper = styled.div`
|
|||
svg {
|
||||
border-radius: 7px;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
// Inside EditorInfoWrapper
|
||||
//..todo
|
||||
const EditorInfoLearnHouseLogo = styled(Image)`
|
||||
border-radius: 6px;
|
||||
margin-right: 0px;
|
||||
`;
|
||||
`
|
||||
const EditorInfoDocName = styled.div`
|
||||
font-size: 16px;
|
||||
justify-content: center;
|
||||
|
|
@ -317,8 +375,7 @@ const EditorInfoDocName = styled.div`
|
|||
padding: 3px;
|
||||
color: #353535;
|
||||
}
|
||||
`;
|
||||
|
||||
`
|
||||
|
||||
const EditorInfoThumbnail = styled.img`
|
||||
height: 25px;
|
||||
|
|
@ -331,7 +388,7 @@ const EditorInfoThumbnail = styled.img`
|
|||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const EditorContentWrapper = styled.div`
|
||||
margin: 40px;
|
||||
|
|
@ -344,9 +401,8 @@ export const EditorContentWrapper = styled.div`
|
|||
// disable chrome outline
|
||||
|
||||
.ProseMirror {
|
||||
|
||||
h1 {
|
||||
font-size: 30px;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
|
@ -393,72 +449,71 @@ export const EditorContentWrapper = styled.div`
|
|||
|
||||
// Code Block
|
||||
pre {
|
||||
background: #0d0d0d;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
font-family: "JetBrainsMono", monospace;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #0d0d0d;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-regexp,
|
||||
.hljs-link,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class {
|
||||
color: #f98181;
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-meta,
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params {
|
||||
color: #fbbc88;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet {
|
||||
color: #b9f18d;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #faf594;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: #70cff8;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-regexp,
|
||||
.hljs-link,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class {
|
||||
color: #f98181;
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-meta,
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params {
|
||||
color: #fbbc88;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet {
|
||||
color: #b9f18d;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #faf594;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: #70cff8;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
iframe {
|
||||
|
|
@ -472,15 +527,12 @@ export const EditorContentWrapper = styled.div`
|
|||
outline: 0px solid transparent;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
padding-left: 20px;
|
||||
list-style-type: decimal;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
|
||||
|
||||
`;
|
||||
|
||||
export default Editor;
|
||||
export default Editor
|
||||
|
|
|
|||
|
|
@ -1,61 +1,66 @@
|
|||
'use client';
|
||||
import { default as React, } from "react";
|
||||
import * as Y from "yjs";
|
||||
import Editor from "./Editor";
|
||||
import { updateActivity } from "@services/courses/activities";
|
||||
import { toast } from "react-hot-toast";
|
||||
import Toast from "@components/StyledElements/Toast/Toast";
|
||||
import { OrgProvider } from "@components/Contexts/OrgContext";
|
||||
'use client'
|
||||
import { default as React } from 'react'
|
||||
import * as Y from 'yjs'
|
||||
import Editor from './Editor'
|
||||
import { updateActivity } from '@services/courses/activities'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import Toast from '@components/StyledElements/Toast/Toast'
|
||||
import { OrgProvider } from '@components/Contexts/OrgContext'
|
||||
|
||||
interface EditorWrapperProps {
|
||||
content: string;
|
||||
activity: any;
|
||||
content: string
|
||||
activity: any
|
||||
course: any
|
||||
org: any;
|
||||
org: any
|
||||
}
|
||||
|
||||
function EditorWrapper(props: EditorWrapperProps): JSX.Element {
|
||||
// A new Y document
|
||||
const ydoc = new Y.Doc();
|
||||
const [providerState, setProviderState] = React.useState<any>({});
|
||||
const [ydocState, setYdocState] = React.useState<any>({});
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const ydoc = new Y.Doc()
|
||||
const [providerState, setProviderState] = React.useState<any>({})
|
||||
const [ydocState, setYdocState] = React.useState<any>({})
|
||||
const [isLoading, setIsLoading] = React.useState(true)
|
||||
|
||||
function createRTCProvider() {
|
||||
// const provider = new WebrtcProvider(props.activity.activity_id, ydoc);
|
||||
// setYdocState(ydoc);
|
||||
// setProviderState(provider);
|
||||
setIsLoading(false);
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async function setContent(content: any) {
|
||||
let activity = props.activity;
|
||||
activity.content = content;
|
||||
let activity = props.activity
|
||||
activity.content = content
|
||||
|
||||
toast.promise(
|
||||
updateActivity(activity, activity.activity_uuid),
|
||||
{
|
||||
loading: 'Saving...',
|
||||
success: <b>Activity saved!</b>,
|
||||
error: <b>Could not save.</b>,
|
||||
}
|
||||
);
|
||||
toast.promise(updateActivity(activity, activity.activity_uuid), {
|
||||
loading: 'Saving...',
|
||||
success: <b>Activity saved!</b>,
|
||||
error: <b>Could not save.</b>,
|
||||
})
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
createRTCProvider();
|
||||
return <div>Loading...</div>;
|
||||
createRTCProvider()
|
||||
return <div>Loading...</div>
|
||||
} else {
|
||||
return <>
|
||||
<Toast></Toast>
|
||||
<OrgProvider orgslug={props.org.slug}>
|
||||
<Editor org={props.org} course={props.course} activity={props.activity} content={props.content} setContent={setContent} provider={providerState} ydoc={ydocState}></Editor>;
|
||||
</OrgProvider>
|
||||
</>
|
||||
return (
|
||||
<>
|
||||
<Toast></Toast>
|
||||
<OrgProvider orgslug={props.org.slug}>
|
||||
<Editor
|
||||
org={props.org}
|
||||
course={props.course}
|
||||
activity={props.activity}
|
||||
content={props.content}
|
||||
setContent={setContent}
|
||||
provider={providerState}
|
||||
ydoc={ydocState}
|
||||
></Editor>
|
||||
;
|
||||
</OrgProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default EditorWrapper;
|
||||
export default EditorWrapper
|
||||
|
|
|
|||
|
|
@ -1,29 +1,29 @@
|
|||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { mergeAttributes, Node } from '@tiptap/core'
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react'
|
||||
|
||||
import InfoCalloutComponent from "./InfoCalloutComponent";
|
||||
import InfoCalloutComponent from './InfoCalloutComponent'
|
||||
|
||||
export default Node.create({
|
||||
name: "calloutInfo",
|
||||
group: "block",
|
||||
name: 'calloutInfo',
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
content: "text*",
|
||||
content: 'text*',
|
||||
|
||||
// TODO : multi line support
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "callout-info",
|
||||
tag: 'callout-info',
|
||||
},
|
||||
];
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["callout-info", mergeAttributes(HTMLAttributes), 0];
|
||||
return ['callout-info', mergeAttributes(HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(InfoCalloutComponent);
|
||||
return ReactNodeViewRenderer(InfoCalloutComponent)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,35 +1,38 @@
|
|||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
||||
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
|
||||
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'
|
||||
import { AlertCircle } from 'lucide-react'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
function InfoCalloutComponent(props: any) {
|
||||
const editorState = useEditorProvider() as any;
|
||||
const isEditable = editorState.isEditable;
|
||||
|
||||
const editorState = useEditorProvider() as any
|
||||
const isEditable = editorState.isEditable
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
<InfoCalloutWrapper className="flex space-x-2 items-center bg-blue-200 rounded-lg text-blue-900 px-3 shadow-inner" contentEditable={isEditable}>
|
||||
<AlertCircle /> <NodeViewContent contentEditable={isEditable} className="content" />
|
||||
<InfoCalloutWrapper
|
||||
className="flex space-x-2 items-center bg-blue-200 rounded-lg text-blue-900 px-3 shadow-inner"
|
||||
contentEditable={isEditable}
|
||||
>
|
||||
<AlertCircle />{' '}
|
||||
<NodeViewContent contentEditable={isEditable} className="content" />
|
||||
</InfoCalloutWrapper>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const InfoCalloutWrapper = styled.div`
|
||||
svg{
|
||||
svg {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 5px;
|
||||
padding: 0.5rem;
|
||||
border: ${(props) => (props.contentEditable ? "2px dashed #1f3a8a12" : "none")};
|
||||
border: ${(props) =>
|
||||
props.contentEditable ? '2px dashed #1f3a8a12' : 'none'};
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
|
||||
|
||||
export default InfoCalloutComponent;
|
||||
export default InfoCalloutComponent
|
||||
|
|
|
|||
|
|
@ -1,29 +1,29 @@
|
|||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { mergeAttributes, Node } from '@tiptap/core'
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react'
|
||||
|
||||
import WarningCalloutComponent from "./WarningCalloutComponent";
|
||||
import WarningCalloutComponent from './WarningCalloutComponent'
|
||||
|
||||
export default Node.create({
|
||||
name: "calloutWarning",
|
||||
group: "block",
|
||||
name: 'calloutWarning',
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
content: "text*",
|
||||
content: 'text*',
|
||||
|
||||
// TODO : multi line support
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "callout-warning",
|
||||
tag: 'callout-warning',
|
||||
},
|
||||
];
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["callout-info", mergeAttributes(HTMLAttributes), 0];
|
||||
return ['callout-info', mergeAttributes(HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(WarningCalloutComponent);
|
||||
return ReactNodeViewRenderer(WarningCalloutComponent)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,25 +1,27 @@
|
|||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
||||
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
|
||||
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
function WarningCalloutComponent(props: any) {
|
||||
const editorState = useEditorProvider() as any;
|
||||
const isEditable = editorState.isEditable;
|
||||
|
||||
const editorState = useEditorProvider() as any
|
||||
const isEditable = editorState.isEditable
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
<CalloutWrapper className="flex space-x-2 items-center bg-yellow-200 rounded-lg text-yellow-900 px-3 shadow-inner" contentEditable={isEditable}>
|
||||
<AlertTriangle /> <NodeViewContent contentEditable={isEditable} className="content" />
|
||||
<CalloutWrapper
|
||||
className="flex space-x-2 items-center bg-yellow-200 rounded-lg text-yellow-900 px-3 shadow-inner"
|
||||
contentEditable={isEditable}
|
||||
>
|
||||
<AlertTriangle />{' '}
|
||||
<NodeViewContent contentEditable={isEditable} className="content" />
|
||||
</CalloutWrapper>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const CalloutWrapper = styled.div`
|
||||
|
||||
|
||||
svg {
|
||||
padding: 3px;
|
||||
}
|
||||
|
|
@ -27,10 +29,11 @@ const CalloutWrapper = styled.div`
|
|||
.content {
|
||||
margin: 5px;
|
||||
padding: 0.5rem;
|
||||
border: ${(props) => (props.contentEditable ? "2px dashed #713f1117" : "none")};
|
||||
border: ${(props) =>
|
||||
props.contentEditable ? '2px dashed #713f1117' : 'none'};
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
const DragHandle = styled.div`
|
||||
position: absolute;
|
||||
|
|
@ -40,6 +43,6 @@ const DragHandle = styled.div`
|
|||
height: 100%;
|
||||
cursor: move;
|
||||
z-index: 1;
|
||||
`;
|
||||
`
|
||||
|
||||
export default WarningCalloutComponent;
|
||||
export default WarningCalloutComponent
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { mergeAttributes, Node } from '@tiptap/core'
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react'
|
||||
|
||||
import ImageBlockComponent from "./ImageBlockComponent";
|
||||
import ImageBlockComponent from './ImageBlockComponent'
|
||||
|
||||
export default Node.create({
|
||||
name: "blockImage",
|
||||
group: "block",
|
||||
name: 'blockImage',
|
||||
group: 'block',
|
||||
|
||||
atom: true,
|
||||
|
||||
|
|
@ -17,22 +17,22 @@ export default Node.create({
|
|||
size: {
|
||||
width: 300,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "block-image",
|
||||
tag: 'block-image',
|
||||
},
|
||||
];
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["block-image", mergeAttributes(HTMLAttributes), 0];
|
||||
return ['block-image', mergeAttributes(HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(ImageBlockComponent);
|
||||
return ReactNodeViewRenderer(ImageBlockComponent)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,95 +1,136 @@
|
|||
import { NodeViewWrapper } from "@tiptap/react";
|
||||
import React, { useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
import { Resizable } from 're-resizable';
|
||||
import { AlertTriangle, Image, Loader } from "lucide-react";
|
||||
import { uploadNewImageFile } from "../../../../../services/blocks/Image/images";
|
||||
import { UploadIcon } from "@radix-ui/react-icons";
|
||||
import { getActivityBlockMediaDirectory } from "@services/media/media";
|
||||
import { useOrg } from "@components/Contexts/OrgContext";
|
||||
import { useCourse } from "@components/Contexts/CourseContext";
|
||||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
||||
import { NodeViewWrapper } from '@tiptap/react'
|
||||
import React, { useEffect } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Resizable } from 're-resizable'
|
||||
import { AlertTriangle, Image, Loader } from 'lucide-react'
|
||||
import { uploadNewImageFile } from '../../../../../services/blocks/Image/images'
|
||||
import { UploadIcon } from '@radix-ui/react-icons'
|
||||
import { getActivityBlockMediaDirectory } from '@services/media/media'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import { useCourse } from '@components/Contexts/CourseContext'
|
||||
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
|
||||
|
||||
function ImageBlockComponent(props: any) {
|
||||
const org = useOrg() as any;
|
||||
const course = useCourse() as any;
|
||||
const editorState = useEditorProvider() as any;
|
||||
|
||||
const isEditable = editorState.isEditable;
|
||||
const [image, setImage] = React.useState(null);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
|
||||
const [imageSize, setImageSize] = React.useState({ width: props.node.attrs.size ? props.node.attrs.size.width : 300 });
|
||||
const fileId = blockObject ? `${blockObject.content.file_id}.${blockObject.content.file_format}` : null;
|
||||
const org = useOrg() as any
|
||||
const course = useCourse() as any
|
||||
const editorState = useEditorProvider() as any
|
||||
|
||||
const isEditable = editorState.isEditable
|
||||
const [image, setImage] = React.useState(null)
|
||||
const [isLoading, setIsLoading] = React.useState(false)
|
||||
const [blockObject, setblockObject] = React.useState(
|
||||
props.node.attrs.blockObject
|
||||
)
|
||||
const [imageSize, setImageSize] = React.useState({
|
||||
width: props.node.attrs.size ? props.node.attrs.size.width : 300,
|
||||
})
|
||||
const fileId = blockObject
|
||||
? `${blockObject.content.file_id}.${blockObject.content.file_format}`
|
||||
: null
|
||||
const handleImageChange = (event: React.ChangeEvent<any>) => {
|
||||
setImage(event.target.files[0]);
|
||||
};
|
||||
setImage(event.target.files[0])
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
let object = await uploadNewImageFile(image, props.extension.options.activity.activity_uuid);
|
||||
setIsLoading(false);
|
||||
setblockObject(object);
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
let object = await uploadNewImageFile(
|
||||
image,
|
||||
props.extension.options.activity.activity_uuid
|
||||
)
|
||||
setIsLoading(false)
|
||||
setblockObject(object)
|
||||
props.updateAttributes({
|
||||
blockObject: object,
|
||||
size: imageSize,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
})
|
||||
}
|
||||
, [course, org]);
|
||||
|
||||
useEffect(() => {}, [course, org])
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="block-image">
|
||||
{!blockObject && isEditable && (
|
||||
<BlockImageWrapper className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2" contentEditable={isEditable}>
|
||||
<BlockImageWrapper
|
||||
className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2"
|
||||
contentEditable={isEditable}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader className="animate-spin animate-pulse text-gray-200" size={50} />
|
||||
<Loader
|
||||
className="animate-spin animate-pulse text-gray-200"
|
||||
size={50}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<Image className="text-gray-200" size={50} />
|
||||
</div>
|
||||
<input className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500" onChange={handleImageChange} type="file" name="" id="" />
|
||||
<button className='p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex' onClick={handleSubmit}><UploadIcon></UploadIcon><p>Submit</p></button>
|
||||
<input
|
||||
className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500"
|
||||
onChange={handleImageChange}
|
||||
type="file"
|
||||
name=""
|
||||
id=""
|
||||
/>
|
||||
<button
|
||||
className="p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
<UploadIcon></UploadIcon>
|
||||
<p>Submit</p>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</BlockImageWrapper>
|
||||
)}
|
||||
{blockObject && (
|
||||
<Resizable defaultSize={{ width: imageSize.width, height: "100%" }}
|
||||
<Resizable
|
||||
defaultSize={{ width: imageSize.width, height: '100%' }}
|
||||
handleStyles={{
|
||||
right: { position: 'unset', width: 7, height: 30, borderRadius: 20, cursor: 'col-resize', backgroundColor: 'black', opacity: '0.3', margin: 'auto', marginLeft: 5 },
|
||||
|
||||
right: {
|
||||
position: 'unset',
|
||||
width: 7,
|
||||
height: 30,
|
||||
borderRadius: 20,
|
||||
cursor: 'col-resize',
|
||||
backgroundColor: 'black',
|
||||
opacity: '0.3',
|
||||
margin: 'auto',
|
||||
marginLeft: 5,
|
||||
},
|
||||
}}
|
||||
style={{
|
||||
margin: 'auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
}}
|
||||
style={{ margin: "auto", display: "flex", justifyContent: "center", alignItems: "center", height: "100%" }}
|
||||
maxWidth={1000}
|
||||
minWidth={200}
|
||||
onResizeStop={(e, direction, ref, d) => {
|
||||
props.updateAttributes({
|
||||
size: {
|
||||
width: imageSize.width + d.width,
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
setImageSize({
|
||||
width: imageSize.width + d.width,
|
||||
});
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
||||
<img
|
||||
src={`${getActivityBlockMediaDirectory(org?.org_uuid,
|
||||
src={`${getActivityBlockMediaDirectory(
|
||||
org?.org_uuid,
|
||||
course?.courseStructure.course_uuid,
|
||||
props.extension.options.activity.activity_uuid,
|
||||
blockObject.block_uuid,
|
||||
blockObject ? fileId : ' ', 'imageBlock')}`}
|
||||
blockObject ? fileId : ' ',
|
||||
'imageBlock'
|
||||
)}`}
|
||||
alt=""
|
||||
className="rounded-lg shadow "
|
||||
/>
|
||||
|
||||
|
||||
</Resizable>
|
||||
)}
|
||||
{isLoading && (
|
||||
|
|
@ -98,29 +139,24 @@ function ImageBlockComponent(props: any) {
|
|||
</div>
|
||||
)}
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default ImageBlockComponent;
|
||||
export default ImageBlockComponent
|
||||
|
||||
const BlockImageWrapper = styled.div`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
|
||||
`;
|
||||
`
|
||||
|
||||
const BlockImage = styled.div`
|
||||
display: flex;
|
||||
|
||||
|
||||
// center
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
|
||||
|
||||
|
||||
`;
|
||||
`
|
||||
|
|
|
|||
|
|
@ -1,35 +1,35 @@
|
|||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { mergeAttributes, Node } from '@tiptap/core'
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react'
|
||||
|
||||
import MathEquationBlockComponent from "./MathEquationBlockComponent";
|
||||
import MathEquationBlockComponent from './MathEquationBlockComponent'
|
||||
|
||||
export default Node.create({
|
||||
name: "blockMathEquation",
|
||||
group: "block",
|
||||
name: 'blockMathEquation',
|
||||
group: 'block',
|
||||
|
||||
atom: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
math_equation: {
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "block-math-equation",
|
||||
tag: 'block-math-equation',
|
||||
},
|
||||
];
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["block-math-equation", mergeAttributes(HTMLAttributes), 0];
|
||||
return ['block-math-equation', mergeAttributes(HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(MathEquationBlockComponent);
|
||||
return ReactNodeViewRenderer(MathEquationBlockComponent)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
import { NodeViewWrapper } from "@tiptap/react";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import "katex/dist/katex.min.css";
|
||||
import { BlockMath } from "react-katex";
|
||||
import { Save } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
||||
import { NodeViewWrapper } from '@tiptap/react'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import { BlockMath } from 'react-katex'
|
||||
import { Save } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
|
||||
|
||||
function MathEquationBlockComponent(props: any) {
|
||||
const [equation, setEquation] = React.useState(props.node.attrs.math_equation);
|
||||
const [isEditing, setIsEditing] = React.useState(true);
|
||||
const editorState = useEditorProvider() as any;
|
||||
const isEditable = editorState.isEditable;
|
||||
const [equation, setEquation] = React.useState(props.node.attrs.math_equation)
|
||||
const [isEditing, setIsEditing] = React.useState(true)
|
||||
const editorState = useEditorProvider() as any
|
||||
const isEditable = editorState.isEditable
|
||||
|
||||
const handleEquationChange = (event: React.ChangeEvent<any>) => {
|
||||
setEquation(event.target.value);
|
||||
setEquation(event.target.value)
|
||||
props.updateAttributes({
|
||||
math_equation: equation,
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const saveEquation = () => {
|
||||
props.updateAttributes({
|
||||
math_equation: equation,
|
||||
});
|
||||
})
|
||||
//setIsEditing(false);
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="block-math-equation">
|
||||
|
|
@ -34,24 +34,38 @@ function MathEquationBlockComponent(props: any) {
|
|||
{isEditing && isEditable && (
|
||||
<>
|
||||
<EditBar>
|
||||
<input value={equation} onChange={handleEquationChange} placeholder="Insert a Math Equation (LaTeX) " type="text" />
|
||||
<input
|
||||
value={equation}
|
||||
onChange={handleEquationChange}
|
||||
placeholder="Insert a Math Equation (LaTeX) "
|
||||
type="text"
|
||||
/>
|
||||
<button className="opacity-1" onClick={() => saveEquation()}>
|
||||
<Save size={15}></Save>
|
||||
</button>
|
||||
</EditBar>
|
||||
<span className="pt-2 text-zinc-500 text-sm">Please refer to this <Link className="text-zinc-900 after:content-['↗']" href="https://katex.org/docs/supported.html" target="_blank"> guide</Link> for supported TeX functions </span>
|
||||
<span className="pt-2 text-zinc-500 text-sm">
|
||||
Please refer to this{' '}
|
||||
<Link
|
||||
className="text-zinc-900 after:content-['↗']"
|
||||
href="https://katex.org/docs/supported.html"
|
||||
target="_blank"
|
||||
>
|
||||
{' '}
|
||||
guide
|
||||
</Link>{' '}
|
||||
for supported TeX functions{' '}
|
||||
</span>
|
||||
</>
|
||||
|
||||
)}
|
||||
</MathEqWrapper>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default MathEquationBlockComponent;
|
||||
export default MathEquationBlockComponent
|
||||
|
||||
const MathEqWrapper = styled.div`
|
||||
`;
|
||||
const MathEqWrapper = styled.div``
|
||||
|
||||
const EditBar = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -82,7 +96,7 @@ const EditBar = styled.div`
|
|||
font-size: 14px;
|
||||
color: #494949;
|
||||
width: 100%;
|
||||
font-family: "DM Sans", sans-serif;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
padding-left: 10px;
|
||||
&:focus {
|
||||
outline: none;
|
||||
|
|
@ -92,4 +106,4 @@ const EditBar = styled.div`
|
|||
color: #49494936;
|
||||
}
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
|
|
|||
|
|
@ -1,21 +1,29 @@
|
|||
import { Extension } from '@tiptap/core';
|
||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
||||
import { Extension } from '@tiptap/core'
|
||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||
|
||||
export const NoTextInput = Extension.create({
|
||||
name: 'noTextInput',
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('noTextInput'),
|
||||
filterTransaction: (transaction) => {
|
||||
// If the transaction is adding text, stop it
|
||||
return !transaction.docChanged || transaction.steps.every((step) => {
|
||||
const { slice } = step.toJSON();
|
||||
return !slice || !slice.content.some((node: { type: string; }) => node.type === 'text');
|
||||
});
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
name: 'noTextInput',
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('noTextInput'),
|
||||
filterTransaction: (transaction) => {
|
||||
// If the transaction is adding text, stop it
|
||||
return (
|
||||
!transaction.docChanged ||
|
||||
transaction.steps.every((step) => {
|
||||
const { slice } = step.toJSON()
|
||||
return (
|
||||
!slice ||
|
||||
!slice.content.some(
|
||||
(node: { type: string }) => node.type === 'text'
|
||||
)
|
||||
)
|
||||
})
|
||||
)
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { mergeAttributes, Node } from '@tiptap/core'
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react'
|
||||
|
||||
import PDFBlockComponent from "./PDFBlockComponent";
|
||||
import PDFBlockComponent from './PDFBlockComponent'
|
||||
|
||||
export default Node.create({
|
||||
name: "blockPDF",
|
||||
group: "block",
|
||||
name: 'blockPDF',
|
||||
group: 'block',
|
||||
|
||||
atom: true,
|
||||
|
||||
|
|
@ -14,22 +14,22 @@ export default Node.create({
|
|||
blockObject: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "block-pdf",
|
||||
tag: 'block-pdf',
|
||||
},
|
||||
];
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["block-pdf", mergeAttributes(HTMLAttributes), 0];
|
||||
return ['block-pdf', mergeAttributes(HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(PDFBlockComponent);
|
||||
return ReactNodeViewRenderer(PDFBlockComponent)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,56 +1,79 @@
|
|||
import { NodeViewWrapper } from "@tiptap/react";
|
||||
import React, { useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
import { AlertTriangle, FileText, Loader } from "lucide-react";
|
||||
import { uploadNewPDFFile } from "../../../../../services/blocks/Pdf/pdf";
|
||||
import { UploadIcon } from "@radix-ui/react-icons";
|
||||
import { getActivityBlockMediaDirectory } from "@services/media/media";
|
||||
import { useOrg } from "@components/Contexts/OrgContext";
|
||||
import { useCourse } from "@components/Contexts/CourseContext";
|
||||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
||||
import { NodeViewWrapper } from '@tiptap/react'
|
||||
import React, { useEffect } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { AlertTriangle, FileText, Loader } from 'lucide-react'
|
||||
import { uploadNewPDFFile } from '../../../../../services/blocks/Pdf/pdf'
|
||||
import { UploadIcon } from '@radix-ui/react-icons'
|
||||
import { getActivityBlockMediaDirectory } from '@services/media/media'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import { useCourse } from '@components/Contexts/CourseContext'
|
||||
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
|
||||
|
||||
function PDFBlockComponent(props: any) {
|
||||
const org = useOrg() as any;
|
||||
const course = useCourse() as any;
|
||||
const [pdf, setPDF] = React.useState(null);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
|
||||
const fileId = blockObject ? `${blockObject.content.file_id}.${blockObject.content.file_format}` : null;
|
||||
const editorState = useEditorProvider() as any;
|
||||
const isEditable = editorState.isEditable;
|
||||
const org = useOrg() as any
|
||||
const course = useCourse() as any
|
||||
const [pdf, setPDF] = React.useState(null)
|
||||
const [isLoading, setIsLoading] = React.useState(false)
|
||||
const [blockObject, setblockObject] = React.useState(
|
||||
props.node.attrs.blockObject
|
||||
)
|
||||
const fileId = blockObject
|
||||
? `${blockObject.content.file_id}.${blockObject.content.file_format}`
|
||||
: null
|
||||
const editorState = useEditorProvider() as any
|
||||
const isEditable = editorState.isEditable
|
||||
|
||||
const handlePDFChange = (event: React.ChangeEvent<any>) => {
|
||||
setPDF(event.target.files[0]);
|
||||
};
|
||||
setPDF(event.target.files[0])
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
let object = await uploadNewPDFFile(pdf, props.extension.options.activity.activity_uuid);
|
||||
setIsLoading(false);
|
||||
setblockObject(object);
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
let object = await uploadNewPDFFile(
|
||||
pdf,
|
||||
props.extension.options.activity.activity_uuid
|
||||
)
|
||||
setIsLoading(false)
|
||||
setblockObject(object)
|
||||
props.updateAttributes({
|
||||
blockObject: object,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
})
|
||||
}
|
||||
, [course, org]);
|
||||
|
||||
useEffect(() => {}, [course, org])
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="block-pdf">
|
||||
{!blockObject && (
|
||||
<BlockPDFWrapper className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2" contentEditable={isEditable}>
|
||||
<BlockPDFWrapper
|
||||
className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2"
|
||||
contentEditable={isEditable}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader className="animate-spin animate-pulse text-gray-200" size={50} />
|
||||
<Loader
|
||||
className="animate-spin animate-pulse text-gray-200"
|
||||
size={50}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<FileText className="text-gray-200" size={50} />
|
||||
</div>
|
||||
<input className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500" onChange={handlePDFChange} type="file" name="" id="" />
|
||||
<button className='p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex' onClick={handleSubmit}><UploadIcon></UploadIcon><p>Submit</p></button>
|
||||
<input
|
||||
className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500"
|
||||
onChange={handlePDFChange}
|
||||
type="file"
|
||||
name=""
|
||||
id=""
|
||||
/>
|
||||
<button
|
||||
className="p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
<UploadIcon></UploadIcon>
|
||||
<p>Submit</p>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</BlockPDFWrapper>
|
||||
|
|
@ -59,11 +82,14 @@ function PDFBlockComponent(props: any) {
|
|||
<BlockPDF>
|
||||
<iframe
|
||||
className="shadow rounded-lg h-96 w-full object-scale-down bg-black"
|
||||
src={`${getActivityBlockMediaDirectory(org?.org_uuid,
|
||||
src={`${getActivityBlockMediaDirectory(
|
||||
org?.org_uuid,
|
||||
course?.courseStructure.course_uuid,
|
||||
props.extension.options.activity.activity_uuid,
|
||||
blockObject.block_uuid,
|
||||
blockObject ? fileId : ' ', 'pdfBlock')}`}
|
||||
blockObject ? fileId : ' ',
|
||||
'pdfBlock'
|
||||
)}`}
|
||||
/>
|
||||
</BlockPDF>
|
||||
)}
|
||||
|
|
@ -73,19 +99,18 @@ function PDFBlockComponent(props: any) {
|
|||
</div>
|
||||
)}
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default PDFBlockComponent;
|
||||
export default PDFBlockComponent
|
||||
|
||||
const BlockPDFWrapper = styled.div`
|
||||
|
||||
// center
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
`;
|
||||
`
|
||||
|
||||
const BlockPDF = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -97,5 +122,5 @@ const BlockPDF = styled.div`
|
|||
// cover
|
||||
object-fit: cover;
|
||||
}
|
||||
`;
|
||||
const PDFNotFound = styled.div``;
|
||||
`
|
||||
const PDFNotFound = styled.div``
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { mergeAttributes, Node } from '@tiptap/core'
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react'
|
||||
|
||||
import QuizBlockComponent from "./QuizBlockComponent";
|
||||
import QuizBlockComponent from './QuizBlockComponent'
|
||||
|
||||
export default Node.create({
|
||||
name: "blockQuiz",
|
||||
group: "block",
|
||||
name: 'blockQuiz',
|
||||
group: 'block',
|
||||
atom: true,
|
||||
|
||||
addAttributes() {
|
||||
|
|
@ -16,22 +16,22 @@ export default Node.create({
|
|||
questions: {
|
||||
default: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "block-quiz",
|
||||
tag: 'block-quiz',
|
||||
},
|
||||
];
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["block-quiz", mergeAttributes(HTMLAttributes), 0];
|
||||
return ['block-quiz', mergeAttributes(HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(QuizBlockComponent);
|
||||
return ReactNodeViewRenderer(QuizBlockComponent)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,188 +1,206 @@
|
|||
import { NodeViewWrapper } from "@tiptap/react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { NodeViewWrapper } from '@tiptap/react'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import React from "react";
|
||||
import { BadgeHelp, Check, Minus, Plus, RefreshCcw } from "lucide-react";
|
||||
import ReactConfetti from "react-confetti";
|
||||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
||||
import React from 'react'
|
||||
import { BadgeHelp, Check, Minus, Plus, RefreshCcw } from 'lucide-react'
|
||||
import ReactConfetti from 'react-confetti'
|
||||
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
|
||||
|
||||
interface Answer {
|
||||
answer_id: string;
|
||||
answer: string;
|
||||
correct: boolean;
|
||||
answer_id: string
|
||||
answer: string
|
||||
correct: boolean
|
||||
}
|
||||
interface Question {
|
||||
question_id: string;
|
||||
question: string;
|
||||
type: "multiple_choice" | 'custom_answer'
|
||||
answers: Answer[];
|
||||
question_id: string
|
||||
question: string
|
||||
type: 'multiple_choice' | 'custom_answer'
|
||||
answers: Answer[]
|
||||
}
|
||||
|
||||
function QuizBlockComponent(props: any) {
|
||||
const [questions, setQuestions] = React.useState(props.node.attrs.questions) as [Question[], any];
|
||||
const [userAnswers, setUserAnswers] = React.useState([]) as [any[], any];
|
||||
const [submitted, setSubmitted] = React.useState(false) as [boolean, any];
|
||||
const [submissionMessage, setSubmissionMessage] = React.useState("") as [string, any];
|
||||
const editorState = useEditorProvider() as any;
|
||||
const isEditable = editorState.isEditable;
|
||||
const [questions, setQuestions] = React.useState(
|
||||
props.node.attrs.questions
|
||||
) as [Question[], any]
|
||||
const [userAnswers, setUserAnswers] = React.useState([]) as [any[], any]
|
||||
const [submitted, setSubmitted] = React.useState(false) as [boolean, any]
|
||||
const [submissionMessage, setSubmissionMessage] = React.useState('') as [
|
||||
string,
|
||||
any,
|
||||
]
|
||||
const editorState = useEditorProvider() as any
|
||||
const isEditable = editorState.isEditable
|
||||
|
||||
const handleAnswerClick = (question_id: string, answer_id: string) => {
|
||||
// if the quiz is submitted, do nothing
|
||||
if (submitted) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const userAnswer = {
|
||||
question_id: question_id,
|
||||
answer_id: answer_id
|
||||
answer_id: answer_id,
|
||||
}
|
||||
const newAnswers = [...userAnswers, userAnswer];
|
||||
const newAnswers = [...userAnswers, userAnswer]
|
||||
|
||||
// only accept one answer per question
|
||||
const filteredAnswers = newAnswers.filter((answer: any) => answer.question_id !== question_id);
|
||||
|
||||
setUserAnswers([...filteredAnswers, userAnswer]);
|
||||
const filteredAnswers = newAnswers.filter(
|
||||
(answer: any) => answer.question_id !== question_id
|
||||
)
|
||||
|
||||
setUserAnswers([...filteredAnswers, userAnswer])
|
||||
}
|
||||
|
||||
const refreshUserSubmission = () => {
|
||||
setUserAnswers([]);
|
||||
setSubmitted(false);
|
||||
setUserAnswers([])
|
||||
setSubmitted(false)
|
||||
}
|
||||
|
||||
const handleUserSubmission = () => {
|
||||
|
||||
if (userAnswers.length === 0) {
|
||||
setSubmissionMessage("Please answer at least one question!");
|
||||
return;
|
||||
setSubmissionMessage('Please answer at least one question!')
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitted(true);
|
||||
setSubmitted(true)
|
||||
|
||||
// check if all submitted answers are correct
|
||||
const correctAnswers = questions.map((question: Question) => {
|
||||
const correctAnswer: any = question.answers.find((answer: Answer) => answer.correct);
|
||||
const userAnswer = userAnswers.find((userAnswer: any) => userAnswer.question_id === question.question_id);
|
||||
const correctAnswer: any = question.answers.find(
|
||||
(answer: Answer) => answer.correct
|
||||
)
|
||||
const userAnswer = userAnswers.find(
|
||||
(userAnswer: any) => userAnswer.question_id === question.question_id
|
||||
)
|
||||
if (correctAnswer.answer_id === userAnswer.answer_id) {
|
||||
return true;
|
||||
return true
|
||||
} else {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// check if all answers are correct
|
||||
const allCorrect = correctAnswers.every((answer: boolean) => answer === true);
|
||||
const allCorrect = correctAnswers.every(
|
||||
(answer: boolean) => answer === true
|
||||
)
|
||||
|
||||
if (allCorrect) {
|
||||
setSubmissionMessage("All answers are correct!");
|
||||
console.log("All answers are correct!");
|
||||
setSubmissionMessage('All answers are correct!')
|
||||
console.log('All answers are correct!')
|
||||
} else {
|
||||
setSubmissionMessage('Some answers are incorrect!')
|
||||
console.log('Some answers are incorrect!')
|
||||
}
|
||||
else {
|
||||
setSubmissionMessage("Some answers are incorrect!");
|
||||
console.log("Some answers are incorrect!");
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
const getAnswerID = (answerIndex: number, questionId : string) => {
|
||||
const alphabet = Array.from({ length: 26 }, (_, i) => String.fromCharCode('A'.charCodeAt(0) + i));
|
||||
let alphabetID = alphabet[answerIndex];
|
||||
const getAnswerID = (answerIndex: number, questionId: string) => {
|
||||
const alphabet = Array.from({ length: 26 }, (_, i) =>
|
||||
String.fromCharCode('A'.charCodeAt(0) + i)
|
||||
)
|
||||
let alphabetID = alphabet[answerIndex]
|
||||
|
||||
// Get question index
|
||||
const questionIndex = questions.findIndex((question: Question) => question.question_id === questionId);
|
||||
let questionID = questionIndex + 1;
|
||||
const questionIndex = questions.findIndex(
|
||||
(question: Question) => question.question_id === questionId
|
||||
)
|
||||
let questionID = questionIndex + 1
|
||||
|
||||
return `${alphabetID}`;
|
||||
return `${alphabetID}`
|
||||
}
|
||||
|
||||
const saveQuestions = (questions: any) => {
|
||||
props.updateAttributes({
|
||||
questions: questions,
|
||||
});
|
||||
setQuestions(questions);
|
||||
|
||||
};
|
||||
})
|
||||
setQuestions(questions)
|
||||
}
|
||||
const addSampleQuestion = () => {
|
||||
const newQuestion = {
|
||||
question_id: uuidv4(),
|
||||
question: "",
|
||||
type: "multiple_choice",
|
||||
question: '',
|
||||
type: 'multiple_choice',
|
||||
answers: [
|
||||
{
|
||||
answer_id: uuidv4(),
|
||||
answer: "",
|
||||
correct: false
|
||||
answer: '',
|
||||
correct: false,
|
||||
},
|
||||
]
|
||||
],
|
||||
}
|
||||
setQuestions([...questions, newQuestion]);
|
||||
setQuestions([...questions, newQuestion])
|
||||
}
|
||||
|
||||
const addAnswer = (question_id: string) => {
|
||||
const newAnswer = {
|
||||
answer_id: uuidv4(),
|
||||
answer: "",
|
||||
correct: false
|
||||
answer: '',
|
||||
correct: false,
|
||||
}
|
||||
|
||||
// check if there is already more thqn 5 answers
|
||||
const question: any = questions.find((question: Question) => question.question_id === question_id);
|
||||
const question: any = questions.find(
|
||||
(question: Question) => question.question_id === question_id
|
||||
)
|
||||
if (question.answers.length >= 5) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
const newQuestions = questions.map((question: Question) => {
|
||||
if (question.question_id === question_id) {
|
||||
question.answers.push(newAnswer);
|
||||
question.answers.push(newAnswer)
|
||||
}
|
||||
return question;
|
||||
});
|
||||
return question
|
||||
})
|
||||
|
||||
saveQuestions(newQuestions);
|
||||
saveQuestions(newQuestions)
|
||||
}
|
||||
|
||||
const changeAnswerValue = (question_id: string, answer_id: string, value: string) => {
|
||||
const changeAnswerValue = (
|
||||
question_id: string,
|
||||
answer_id: string,
|
||||
value: string
|
||||
) => {
|
||||
const newQuestions = questions.map((question: Question) => {
|
||||
if (question.question_id === question_id) {
|
||||
question.answers.map((answer: Answer) => {
|
||||
if (answer.answer_id === answer_id) {
|
||||
answer.answer = value;
|
||||
answer.answer = value
|
||||
}
|
||||
return answer;
|
||||
});
|
||||
return answer
|
||||
})
|
||||
}
|
||||
return question;
|
||||
});
|
||||
saveQuestions(newQuestions);
|
||||
return question
|
||||
})
|
||||
saveQuestions(newQuestions)
|
||||
}
|
||||
|
||||
const changeQuestionValue = (question_id: string, value: string) => {
|
||||
const newQuestions = questions.map((question: Question) => {
|
||||
if (question.question_id === question_id) {
|
||||
question.question = value;
|
||||
question.question = value
|
||||
}
|
||||
return question;
|
||||
});
|
||||
saveQuestions(newQuestions);
|
||||
return question
|
||||
})
|
||||
saveQuestions(newQuestions)
|
||||
}
|
||||
|
||||
const deleteQuestion = (question_id: string) => {
|
||||
const newQuestions = questions.filter((question: Question) => question.question_id !== question_id);
|
||||
saveQuestions(newQuestions);
|
||||
const newQuestions = questions.filter(
|
||||
(question: Question) => question.question_id !== question_id
|
||||
)
|
||||
saveQuestions(newQuestions)
|
||||
}
|
||||
|
||||
const deleteAnswer = (question_id: string, answer_id: string) => {
|
||||
const newQuestions = questions.map((question: Question) => {
|
||||
if (question.question_id === question_id) {
|
||||
question.answers = question.answers.filter((answer: Answer) => answer.answer_id !== answer_id);
|
||||
question.answers = question.answers.filter(
|
||||
(answer: Answer) => answer.answer_id !== answer_id
|
||||
)
|
||||
}
|
||||
return question;
|
||||
});
|
||||
saveQuestions(newQuestions);
|
||||
return question
|
||||
})
|
||||
saveQuestions(newQuestions)
|
||||
}
|
||||
|
||||
const markAnswerCorrect = (question_id: string, answer_id: string) => {
|
||||
|
|
@ -190,54 +208,68 @@ function QuizBlockComponent(props: any) {
|
|||
if (question.question_id === question_id) {
|
||||
question.answers.map((answer: Answer) => {
|
||||
if (answer.answer_id === answer_id) {
|
||||
answer.correct = true;
|
||||
answer.correct = true
|
||||
} else {
|
||||
answer.correct = false;
|
||||
answer.correct = false
|
||||
}
|
||||
|
||||
return answer;
|
||||
});
|
||||
return answer
|
||||
})
|
||||
}
|
||||
return question;
|
||||
});
|
||||
saveQuestions(newQuestions);
|
||||
return question
|
||||
})
|
||||
saveQuestions(newQuestions)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="block-quiz">
|
||||
|
||||
<div
|
||||
//style={{ background: "radial-gradient(152.15% 150.08% at 56.45% -6.67%, rgba(180, 255, 250, 0.10) 5.53%, rgba(202, 201, 255, 0.10) 66.76%)" }}
|
||||
className="rounded-xl px-5 py-2 bg-slate-100 transition-all ease-linear"
|
||||
>
|
||||
<div className="flex space-x-2 pt-1 items-center text-sm overflow-hidden">
|
||||
{(submitted && submissionMessage == "All answers are correct!") &&
|
||||
{submitted && submissionMessage == 'All answers are correct!' && (
|
||||
<ReactConfetti
|
||||
numberOfPieces={submitted ? 1400 : 0}
|
||||
recycle={false}
|
||||
className="w-full h-screen"
|
||||
/>
|
||||
}
|
||||
)}
|
||||
<div className="flex space-x-2 items-center text-sm">
|
||||
<BadgeHelp className='text-slate-400' size={15} />
|
||||
<p className="uppercase tracking-widest text-xs font-bold py-1 text-slate-400">Quiz</p>
|
||||
<BadgeHelp className="text-slate-400" size={15} />
|
||||
<p className="uppercase tracking-widest text-xs font-bold py-1 text-slate-400">
|
||||
Quiz
|
||||
</p>
|
||||
</div>
|
||||
<div className="grow flex items-center justify-center">
|
||||
|
||||
</div>
|
||||
{isEditable ?
|
||||
<div className="grow flex items-center justify-center"></div>
|
||||
{isEditable ? (
|
||||
<div>
|
||||
<button onClick={addSampleQuestion} className="bg-slate-200 hover:bg-slate-300 text-slate-800 font-bold py-1 px-2 rounded-lg text-xs">Add Question</button>
|
||||
<button
|
||||
onClick={addSampleQuestion}
|
||||
className="bg-slate-200 hover:bg-slate-300 text-slate-800 font-bold py-1 px-2 rounded-lg text-xs"
|
||||
>
|
||||
Add Question
|
||||
</button>
|
||||
</div>
|
||||
:
|
||||
) : (
|
||||
<div className="flex space-x-1 items-center">
|
||||
<div onClick={() => refreshUserSubmission()} className="cursor-pointer px-2">
|
||||
<RefreshCcw className='text-slate-400 cursor-pointer' size={15} />
|
||||
<div
|
||||
onClick={() => refreshUserSubmission()}
|
||||
className="cursor-pointer px-2"
|
||||
>
|
||||
<RefreshCcw
|
||||
className="text-slate-400 cursor-pointer"
|
||||
size={15}
|
||||
/>
|
||||
</div>
|
||||
<button onClick={() => handleUserSubmission()} className="bg-slate-200 hover:bg-slate-300 text-slate-800 font-bold py-1 px-2 rounded-lg text-xs">Submit</button>
|
||||
<button
|
||||
onClick={() => handleUserSubmission()}
|
||||
className="bg-slate-200 hover:bg-slate-300 text-slate-800 font-bold py-1 px-2 rounded-lg text-xs"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{questions.map((question: Question) => (
|
||||
|
|
@ -245,20 +277,32 @@ function QuizBlockComponent(props: any) {
|
|||
<div className="question">
|
||||
<div className="flex space-x-2 items-center">
|
||||
<div className="flex-grow">
|
||||
{isEditable ?
|
||||
<input value={question.question} placeholder="Your Question" onChange={(e) => changeQuestionValue(question.question_id, e.target.value)} className="text-slate-800 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-md font-bold w-full"></input>
|
||||
:
|
||||
<p className="text-slate-800 bg-[#00008b00] rounded-md text-md font-bold w-full">{question.question}</p>
|
||||
}
|
||||
{isEditable ? (
|
||||
<input
|
||||
value={question.question}
|
||||
placeholder="Your Question"
|
||||
onChange={(e) =>
|
||||
changeQuestionValue(
|
||||
question.question_id,
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
className="text-slate-800 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-md font-bold w-full"
|
||||
></input>
|
||||
) : (
|
||||
<p className="text-slate-800 bg-[#00008b00] rounded-md text-md font-bold w-full">
|
||||
{question.question}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{isEditable &&
|
||||
{isEditable && (
|
||||
<div
|
||||
onClick={() => deleteQuestion(question.question_id)}
|
||||
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer">
|
||||
<Minus
|
||||
className="mx-auto text-slate-400" size={12} />
|
||||
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer"
|
||||
>
|
||||
<Minus className="mx-auto text-slate-400" size={12} />
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div className="answers flex py-2 space-x-3">
|
||||
{question.answers.map((answer: Answer) => (
|
||||
|
|
@ -266,60 +310,119 @@ function QuizBlockComponent(props: any) {
|
|||
key={answer.answer_id}
|
||||
className={twMerge(
|
||||
'outline outline-3 pr-2 shadow w-full flex items-center space-x-2 h-[30px] bg-opacity-50 hover:bg-opacity-100 hover:shadow-md rounded-s rounded-lg bg-white text-sm hover:scale-105 active:scale-110 duration-150 cursor-pointer ease-linear',
|
||||
answer.correct && isEditable ? 'outline-lime-300' : 'outline-white',
|
||||
userAnswers.find((userAnswer: any) => (userAnswer.question_id === question.question_id && userAnswer.answer_id === answer.answer_id) && !isEditable) ? 'outline-slate-300' : '',
|
||||
(submitted && answer.correct) ? 'outline-lime-300 text-lime' : '',
|
||||
(submitted && !answer.correct) && userAnswers.find((userAnswer: any) => userAnswer.question_id === question.question_id && userAnswer.answer_id === answer.answer_id) ? 'outline-red-400' : '',
|
||||
)
|
||||
answer.correct && isEditable
|
||||
? 'outline-lime-300'
|
||||
: 'outline-white',
|
||||
userAnswers.find(
|
||||
(userAnswer: any) =>
|
||||
userAnswer.question_id === question.question_id &&
|
||||
userAnswer.answer_id === answer.answer_id &&
|
||||
!isEditable
|
||||
)
|
||||
? 'outline-slate-300'
|
||||
: '',
|
||||
submitted && answer.correct
|
||||
? 'outline-lime-300 text-lime'
|
||||
: '',
|
||||
submitted &&
|
||||
!answer.correct &&
|
||||
userAnswers.find(
|
||||
(userAnswer: any) =>
|
||||
userAnswer.question_id === question.question_id &&
|
||||
userAnswer.answer_id === answer.answer_id
|
||||
)
|
||||
? 'outline-red-400'
|
||||
: ''
|
||||
)}
|
||||
onClick={() =>
|
||||
handleAnswerClick(question.question_id, answer.answer_id)
|
||||
}
|
||||
onClick={() => handleAnswerClick(question.question_id, answer.answer_id)}
|
||||
>
|
||||
<div className={twMerge(
|
||||
"bg-white font-bold text-base flex items-center h-full w-[40px] rounded-l-md text-slate-800",
|
||||
answer.correct && isEditable ? 'bg-lime-300 text-lime-800 outline-none' : 'bg-white',
|
||||
(submitted && answer.correct) ? 'bg-lime-300 text-lime-800 outline-none' : '',
|
||||
(submitted && !answer.correct) && userAnswers.find((userAnswer: any) => userAnswer.question_id === question.question_id && userAnswer.answer_id === answer.answer_id) ? 'bg-red-400 text-red-800 outline-none' : '',
|
||||
)}>
|
||||
<p className="mx-auto font-bold text-sm ">{getAnswerID(question.answers.indexOf(answer),question.question_id)}</p>
|
||||
<div
|
||||
className={twMerge(
|
||||
'bg-white font-bold text-base flex items-center h-full w-[40px] rounded-l-md text-slate-800',
|
||||
answer.correct && isEditable
|
||||
? 'bg-lime-300 text-lime-800 outline-none'
|
||||
: 'bg-white',
|
||||
submitted && answer.correct
|
||||
? 'bg-lime-300 text-lime-800 outline-none'
|
||||
: '',
|
||||
submitted &&
|
||||
!answer.correct &&
|
||||
userAnswers.find(
|
||||
(userAnswer: any) =>
|
||||
userAnswer.question_id === question.question_id &&
|
||||
userAnswer.answer_id === answer.answer_id
|
||||
)
|
||||
? 'bg-red-400 text-red-800 outline-none'
|
||||
: ''
|
||||
)}
|
||||
>
|
||||
<p className="mx-auto font-bold text-sm ">
|
||||
{getAnswerID(
|
||||
question.answers.indexOf(answer),
|
||||
question.question_id
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{isEditable ?
|
||||
<input value={answer.answer} onChange={(e) => changeAnswerValue(question.question_id, answer.answer_id, e.target.value)} placeholder="Answer" className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold"></input>
|
||||
:
|
||||
<p className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] rounded-md ext-sm font-bold">{answer.answer}</p>
|
||||
}
|
||||
{isEditable &&
|
||||
{isEditable ? (
|
||||
<input
|
||||
value={answer.answer}
|
||||
onChange={(e) =>
|
||||
changeAnswerValue(
|
||||
question.question_id,
|
||||
answer.answer_id,
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
placeholder="Answer"
|
||||
className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold"
|
||||
></input>
|
||||
) : (
|
||||
<p className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] rounded-md ext-sm font-bold">
|
||||
{answer.answer}
|
||||
</p>
|
||||
)}
|
||||
{isEditable && (
|
||||
<div className="flex space-x-1 items-center">
|
||||
<div
|
||||
onClick={() => markAnswerCorrect(question.question_id, answer.answer_id)}
|
||||
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-lime-300 hover:bg-lime-400 transition-all ease-linear text-sm cursor-pointer ">
|
||||
<Check
|
||||
className="mx-auto text-lime-800" size={12} />
|
||||
onClick={() =>
|
||||
markAnswerCorrect(
|
||||
question.question_id,
|
||||
answer.answer_id
|
||||
)
|
||||
}
|
||||
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-lime-300 hover:bg-lime-400 transition-all ease-linear text-sm cursor-pointer "
|
||||
>
|
||||
<Check className="mx-auto text-lime-800" size={12} />
|
||||
</div>
|
||||
<div
|
||||
onClick={() => deleteAnswer(question.question_id, answer.answer_id)}
|
||||
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer">
|
||||
<Minus
|
||||
className="mx-auto text-slate-400" size={12} />
|
||||
onClick={() =>
|
||||
deleteAnswer(question.question_id, answer.answer_id)
|
||||
}
|
||||
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer"
|
||||
>
|
||||
<Minus className="mx-auto text-slate-400" size={12} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{isEditable &&
|
||||
<div onClick={() => addAnswer(question.question_id)} className="outline outline-3 w-[30px] flex-none flex items-center h-[30px] outline-white hover:bg-opacity-100 hover:shadow-md rounded-lg bg-white text-sm hover:scale-105 active:scale-110 duration-150 cursor-pointer ease-linear">
|
||||
{isEditable && (
|
||||
<div
|
||||
onClick={() => addAnswer(question.question_id)}
|
||||
className="outline outline-3 w-[30px] flex-none flex items-center h-[30px] outline-white hover:bg-opacity-100 hover:shadow-md rounded-lg bg-white text-sm hover:scale-105 active:scale-110 duration-150 cursor-pointer ease-linear"
|
||||
>
|
||||
<Plus className="mx-auto text-slate-800" size={15} />
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default QuizBlockComponent;
|
||||
export default QuizBlockComponent
|
||||
|
|
|
|||
|
|
@ -1,34 +1,34 @@
|
|||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { mergeAttributes, Node } from '@tiptap/core'
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react'
|
||||
|
||||
import VideoBlockComponent from "./VideoBlockComponent";
|
||||
import VideoBlockComponent from './VideoBlockComponent'
|
||||
|
||||
export default Node.create({
|
||||
name: "blockVideo",
|
||||
group: "block",
|
||||
name: 'blockVideo',
|
||||
group: 'block',
|
||||
atom: true,
|
||||
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
blockObject: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "block-video",
|
||||
tag: 'block-video',
|
||||
},
|
||||
];
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["block-video", mergeAttributes(HTMLAttributes), 0];
|
||||
return ['block-video', mergeAttributes(HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(VideoBlockComponent);
|
||||
return ReactNodeViewRenderer(VideoBlockComponent)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,56 +1,79 @@
|
|||
import { NodeViewWrapper } from "@tiptap/react";
|
||||
import { Loader, Video } from "lucide-react";
|
||||
import React, { useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
import { uploadNewVideoFile } from "../../../../../services/blocks/Video/video";
|
||||
import { getActivityBlockMediaDirectory } from "@services/media/media";
|
||||
import { UploadIcon } from "@radix-ui/react-icons";
|
||||
import { useOrg } from "@components/Contexts/OrgContext";
|
||||
import { useCourse } from "@components/Contexts/CourseContext";
|
||||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
||||
import { NodeViewWrapper } from '@tiptap/react'
|
||||
import { Loader, Video } from 'lucide-react'
|
||||
import React, { useEffect } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { uploadNewVideoFile } from '../../../../../services/blocks/Video/video'
|
||||
import { getActivityBlockMediaDirectory } from '@services/media/media'
|
||||
import { UploadIcon } from '@radix-ui/react-icons'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import { useCourse } from '@components/Contexts/CourseContext'
|
||||
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
|
||||
|
||||
function VideoBlockComponents(props: any) {
|
||||
const org = useOrg() as any;
|
||||
const course = useCourse() as any;
|
||||
const editorState = useEditorProvider() as any;
|
||||
const isEditable = editorState.isEditable;
|
||||
const [video, setVideo] = React.useState(null);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
|
||||
const fileId = blockObject ? `${blockObject.content.file_id}.${blockObject.content.file_format}` : null;
|
||||
const org = useOrg() as any
|
||||
const course = useCourse() as any
|
||||
const editorState = useEditorProvider() as any
|
||||
const isEditable = editorState.isEditable
|
||||
const [video, setVideo] = React.useState(null)
|
||||
const [isLoading, setIsLoading] = React.useState(false)
|
||||
const [blockObject, setblockObject] = React.useState(
|
||||
props.node.attrs.blockObject
|
||||
)
|
||||
const fileId = blockObject
|
||||
? `${blockObject.content.file_id}.${blockObject.content.file_format}`
|
||||
: null
|
||||
|
||||
const handleVideoChange = (event: React.ChangeEvent<any>) => {
|
||||
setVideo(event.target.files[0]);
|
||||
};
|
||||
setVideo(event.target.files[0])
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
let object = await uploadNewVideoFile(video, props.extension.options.activity.activity_uuid);
|
||||
setIsLoading(false);
|
||||
setblockObject(object);
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
let object = await uploadNewVideoFile(
|
||||
video,
|
||||
props.extension.options.activity.activity_uuid
|
||||
)
|
||||
setIsLoading(false)
|
||||
setblockObject(object)
|
||||
props.updateAttributes({
|
||||
blockObject: object,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
})
|
||||
}
|
||||
, [course, org]);
|
||||
|
||||
useEffect(() => {}, [course, org])
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="block-video">
|
||||
{!blockObject && (
|
||||
<BlockVideoWrapper className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2" contentEditable={isEditable}>
|
||||
<BlockVideoWrapper
|
||||
className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2"
|
||||
contentEditable={isEditable}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader className="animate-spin animate-pulse text-gray-200" size={50} />
|
||||
<Loader
|
||||
className="animate-spin animate-pulse text-gray-200"
|
||||
size={50}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<Video className="text-gray-200" size={50} />
|
||||
</div>
|
||||
<input className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500" onChange={handleVideoChange} type="file" name="" id="" />
|
||||
<button className='p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex' onClick={handleSubmit}><UploadIcon></UploadIcon><p>Submit</p></button>
|
||||
<input
|
||||
className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500"
|
||||
onChange={handleVideoChange}
|
||||
type="file"
|
||||
name=""
|
||||
id=""
|
||||
/>
|
||||
<button
|
||||
className="p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
<UploadIcon></UploadIcon>
|
||||
<p>Submit</p>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</BlockVideoWrapper>
|
||||
|
|
@ -60,31 +83,33 @@ function VideoBlockComponents(props: any) {
|
|||
<video
|
||||
controls
|
||||
className="rounded-lg shadow h-96 w-full object-scale-down bg-black"
|
||||
src={`${getActivityBlockMediaDirectory(org?.org_uuid,
|
||||
src={`${getActivityBlockMediaDirectory(
|
||||
org?.org_uuid,
|
||||
course?.courseStructure.course_uuid,
|
||||
props.extension.options.activity.activity_uuid,
|
||||
blockObject.block_uuid,
|
||||
blockObject ? fileId : ' ', 'videoBlock')}`}
|
||||
blockObject ? fileId : ' ',
|
||||
'videoBlock'
|
||||
)}`}
|
||||
></video>
|
||||
</BlockVideo>
|
||||
)}
|
||||
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
)
|
||||
}
|
||||
const BlockVideoWrapper = styled.div`
|
||||
|
||||
//border: ${(props) => (props.contentEditable ? "2px dashed #713f1117" : "none")};
|
||||
//border: ${(props) =>
|
||||
props.contentEditable ? '2px dashed #713f1117' : 'none'};
|
||||
|
||||
// center
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
`;
|
||||
`
|
||||
|
||||
const BlockVideo = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
export default VideoBlockComponents;
|
||||
`
|
||||
export default VideoBlockComponents
|
||||
|
|
|
|||
|
|
@ -1,26 +1,43 @@
|
|||
import styled from "styled-components";
|
||||
import { FontBoldIcon, FontItalicIcon, StrikethroughIcon, ArrowLeftIcon, ArrowRightIcon, DividerVerticalIcon, ListBulletIcon } from "@radix-ui/react-icons";
|
||||
import { AlertCircle, AlertTriangle, BadgeHelp, Code, FileText, ImagePlus, Sigma, Video, Youtube } from "lucide-react";
|
||||
import ToolTip from "@components/StyledElements/Tooltip/Tooltip";
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
FontBoldIcon,
|
||||
FontItalicIcon,
|
||||
StrikethroughIcon,
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
DividerVerticalIcon,
|
||||
ListBulletIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
import {
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
BadgeHelp,
|
||||
Code,
|
||||
FileText,
|
||||
ImagePlus,
|
||||
Sigma,
|
||||
Video,
|
||||
Youtube,
|
||||
} from 'lucide-react'
|
||||
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
|
||||
|
||||
export const ToolbarButtons = ({ editor, props }: any) => {
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// YouTube extension
|
||||
const addYoutubeVideo = () => {
|
||||
const url = prompt("Enter YouTube URL");
|
||||
const url = prompt('Enter YouTube URL')
|
||||
|
||||
if (url) {
|
||||
editor.commands.setYoutubeVideo({
|
||||
src: url,
|
||||
width: 640,
|
||||
height: 480,
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolButtonsWrapper>
|
||||
|
|
@ -30,16 +47,28 @@ export const ToolbarButtons = ({ editor, props }: any) => {
|
|||
<ToolBtn onClick={() => editor.chain().focus().redo().run()}>
|
||||
<ArrowRightIcon />
|
||||
</ToolBtn>
|
||||
<ToolBtn onClick={() => editor.chain().focus().toggleBold().run()} className={editor.isActive("bold") ? "is-active" : ""}>
|
||||
<ToolBtn
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
className={editor.isActive('bold') ? 'is-active' : ''}
|
||||
>
|
||||
<FontBoldIcon />
|
||||
</ToolBtn>
|
||||
<ToolBtn onClick={() => editor.chain().focus().toggleItalic().run()} className={editor.isActive("italic") ? "is-active" : ""}>
|
||||
<ToolBtn
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
className={editor.isActive('italic') ? 'is-active' : ''}
|
||||
>
|
||||
<FontItalicIcon />
|
||||
</ToolBtn>
|
||||
<ToolBtn onClick={() => editor.chain().focus().toggleStrike().run()} className={editor.isActive("strike") ? "is-active" : ""}>
|
||||
<ToolBtn
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
className={editor.isActive('strike') ? 'is-active' : ''}
|
||||
>
|
||||
<StrikethroughIcon />
|
||||
</ToolBtn>
|
||||
<ToolBtn onClick={() => editor.chain().focus().toggleOrderedList().run()} className={editor.isActive('orderedList') ? 'is-active' : ''}>
|
||||
<ToolBtn
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
className={editor.isActive('orderedList') ? 'is-active' : ''}
|
||||
>
|
||||
<ListBulletIcon />
|
||||
</ToolBtn>
|
||||
<ToolSelect
|
||||
|
|
@ -59,25 +88,33 @@ export const ToolbarButtons = ({ editor, props }: any) => {
|
|||
<option value="6">Heading 6</option>
|
||||
</ToolSelect>
|
||||
{/* TODO: fix this : toggling only works one-way */}
|
||||
<DividerVerticalIcon style={{ marginTop: "auto", marginBottom: "auto", color: "grey" }} />
|
||||
<ToolTip content={"Info Callout"}>
|
||||
<ToolBtn onClick={() => editor.chain().focus().toggleNode("calloutInfo").run()}>
|
||||
<DividerVerticalIcon
|
||||
style={{ marginTop: 'auto', marginBottom: 'auto', color: 'grey' }}
|
||||
/>
|
||||
<ToolTip content={'Info Callout'}>
|
||||
<ToolBtn
|
||||
onClick={() => editor.chain().focus().toggleNode('calloutInfo').run()}
|
||||
>
|
||||
<AlertCircle size={15} />
|
||||
</ToolBtn>
|
||||
</ToolTip>
|
||||
<ToolTip content={"Warning Callout"}>
|
||||
<ToolBtn onClick={() => editor.chain().focus().toggleNode("calloutWarning").run()}>
|
||||
<ToolTip content={'Warning Callout'}>
|
||||
<ToolBtn
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleNode('calloutWarning').run()
|
||||
}
|
||||
>
|
||||
<AlertTriangle size={15} />
|
||||
</ToolBtn>
|
||||
</ToolTip>
|
||||
<ToolTip content={"Image"}>
|
||||
<ToolTip content={'Image'}>
|
||||
<ToolBtn
|
||||
onClick={() =>
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContent({
|
||||
type: "blockImage",
|
||||
type: 'blockImage',
|
||||
})
|
||||
.run()
|
||||
}
|
||||
|
|
@ -85,15 +122,14 @@ export const ToolbarButtons = ({ editor, props }: any) => {
|
|||
<ImagePlus size={15} />
|
||||
</ToolBtn>
|
||||
</ToolTip>
|
||||
<ToolTip
|
||||
content={"Video"}>
|
||||
<ToolTip content={'Video'}>
|
||||
<ToolBtn
|
||||
onClick={() =>
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContent({
|
||||
type: "blockVideo",
|
||||
type: 'blockVideo',
|
||||
})
|
||||
.run()
|
||||
}
|
||||
|
|
@ -101,19 +137,19 @@ export const ToolbarButtons = ({ editor, props }: any) => {
|
|||
<Video size={15} />
|
||||
</ToolBtn>
|
||||
</ToolTip>
|
||||
<ToolTip content={"YouTube video"}>
|
||||
<ToolTip content={'YouTube video'}>
|
||||
<ToolBtn onClick={() => addYoutubeVideo()}>
|
||||
<Youtube size={15} />
|
||||
</ToolBtn>
|
||||
</ToolTip>
|
||||
<ToolTip content={"Math Equation (LaTeX)"}>
|
||||
<ToolTip content={'Math Equation (LaTeX)'}>
|
||||
<ToolBtn
|
||||
onClick={() =>
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContent({
|
||||
type: "blockMathEquation",
|
||||
type: 'blockMathEquation',
|
||||
})
|
||||
.run()
|
||||
}
|
||||
|
|
@ -121,14 +157,14 @@ export const ToolbarButtons = ({ editor, props }: any) => {
|
|||
<Sigma size={15} />
|
||||
</ToolBtn>
|
||||
</ToolTip>
|
||||
<ToolTip content={"PDF Document"}>
|
||||
<ToolTip content={'PDF Document'}>
|
||||
<ToolBtn
|
||||
onClick={() =>
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContent({
|
||||
type: "blockPDF",
|
||||
type: 'blockPDF',
|
||||
})
|
||||
.run()
|
||||
}
|
||||
|
|
@ -136,14 +172,14 @@ export const ToolbarButtons = ({ editor, props }: any) => {
|
|||
<FileText size={15} />
|
||||
</ToolBtn>
|
||||
</ToolTip>
|
||||
<ToolTip content={"Interactive Quiz"}>
|
||||
<ToolTip content={'Interactive Quiz'}>
|
||||
<ToolBtn
|
||||
onClick={() =>
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContent({
|
||||
type: "blockQuiz",
|
||||
type: 'blockQuiz',
|
||||
})
|
||||
.run()
|
||||
}
|
||||
|
|
@ -151,7 +187,7 @@ export const ToolbarButtons = ({ editor, props }: any) => {
|
|||
<BadgeHelp size={15} />
|
||||
</ToolBtn>
|
||||
</ToolTip>
|
||||
<ToolTip content={"Code Block"}>
|
||||
<ToolTip content={'Code Block'}>
|
||||
<ToolBtn
|
||||
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||
className={editor.isActive('codeBlock') ? 'is-active' : ''}
|
||||
|
|
@ -160,15 +196,15 @@ export const ToolbarButtons = ({ editor, props }: any) => {
|
|||
</ToolBtn>
|
||||
</ToolTip>
|
||||
</ToolButtonsWrapper>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const ToolButtonsWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: left;
|
||||
justify-content: left;
|
||||
`;
|
||||
`
|
||||
|
||||
const ToolBtn = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -197,7 +233,7 @@ const ToolBtn = styled.div`
|
|||
background: rgba(217, 217, 217, 0.48);
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
const ToolSelect = styled.select`
|
||||
display: flex;
|
||||
|
|
@ -208,6 +244,6 @@ const ToolSelect = styled.select`
|
|||
height: 25px;
|
||||
padding: 5px;
|
||||
font-size: 11px;
|
||||
font-family: "DM Sans";
|
||||
font-family: 'DM Sans';
|
||||
margin-right: 5px;
|
||||
`;
|
||||
`
|
||||
|
|
|
|||
|
|
@ -1,33 +1,54 @@
|
|||
'use client';
|
||||
import { motion } from "framer-motion";
|
||||
'use client'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
const variants = {
|
||||
hidden: { opacity: 0, x: 0, y: 0 },
|
||||
enter: { opacity: 1, x: 0, y: 0 },
|
||||
exit: { opacity: 0, x: 0, y: 0 },
|
||||
};
|
||||
|
||||
function PageLoading() {
|
||||
|
||||
return (
|
||||
<motion.main
|
||||
variants={variants} // Pass the variant object into Framer Motion
|
||||
initial="hidden" // Set the initial state to variants.hidden
|
||||
animate="enter" // Animated state to variants.enter
|
||||
exit="exit" // Exit state (used later) to variants.exit
|
||||
transition={{ type: "linear" }} // Set the transition to linear
|
||||
className=""
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 py-20 transition-all">
|
||||
<div className="animate-pulse mx-auto flex space-x-4">
|
||||
<svg className="mx-auto" width="295" height="295" viewBox="0 0 295 295" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect opacity="0.51" x="6.5" y="6.5" width="282" height="282" rx="78.5" stroke="#454545" strokeOpacity="0.46" strokeWidth="13" strokeDasharray="11 11" />
|
||||
<path d="M135.8 200.8V130L122.2 114.6L135.8 110.4V102.8L122.2 87.4L159.8 76V200.8L174.6 218H121L135.8 200.8Z" fill="#454545" fillOpacity="0.13" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</motion.main>
|
||||
)
|
||||
hidden: { opacity: 0, x: 0, y: 0 },
|
||||
enter: { opacity: 1, x: 0, y: 0 },
|
||||
exit: { opacity: 0, x: 0, y: 0 },
|
||||
}
|
||||
|
||||
export default PageLoading
|
||||
function PageLoading() {
|
||||
return (
|
||||
<motion.main
|
||||
variants={variants} // Pass the variant object into Framer Motion
|
||||
initial="hidden" // Set the initial state to variants.hidden
|
||||
animate="enter" // Animated state to variants.enter
|
||||
exit="exit" // Exit state (used later) to variants.exit
|
||||
transition={{ type: 'linear' }} // Set the transition to linear
|
||||
className=""
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 py-20 transition-all">
|
||||
<div className="animate-pulse mx-auto flex space-x-4">
|
||||
<svg
|
||||
className="mx-auto"
|
||||
width="295"
|
||||
height="295"
|
||||
viewBox="0 0 295 295"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
opacity="0.51"
|
||||
x="6.5"
|
||||
y="6.5"
|
||||
width="282"
|
||||
height="282"
|
||||
rx="78.5"
|
||||
stroke="#454545"
|
||||
strokeOpacity="0.46"
|
||||
strokeWidth="13"
|
||||
strokeDasharray="11 11"
|
||||
/>
|
||||
<path
|
||||
d="M135.8 200.8V130L122.2 114.6L135.8 110.4V102.8L122.2 87.4L159.8 76V200.8L174.6 218H121L135.8 200.8Z"
|
||||
fill="#454545"
|
||||
fillOpacity="0.13"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</motion.main>
|
||||
)
|
||||
}
|
||||
|
||||
export default PageLoading
|
||||
|
|
|
|||
|
|
@ -1,56 +1,63 @@
|
|||
'use client';
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { getAPIUrl, getUriWithOrg } from "@services/config/config";
|
||||
import { HeaderProfileBox } from "@components/Security/HeaderProfileBox";
|
||||
import MenuLinks from "./MenuLinks";
|
||||
import { getOrgLogoMediaDirectory } from "@services/media/media";
|
||||
import useSWR from "swr";
|
||||
import { swrFetcher } from "@services/utils/ts/requests";
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
|
||||
import { HeaderProfileBox } from '@components/Security/HeaderProfileBox'
|
||||
import MenuLinks from './MenuLinks'
|
||||
import { getOrgLogoMediaDirectory } from '@services/media/media'
|
||||
import useSWR from 'swr'
|
||||
import { swrFetcher } from '@services/utils/ts/requests'
|
||||
|
||||
export const Menu = (props: any) => {
|
||||
const orgslug = props.orgslug;
|
||||
const [feedbackModal, setFeedbackModal] = React.useState(false);
|
||||
const { data: org, error: error, isLoading } = useSWR(`${getAPIUrl()}orgs/slug/${orgslug}`, swrFetcher);
|
||||
const orgslug = props.orgslug
|
||||
const [feedbackModal, setFeedbackModal] = React.useState(false)
|
||||
const {
|
||||
data: org,
|
||||
error: error,
|
||||
isLoading,
|
||||
} = useSWR(`${getAPIUrl()}orgs/slug/${orgslug}`, swrFetcher)
|
||||
|
||||
function closeFeedbackModal() {
|
||||
setFeedbackModal(false);
|
||||
}
|
||||
function closeFeedbackModal() {
|
||||
setFeedbackModal(false)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="backdrop-blur-lg h-[60px] blur-3xl z-10" style={{
|
||||
}}>
|
||||
<div className="h-[150px] blur-3xl z-0" style={{
|
||||
background: "radial-gradient(1397.20% 56.18% at 75.99% 53.73%, rgba(253, 182, 207, 0.08) 0%, rgba(3, 110, 146, 0.08) 100%)"
|
||||
}}></div>
|
||||
|
||||
</div>
|
||||
<div className="backdrop-blur-lg bg-white/90 fixed flex top-0 left-0 right-0 h-[60px] ring-1 ring-inset ring-gray-500/10 items-center space-x-5 shadow-[0px_4px_16px_rgba(0,0,0,0.03)] z-50">
|
||||
<div className="flex items-center space-x-5 w-full max-w-screen-2xl mx-auto px-16">
|
||||
<div className="logo flex ">
|
||||
<Link href={getUriWithOrg(orgslug, "/")}>
|
||||
<div className="flex w-auto h-9 rounded-md items-center m-auto py-1 justify-center" >
|
||||
{org?.logo_image ? (
|
||||
<img
|
||||
src={`${getOrgLogoMediaDirectory(org.org_uuid, org?.logo_image)}`}
|
||||
alt="Learnhouse"
|
||||
style={{ width: "auto", height: "100%" }}
|
||||
className="rounded-md"
|
||||
/>
|
||||
) : (
|
||||
<LearnHouseLogo></LearnHouseLogo>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="links flex grow">
|
||||
<MenuLinks orgslug={orgslug} />
|
||||
</div>
|
||||
<div className="profile flex items-center">
|
||||
|
||||
{/* <Modal
|
||||
return (
|
||||
<>
|
||||
<div className="backdrop-blur-lg h-[60px] blur-3xl z-10" style={{}}>
|
||||
<div
|
||||
className="h-[150px] blur-3xl z-0"
|
||||
style={{
|
||||
background:
|
||||
'radial-gradient(1397.20% 56.18% at 75.99% 53.73%, rgba(253, 182, 207, 0.08) 0%, rgba(3, 110, 146, 0.08) 100%)',
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div className="backdrop-blur-lg bg-white/90 fixed flex top-0 left-0 right-0 h-[60px] ring-1 ring-inset ring-gray-500/10 items-center space-x-5 shadow-[0px_4px_16px_rgba(0,0,0,0.03)] z-50">
|
||||
<div className="flex items-center space-x-5 w-full max-w-screen-2xl mx-auto px-16">
|
||||
<div className="logo flex ">
|
||||
<Link href={getUriWithOrg(orgslug, '/')}>
|
||||
<div className="flex w-auto h-9 rounded-md items-center m-auto py-1 justify-center">
|
||||
{org?.logo_image ? (
|
||||
<img
|
||||
src={`${getOrgLogoMediaDirectory(
|
||||
org.org_uuid,
|
||||
org?.logo_image
|
||||
)}`}
|
||||
alt="Learnhouse"
|
||||
style={{ width: 'auto', height: '100%' }}
|
||||
className="rounded-md"
|
||||
/>
|
||||
) : (
|
||||
<LearnHouseLogo></LearnHouseLogo>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="links flex grow">
|
||||
<MenuLinks orgslug={orgslug} />
|
||||
</div>
|
||||
<div className="profile flex items-center">
|
||||
{/* <Modal
|
||||
isDialogOpen={feedbackModal}
|
||||
onOpenChange={setFeedbackModal}
|
||||
minHeight="sm"
|
||||
|
|
@ -63,33 +70,60 @@ export const Menu = (props: any) => {
|
|||
</div>
|
||||
}
|
||||
/> */}
|
||||
<HeaderProfileBox />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
<HeaderProfileBox />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const LearnHouseLogo = () => {
|
||||
return (
|
||||
<svg width="133" height="80" viewBox="0 0 433 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="80" height="80" rx="24" fill="black" />
|
||||
<rect width="80" height="80" rx="24" fill="url(#paint0_angular_1555_220)" />
|
||||
<rect x="0.5" y="0.5" width="79" height="79" rx="23.5" stroke="white" strokeOpacity="0.12" />
|
||||
<path d="M37.546 55.926V35.04L33.534 30.497L37.546 29.258V27.016L33.534 22.473L44.626 19.11V55.926L48.992 61H33.18L37.546 55.926Z" fill="white" />
|
||||
<path d="M113.98 54.98V30.2L109.22 24.81L113.98 23.34V20.68L109.22 15.29L122.38 11.3V54.98L127.56 61H108.8L113.98 54.98ZM157.704 41.19V41.26H135.234C136.004 50.29 140.834 54.07 146.294 54.07C151.054 54.07 155.254 51.69 156.304 48.75L157.354 49.17C154.834 55.54 149.864 61.98 141.534 61.98C132.364 61.98 127.184 53.79 127.184 45.39C127.184 36.36 132.784 26 144.194 26C152.524 26 157.634 31.6 157.704 41.05L157.774 41.19H157.704ZM148.674 39.16V38.53C148.674 31.04 145.664 28.1 142.584 28.1C137.264 28.1 135.094 34.47 135.094 38.67V39.16H148.674ZM178.717 61V55.12C176.057 57.71 171.157 61.7 166.537 61.7C161.707 61.7 158.137 59.32 158.137 53.65C158.137 46.51 166.607 42.87 178.717 38.6C178.717 33 178.577 28.66 172.837 28.66C167.237 28.66 163.877 32.58 160.307 37.9H159.817V26.7H188.657L187.117 32.72V56.45H187.187L192.367 61H178.717ZM178.717 53.23V40.56C167.727 44.97 167.377 47.98 167.377 51.34C167.377 54.7 169.687 56.17 172.627 56.17C174.797 56.17 176.967 55.05 178.717 53.23ZM221.429 39.09H220.869C217.789 31.74 213.659 29.29 210.439 29.29C205.609 29.29 205.609 32.79 205.609 39.93V54.98L212.119 61H192.029L197.209 54.98V32.09L192.449 26.7H221.429V39.09ZM261.467 61H242.707L247.747 54.98V39.44C247.747 34.05 246.977 30.62 241.587 30.62C238.997 30.62 236.337 31.74 234.097 34.75V54.98L239.137 61H220.377L225.697 54.98V36.08L220.937 30.69L234.097 26V32.37C236.897 28.03 241.447 25.86 245.647 25.86C252.787 25.86 256.147 30.48 256.147 37.06V54.98L261.467 61ZM274.343 11.3V32.23C277.143 27.89 281.693 25.72 285.893 25.72C293.033 25.72 296.393 30.34 296.393 36.92V54.98H296.463L301.643 61H282.883L287.993 55.05V39.3C287.993 33.91 287.223 30.48 281.833 30.48C279.243 30.48 276.583 31.6 274.343 34.61V54.98L279.523 61H260.763L265.943 54.98V21.38L261.183 15.99L274.343 11.3ZM335.945 42.31C335.945 51.34 329.855 61.84 316.835 61.84C306.895 61.84 301.645 53.79 301.645 45.39C301.645 36.36 307.735 25.86 320.755 25.86C330.695 25.86 335.945 33.91 335.945 42.31ZM316.975 28.52C311.165 28.52 310.535 34.82 310.535 39.02C310.535 49.94 314.525 59.18 320.685 59.18C325.865 59.18 327.195 52.32 327.195 48.68C327.195 37.76 323.135 28.52 316.975 28.52ZM349.01 26.63V48.12C349.01 53.51 349.78 56.94 355.17 56.94C357.55 56.94 360 55.75 361.82 53.65V32.72L356.64 26.63H370.22V55.26L374.98 61L361.82 61.42V55.82C359.3 59.32 356.08 61.7 351.11 61.7C343.97 61.7 340.61 57.08 340.61 50.5V32.72L335.36 26.63H349.01ZM374.617 47.77H375.177C376.997 53.79 382.527 59.04 388.267 59.04C391.137 59.04 393.517 57.64 393.517 54.49C393.517 46.23 374.967 50.29 374.967 36.43C374.967 31.25 379.517 26.7 386.657 26.7H394.357L396.947 25.23V36.85L396.527 36.78C394.007 32.23 389.807 28.87 385.327 28.94C382.387 29.01 380.707 30.83 380.707 33.56C380.707 40.77 399.887 37.62 399.887 50.43C399.887 58.55 391.697 61.7 386.167 61.7C382.667 61.7 377.907 61.21 375.247 60.09L374.617 47.77ZM430.416 41.19V41.26H407.946C408.716 50.29 413.546 54.07 419.006 54.07C423.766 54.07 427.966 51.69 429.016 48.75L430.066 49.17C427.546 55.54 422.576 61.98 414.246 61.98C405.076 61.98 399.896 53.79 399.896 45.39C399.896 36.36 405.496 26 416.906 26C425.236 26 430.346 31.6 430.416 41.05L430.486 41.19H430.416ZM421.386 39.16V38.53C421.386 31.04 418.376 28.1 415.296 28.1C409.976 28.1 407.806 34.47 407.806 38.67V39.16H421.386Z" fill="#121212" />
|
||||
<defs>
|
||||
<radialGradient id="paint0_angular_1555_220" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(40 40) rotate(90) scale(40)">
|
||||
<stop stopColor="#FBFBFB" stopOpacity="0.15" />
|
||||
<stop offset="0.442708" stopOpacity="0.1" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
width="133"
|
||||
height="80"
|
||||
viewBox="0 0 433 80"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="80" height="80" rx="24" fill="black" />
|
||||
<rect
|
||||
width="80"
|
||||
height="80"
|
||||
rx="24"
|
||||
fill="url(#paint0_angular_1555_220)"
|
||||
/>
|
||||
<rect
|
||||
x="0.5"
|
||||
y="0.5"
|
||||
width="79"
|
||||
height="79"
|
||||
rx="23.5"
|
||||
stroke="white"
|
||||
strokeOpacity="0.12"
|
||||
/>
|
||||
<path
|
||||
d="M37.546 55.926V35.04L33.534 30.497L37.546 29.258V27.016L33.534 22.473L44.626 19.11V55.926L48.992 61H33.18L37.546 55.926Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M113.98 54.98V30.2L109.22 24.81L113.98 23.34V20.68L109.22 15.29L122.38 11.3V54.98L127.56 61H108.8L113.98 54.98ZM157.704 41.19V41.26H135.234C136.004 50.29 140.834 54.07 146.294 54.07C151.054 54.07 155.254 51.69 156.304 48.75L157.354 49.17C154.834 55.54 149.864 61.98 141.534 61.98C132.364 61.98 127.184 53.79 127.184 45.39C127.184 36.36 132.784 26 144.194 26C152.524 26 157.634 31.6 157.704 41.05L157.774 41.19H157.704ZM148.674 39.16V38.53C148.674 31.04 145.664 28.1 142.584 28.1C137.264 28.1 135.094 34.47 135.094 38.67V39.16H148.674ZM178.717 61V55.12C176.057 57.71 171.157 61.7 166.537 61.7C161.707 61.7 158.137 59.32 158.137 53.65C158.137 46.51 166.607 42.87 178.717 38.6C178.717 33 178.577 28.66 172.837 28.66C167.237 28.66 163.877 32.58 160.307 37.9H159.817V26.7H188.657L187.117 32.72V56.45H187.187L192.367 61H178.717ZM178.717 53.23V40.56C167.727 44.97 167.377 47.98 167.377 51.34C167.377 54.7 169.687 56.17 172.627 56.17C174.797 56.17 176.967 55.05 178.717 53.23ZM221.429 39.09H220.869C217.789 31.74 213.659 29.29 210.439 29.29C205.609 29.29 205.609 32.79 205.609 39.93V54.98L212.119 61H192.029L197.209 54.98V32.09L192.449 26.7H221.429V39.09ZM261.467 61H242.707L247.747 54.98V39.44C247.747 34.05 246.977 30.62 241.587 30.62C238.997 30.62 236.337 31.74 234.097 34.75V54.98L239.137 61H220.377L225.697 54.98V36.08L220.937 30.69L234.097 26V32.37C236.897 28.03 241.447 25.86 245.647 25.86C252.787 25.86 256.147 30.48 256.147 37.06V54.98L261.467 61ZM274.343 11.3V32.23C277.143 27.89 281.693 25.72 285.893 25.72C293.033 25.72 296.393 30.34 296.393 36.92V54.98H296.463L301.643 61H282.883L287.993 55.05V39.3C287.993 33.91 287.223 30.48 281.833 30.48C279.243 30.48 276.583 31.6 274.343 34.61V54.98L279.523 61H260.763L265.943 54.98V21.38L261.183 15.99L274.343 11.3ZM335.945 42.31C335.945 51.34 329.855 61.84 316.835 61.84C306.895 61.84 301.645 53.79 301.645 45.39C301.645 36.36 307.735 25.86 320.755 25.86C330.695 25.86 335.945 33.91 335.945 42.31ZM316.975 28.52C311.165 28.52 310.535 34.82 310.535 39.02C310.535 49.94 314.525 59.18 320.685 59.18C325.865 59.18 327.195 52.32 327.195 48.68C327.195 37.76 323.135 28.52 316.975 28.52ZM349.01 26.63V48.12C349.01 53.51 349.78 56.94 355.17 56.94C357.55 56.94 360 55.75 361.82 53.65V32.72L356.64 26.63H370.22V55.26L374.98 61L361.82 61.42V55.82C359.3 59.32 356.08 61.7 351.11 61.7C343.97 61.7 340.61 57.08 340.61 50.5V32.72L335.36 26.63H349.01ZM374.617 47.77H375.177C376.997 53.79 382.527 59.04 388.267 59.04C391.137 59.04 393.517 57.64 393.517 54.49C393.517 46.23 374.967 50.29 374.967 36.43C374.967 31.25 379.517 26.7 386.657 26.7H394.357L396.947 25.23V36.85L396.527 36.78C394.007 32.23 389.807 28.87 385.327 28.94C382.387 29.01 380.707 30.83 380.707 33.56C380.707 40.77 399.887 37.62 399.887 50.43C399.887 58.55 391.697 61.7 386.167 61.7C382.667 61.7 377.907 61.21 375.247 60.09L374.617 47.77ZM430.416 41.19V41.26H407.946C408.716 50.29 413.546 54.07 419.006 54.07C423.766 54.07 427.966 51.69 429.016 48.75L430.066 49.17C427.546 55.54 422.576 61.98 414.246 61.98C405.076 61.98 399.896 53.79 399.896 45.39C399.896 36.36 405.496 26 416.906 26C425.236 26 430.346 31.6 430.416 41.05L430.486 41.19H430.416ZM421.386 39.16V38.53C421.386 31.04 418.376 28.1 415.296 28.1C409.976 28.1 407.806 34.47 407.806 38.67V39.16H421.386Z"
|
||||
fill="#121212"
|
||||
/>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="paint0_angular_1555_220"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(40 40) rotate(90) scale(40)"
|
||||
>
|
||||
<stop stopColor="#FBFBFB" stopOpacity="0.15" />
|
||||
<stop offset="0.442708" stopOpacity="0.1" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +1,94 @@
|
|||
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
||||
import { getUriWithOrg } from '@services/config/config';
|
||||
import Link from 'next/link';
|
||||
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
|
||||
import { getUriWithOrg } from '@services/config/config'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
function MenuLinks(props: { orgslug: string }) {
|
||||
return (
|
||||
<div>
|
||||
<ul className="flex space-x-4">
|
||||
<LinkItem link="/courses" type="courses" orgslug={props.orgslug}></LinkItem>
|
||||
<LinkItem link="/collections" type="collections" orgslug={props.orgslug}></LinkItem>
|
||||
<AuthenticatedClientElement checkMethod='authentication'>
|
||||
<LinkItem link="/trail" type="trail" orgslug={props.orgslug}></LinkItem>
|
||||
</AuthenticatedClientElement>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
<ul className="flex space-x-4">
|
||||
<LinkItem
|
||||
link="/courses"
|
||||
type="courses"
|
||||
orgslug={props.orgslug}
|
||||
></LinkItem>
|
||||
<LinkItem
|
||||
link="/collections"
|
||||
type="collections"
|
||||
orgslug={props.orgslug}
|
||||
></LinkItem>
|
||||
<AuthenticatedClientElement checkMethod="authentication">
|
||||
<LinkItem
|
||||
link="/trail"
|
||||
type="trail"
|
||||
orgslug={props.orgslug}
|
||||
></LinkItem>
|
||||
</AuthenticatedClientElement>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const LinkItem = (props: any) => {
|
||||
const link = props.link;
|
||||
const orgslug = props.orgslug;
|
||||
return (
|
||||
<Link href={getUriWithOrg(orgslug, link)}>
|
||||
<li className="flex space-x-3 items-center text-[#909192] font-medium">
|
||||
{props.type == 'courses' &&
|
||||
<>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.9987 1.66663H6.66536C5.78131 1.66663 4.93346 2.01782 4.30834 2.64294C3.68322 3.26806 3.33203 4.1159 3.33203 4.99996V15C3.33203 15.884 3.68322 16.7319 4.30834 17.357C4.93346 17.9821 5.78131 18.3333 6.66536 18.3333H14.9987C15.4407 18.3333 15.8646 18.1577 16.1772 17.8451C16.4898 17.5326 16.6654 17.1087 16.6654 16.6666V3.33329C16.6654 2.89127 16.4898 2.46734 16.1772 2.15478C15.8646 1.84222 15.4407 1.66663 14.9987 1.66663ZM4.9987 4.99996C4.9987 4.55793 5.17429 4.13401 5.48685 3.82145C5.79941 3.50889 6.22334 3.33329 6.66536 3.33329H14.9987V11.6666H6.66536C6.0779 11.6691 5.50203 11.8303 4.9987 12.1333V4.99996ZM6.66536 16.6666C6.22334 16.6666 5.79941 16.491 5.48685 16.1785C5.17429 15.8659 4.9987 15.442 4.9987 15C4.9987 14.5579 5.17429 14.134 5.48685 13.8214C5.79941 13.5089 6.22334 13.3333 6.66536 13.3333H14.9987V16.6666H6.66536ZM8.33203 6.66663H11.6654C11.8864 6.66663 12.0983 6.57883 12.2546 6.42255C12.4109 6.26627 12.4987 6.05431 12.4987 5.83329C12.4987 5.61228 12.4109 5.40032 12.2546 5.24404C12.0983 5.08776 11.8864 4.99996 11.6654 4.99996H8.33203C8.11102 4.99996 7.89906 5.08776 7.74278 5.24404C7.5865 5.40032 7.4987 5.61228 7.4987 5.83329C7.4987 6.05431 7.5865 6.26627 7.74278 6.42255C7.89906 6.57883 8.11102 6.66663 8.33203 6.66663V6.66663Z" fill="#898A8B" />
|
||||
</svg>
|
||||
<span>Courses</span>
|
||||
</>}
|
||||
const link = props.link
|
||||
const orgslug = props.orgslug
|
||||
return (
|
||||
<Link href={getUriWithOrg(orgslug, link)}>
|
||||
<li className="flex space-x-3 items-center text-[#909192] font-medium">
|
||||
{props.type == 'courses' && (
|
||||
<>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14.9987 1.66663H6.66536C5.78131 1.66663 4.93346 2.01782 4.30834 2.64294C3.68322 3.26806 3.33203 4.1159 3.33203 4.99996V15C3.33203 15.884 3.68322 16.7319 4.30834 17.357C4.93346 17.9821 5.78131 18.3333 6.66536 18.3333H14.9987C15.4407 18.3333 15.8646 18.1577 16.1772 17.8451C16.4898 17.5326 16.6654 17.1087 16.6654 16.6666V3.33329C16.6654 2.89127 16.4898 2.46734 16.1772 2.15478C15.8646 1.84222 15.4407 1.66663 14.9987 1.66663ZM4.9987 4.99996C4.9987 4.55793 5.17429 4.13401 5.48685 3.82145C5.79941 3.50889 6.22334 3.33329 6.66536 3.33329H14.9987V11.6666H6.66536C6.0779 11.6691 5.50203 11.8303 4.9987 12.1333V4.99996ZM6.66536 16.6666C6.22334 16.6666 5.79941 16.491 5.48685 16.1785C5.17429 15.8659 4.9987 15.442 4.9987 15C4.9987 14.5579 5.17429 14.134 5.48685 13.8214C5.79941 13.5089 6.22334 13.3333 6.66536 13.3333H14.9987V16.6666H6.66536ZM8.33203 6.66663H11.6654C11.8864 6.66663 12.0983 6.57883 12.2546 6.42255C12.4109 6.26627 12.4987 6.05431 12.4987 5.83329C12.4987 5.61228 12.4109 5.40032 12.2546 5.24404C12.0983 5.08776 11.8864 4.99996 11.6654 4.99996H8.33203C8.11102 4.99996 7.89906 5.08776 7.74278 5.24404C7.5865 5.40032 7.4987 5.61228 7.4987 5.83329C7.4987 6.05431 7.5865 6.26627 7.74278 6.42255C7.89906 6.57883 8.11102 6.66663 8.33203 6.66663V6.66663Z"
|
||||
fill="#898A8B"
|
||||
/>
|
||||
</svg>
|
||||
<span>Courses</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{props.type == 'collections' &&
|
||||
<>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.0567 6.14171C17.0567 6.14171 17.0567 6.14171 17.0567 6.07504L17.0067 5.95004C16.9893 5.92352 16.9698 5.89844 16.9483 5.87504C16.926 5.83976 16.901 5.80632 16.8733 5.77504L16.7983 5.71671L16.665 5.65004L10.415 1.79171C10.2826 1.70893 10.1295 1.66504 9.97333 1.66504C9.81715 1.66504 9.66411 1.70893 9.53166 1.79171L3.33166 5.65004L3.25666 5.71671L3.18166 5.77504C3.15404 5.80632 3.12896 5.83976 3.10666 5.87504C3.08524 5.89844 3.06573 5.92352 3.04833 5.95004L2.99833 6.07504C2.99833 6.07504 2.99833 6.07504 2.99833 6.14171C2.99014 6.2137 2.99014 6.28639 2.99833 6.35838V13.6417C2.99805 13.7833 3.03386 13.9227 3.10239 14.0466C3.17092 14.1706 3.2699 14.275 3.39 14.35L9.64 18.2084C9.67846 18.2321 9.72076 18.2491 9.765 18.2584C9.765 18.2584 9.80666 18.2584 9.83166 18.2584C9.97265 18.3031 10.124 18.3031 10.265 18.2584C10.265 18.2584 10.3067 18.2584 10.3317 18.2584C10.3759 18.2491 10.4182 18.2321 10.4567 18.2084L16.665 14.35C16.7851 14.275 16.8841 14.1706 16.9526 14.0466C17.0211 13.9227 17.0569 13.7833 17.0567 13.6417V6.35838C17.0649 6.28639 17.0649 6.2137 17.0567 6.14171ZM9.165 16.0084L4.58166 13.175V7.85838L9.165 10.6834V16.0084ZM9.99833 9.24171L5.33166 6.35838L9.99833 3.48337L14.665 6.35838L9.99833 9.24171ZM15.415 13.175L10.8317 16.0084V10.6834L15.415 7.85838V13.175Z" fill="#898A8B" />
|
||||
</svg>
|
||||
<span>Collections</span>
|
||||
</>}
|
||||
{props.type == 'collections' && (
|
||||
<>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17.0567 6.14171C17.0567 6.14171 17.0567 6.14171 17.0567 6.07504L17.0067 5.95004C16.9893 5.92352 16.9698 5.89844 16.9483 5.87504C16.926 5.83976 16.901 5.80632 16.8733 5.77504L16.7983 5.71671L16.665 5.65004L10.415 1.79171C10.2826 1.70893 10.1295 1.66504 9.97333 1.66504C9.81715 1.66504 9.66411 1.70893 9.53166 1.79171L3.33166 5.65004L3.25666 5.71671L3.18166 5.77504C3.15404 5.80632 3.12896 5.83976 3.10666 5.87504C3.08524 5.89844 3.06573 5.92352 3.04833 5.95004L2.99833 6.07504C2.99833 6.07504 2.99833 6.07504 2.99833 6.14171C2.99014 6.2137 2.99014 6.28639 2.99833 6.35838V13.6417C2.99805 13.7833 3.03386 13.9227 3.10239 14.0466C3.17092 14.1706 3.2699 14.275 3.39 14.35L9.64 18.2084C9.67846 18.2321 9.72076 18.2491 9.765 18.2584C9.765 18.2584 9.80666 18.2584 9.83166 18.2584C9.97265 18.3031 10.124 18.3031 10.265 18.2584C10.265 18.2584 10.3067 18.2584 10.3317 18.2584C10.3759 18.2491 10.4182 18.2321 10.4567 18.2084L16.665 14.35C16.7851 14.275 16.8841 14.1706 16.9526 14.0466C17.0211 13.9227 17.0569 13.7833 17.0567 13.6417V6.35838C17.0649 6.28639 17.0649 6.2137 17.0567 6.14171ZM9.165 16.0084L4.58166 13.175V7.85838L9.165 10.6834V16.0084ZM9.99833 9.24171L5.33166 6.35838L9.99833 3.48337L14.665 6.35838L9.99833 9.24171ZM15.415 13.175L10.8317 16.0084V10.6834L15.415 7.85838V13.175Z"
|
||||
fill="#898A8B"
|
||||
/>
|
||||
</svg>
|
||||
<span>Collections</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{props.type == 'trail' &&
|
||||
<>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.5751 7.95841C16.5059 7.82098 16.3999 7.70541 16.269 7.62451C16.1381 7.54361 15.9874 7.50054 15.8335 7.50008H11.6668V2.50008C11.6757 2.31731 11.6243 2.13669 11.5204 1.98608C11.4164 1.83547 11.2658 1.72325 11.0918 1.66674C10.9245 1.6117 10.744 1.61108 10.5763 1.66498C10.4087 1.71888 10.2624 1.82452 10.1585 1.96674L3.4918 11.1334C3.40827 11.2541 3.35811 11.3948 3.3464 11.5411C3.3347 11.6874 3.36186 11.8343 3.42513 11.9667C3.4834 12.1182 3.58462 12.2493 3.71637 12.3441C3.84812 12.4388 4.00467 12.493 4.1668 12.5001H8.33346V17.5001C8.33359 17.6758 8.38927 17.847 8.49254 17.9892C8.59581 18.1314 8.74139 18.2373 8.90846 18.2917C8.99219 18.3177 9.07915 18.3317 9.1668 18.3334C9.29828 18.3338 9.42799 18.303 9.5453 18.2436C9.66262 18.1842 9.76422 18.0979 9.8418 17.9917L16.5085 8.82508C16.5982 8.70074 16.652 8.55404 16.6637 8.40112C16.6755 8.24821 16.6448 8.09502 16.5751 7.95841ZM10.0001 14.9334V11.6667C10.0001 11.4457 9.91233 11.2338 9.75605 11.0775C9.59977 10.9212 9.38781 10.8334 9.1668 10.8334H5.83346L10.0001 5.06674V8.33341C10.0001 8.55442 10.0879 8.76638 10.2442 8.92267C10.4005 9.07895 10.6124 9.16674 10.8335 9.16674H14.1668L10.0001 14.9334Z" fill="#909192" />
|
||||
</svg>
|
||||
<span>Trail</span>
|
||||
</>}
|
||||
</li>
|
||||
</Link>
|
||||
)
|
||||
{props.type == 'trail' && (
|
||||
<>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16.5751 7.95841C16.5059 7.82098 16.3999 7.70541 16.269 7.62451C16.1381 7.54361 15.9874 7.50054 15.8335 7.50008H11.6668V2.50008C11.6757 2.31731 11.6243 2.13669 11.5204 1.98608C11.4164 1.83547 11.2658 1.72325 11.0918 1.66674C10.9245 1.6117 10.744 1.61108 10.5763 1.66498C10.4087 1.71888 10.2624 1.82452 10.1585 1.96674L3.4918 11.1334C3.40827 11.2541 3.35811 11.3948 3.3464 11.5411C3.3347 11.6874 3.36186 11.8343 3.42513 11.9667C3.4834 12.1182 3.58462 12.2493 3.71637 12.3441C3.84812 12.4388 4.00467 12.493 4.1668 12.5001H8.33346V17.5001C8.33359 17.6758 8.38927 17.847 8.49254 17.9892C8.59581 18.1314 8.74139 18.2373 8.90846 18.2917C8.99219 18.3177 9.07915 18.3317 9.1668 18.3334C9.29828 18.3338 9.42799 18.303 9.5453 18.2436C9.66262 18.1842 9.76422 18.0979 9.8418 17.9917L16.5085 8.82508C16.5982 8.70074 16.652 8.55404 16.6637 8.40112C16.6755 8.24821 16.6448 8.09502 16.5751 7.95841ZM10.0001 14.9334V11.6667C10.0001 11.4457 9.91233 11.2338 9.75605 11.0775C9.59977 10.9212 9.38781 10.8334 9.1668 10.8334H5.83346L10.0001 5.06674V8.33341C10.0001 8.55442 10.0879 8.76638 10.2442 8.92267C10.4005 9.07895 10.6124 9.16674 10.8335 9.16674H14.1668L10.0001 14.9334Z"
|
||||
fill="#909192"
|
||||
/>
|
||||
</svg>
|
||||
<span>Trail</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
export default MenuLinks
|
||||
export default MenuLinks
|
||||
|
|
|
|||
|
|
@ -1,71 +1,79 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import Link from "next/link";
|
||||
import { getNewAccessTokenUsingRefreshToken, getUserInfo } from "@services/auth/auth";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useRouter } from "next/router";
|
||||
import { Settings } from "lucide-react";
|
||||
import UserAvatar from "@components/Objects/UserAvatar";
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
getNewAccessTokenUsingRefreshToken,
|
||||
getUserInfo,
|
||||
} from '@services/auth/auth'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Settings } from 'lucide-react'
|
||||
import UserAvatar from '@components/Objects/UserAvatar'
|
||||
|
||||
export interface Auth {
|
||||
access_token: string;
|
||||
isAuthenticated: boolean;
|
||||
userInfo: any;
|
||||
isLoading: boolean;
|
||||
access_token: string
|
||||
isAuthenticated: boolean
|
||||
userInfo: any
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
function ProfileArea() {
|
||||
const PRIVATE_ROUTES = ['/course/*/edit', '/settings*', '/trail']
|
||||
const NON_AUTHENTICATED_ROUTES = ['/login', '/register']
|
||||
|
||||
|
||||
const PRIVATE_ROUTES = ["/course/*/edit", "/settings*", "/trail"];
|
||||
const NON_AUTHENTICATED_ROUTES = ["/login", "/register"];
|
||||
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [auth, setAuth] = React.useState<Auth>({ access_token: "", isAuthenticated: false, userInfo: {}, isLoading: true });
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const [auth, setAuth] = React.useState<Auth>({
|
||||
access_token: '',
|
||||
isAuthenticated: false,
|
||||
userInfo: {},
|
||||
isLoading: true,
|
||||
})
|
||||
|
||||
async function checkRefreshToken() {
|
||||
let data = await getNewAccessTokenUsingRefreshToken();
|
||||
let data = await getNewAccessTokenUsingRefreshToken()
|
||||
if (data) {
|
||||
return data.access_token;
|
||||
return data.access_token
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
checkAuth();
|
||||
}, [pathname]);
|
||||
checkAuth()
|
||||
}, [pathname])
|
||||
|
||||
async function checkAuth() {
|
||||
try {
|
||||
let access_token = await checkRefreshToken();
|
||||
let userInfo = {};
|
||||
let isLoading = false;
|
||||
let access_token = await checkRefreshToken()
|
||||
let userInfo = {}
|
||||
let isLoading = false
|
||||
|
||||
if (access_token) {
|
||||
userInfo = await getUserInfo(access_token);
|
||||
setAuth({ access_token, isAuthenticated: true, userInfo, isLoading });
|
||||
userInfo = await getUserInfo(access_token)
|
||||
setAuth({ access_token, isAuthenticated: true, userInfo, isLoading })
|
||||
|
||||
// Redirect to home if user is trying to access a NON_AUTHENTICATED_ROUTES route
|
||||
|
||||
if (NON_AUTHENTICATED_ROUTES.some((route) => new RegExp(`^${route.replace("*", ".*")}$`).test(pathname))) {
|
||||
router.push("/");
|
||||
if (
|
||||
NON_AUTHENTICATED_ROUTES.some((route) =>
|
||||
new RegExp(`^${route.replace('*', '.*')}$`).test(pathname)
|
||||
)
|
||||
) {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
setAuth({ access_token, isAuthenticated: false, userInfo, isLoading });
|
||||
setAuth({ access_token, isAuthenticated: false, userInfo, isLoading })
|
||||
|
||||
// Redirect to login if user is trying to access a private route
|
||||
if (PRIVATE_ROUTES.some((route) => new RegExp(`^${route.replace("*", ".*")}$`).test(pathname))) {
|
||||
router.push("/login");
|
||||
if (
|
||||
PRIVATE_ROUTES.some((route) =>
|
||||
new RegExp(`^${route.replace('*', '.*')}$`).test(pathname)
|
||||
)
|
||||
) {
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
return (
|
||||
<ProfileAreaStyled>
|
||||
|
|
@ -73,14 +81,10 @@ function ProfileArea() {
|
|||
<UnidentifiedArea>
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/login">
|
||||
Login
|
||||
</Link>
|
||||
<Link href="/login">Login</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/signup">
|
||||
Sign up
|
||||
</Link>
|
||||
<Link href="/signup">Sign up</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</UnidentifiedArea>
|
||||
|
|
@ -89,9 +93,11 @@ function ProfileArea() {
|
|||
<AccountArea>
|
||||
<div>{auth.userInfo.user_object.username}</div>
|
||||
<div>
|
||||
<UserAvatar width={40} />
|
||||
<UserAvatar width={40} />
|
||||
</div>
|
||||
<Link href={"/dash"}><Settings /></Link>
|
||||
<Link href={'/dash'}>
|
||||
<Settings />
|
||||
</Link>
|
||||
</AccountArea>
|
||||
)}
|
||||
</ProfileAreaStyled>
|
||||
|
|
@ -103,8 +109,6 @@ const AccountArea = styled.div`
|
|||
display: flex;
|
||||
place-items: center;
|
||||
|
||||
|
||||
|
||||
div {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
|
@ -112,13 +116,13 @@ const AccountArea = styled.div`
|
|||
width: 29px;
|
||||
border-radius: 19px;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
const ProfileAreaStyled = styled.div`
|
||||
display: flex;
|
||||
place-items: stretch;
|
||||
place-items: center;
|
||||
`;
|
||||
`
|
||||
|
||||
const UnidentifiedArea = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -138,7 +142,6 @@ const UnidentifiedArea = styled.div`
|
|||
color: #171717;
|
||||
}
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
|
||||
export default ProfileArea
|
||||
export default ProfileArea
|
||||
|
|
|
|||
|
|
@ -1,112 +1,144 @@
|
|||
import React, { useState } from "react";
|
||||
import DynamicPageActivityImage from "public/activities_types/dynamic-page-activity.png";
|
||||
import VideoPageActivityImage from "public//activities_types/video-page-activity.png";
|
||||
import DocumentPdfPageActivityImage from "public//activities_types/documentpdf-page-activity.png";
|
||||
import { styled } from '@stitches/react';
|
||||
import DynamicCanvaModal from "./NewActivityModal/DynamicCanva";
|
||||
import VideoModal from "./NewActivityModal/Video";
|
||||
import Image from "next/image";
|
||||
import DocumentPdfModal from "./NewActivityModal/DocumentPdf";
|
||||
|
||||
function NewActivityModal({ closeModal, submitActivity, submitFileActivity, submitExternalVideo, chapterId, course }: any) {
|
||||
const [selectedView, setSelectedView] = useState("home");
|
||||
import React, { useState } from 'react'
|
||||
import DynamicPageActivityImage from 'public/activities_types/dynamic-page-activity.png'
|
||||
import VideoPageActivityImage from 'public//activities_types/video-page-activity.png'
|
||||
import DocumentPdfPageActivityImage from 'public//activities_types/documentpdf-page-activity.png'
|
||||
import { styled } from '@stitches/react'
|
||||
import DynamicCanvaModal from './NewActivityModal/DynamicCanva'
|
||||
import VideoModal from './NewActivityModal/Video'
|
||||
import Image from 'next/image'
|
||||
import DocumentPdfModal from './NewActivityModal/DocumentPdf'
|
||||
|
||||
function NewActivityModal({
|
||||
closeModal,
|
||||
submitActivity,
|
||||
submitFileActivity,
|
||||
submitExternalVideo,
|
||||
chapterId,
|
||||
course,
|
||||
}: any) {
|
||||
const [selectedView, setSelectedView] = useState('home')
|
||||
|
||||
return (
|
||||
<div>
|
||||
{selectedView === "home" && (
|
||||
{selectedView === 'home' && (
|
||||
<ActivityChooserWrapper>
|
||||
<ActivityOption onClick={() => { setSelectedView("dynamic") }}>
|
||||
<ActivityOption
|
||||
onClick={() => {
|
||||
setSelectedView('dynamic')
|
||||
}}
|
||||
>
|
||||
<ActivityTypeImage>
|
||||
<Image alt="Dynamic Page" src={DynamicPageActivityImage}></Image>
|
||||
</ActivityTypeImage>
|
||||
<ActivityTypeTitle>Dynamic Page</ActivityTypeTitle>
|
||||
</ActivityOption>
|
||||
<ActivityOption onClick={() => { setSelectedView("video") }}>
|
||||
<ActivityOption
|
||||
onClick={() => {
|
||||
setSelectedView('video')
|
||||
}}
|
||||
>
|
||||
<ActivityTypeImage>
|
||||
<Image alt="Video Page" src={VideoPageActivityImage}></Image>
|
||||
</ActivityTypeImage>
|
||||
<ActivityTypeTitle>Video Page</ActivityTypeTitle>
|
||||
</ActivityOption>
|
||||
<ActivityOption onClick={() => { setSelectedView("documentpdf") }}>
|
||||
<ActivityOption
|
||||
onClick={() => {
|
||||
setSelectedView('documentpdf')
|
||||
}}
|
||||
>
|
||||
<ActivityTypeImage>
|
||||
<Image alt="Document PDF Page" src={DocumentPdfPageActivityImage}></Image>
|
||||
<Image
|
||||
alt="Document PDF Page"
|
||||
src={DocumentPdfPageActivityImage}
|
||||
></Image>
|
||||
</ActivityTypeImage>
|
||||
<ActivityTypeTitle>PDF Document Page</ActivityTypeTitle>
|
||||
</ActivityOption>
|
||||
</ActivityChooserWrapper>
|
||||
)}
|
||||
|
||||
{selectedView === "dynamic" && (
|
||||
<DynamicCanvaModal submitActivity={submitActivity} chapterId={chapterId} course={course} />
|
||||
{selectedView === 'dynamic' && (
|
||||
<DynamicCanvaModal
|
||||
submitActivity={submitActivity}
|
||||
chapterId={chapterId}
|
||||
course={course}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedView === "video" && (
|
||||
<VideoModal submitFileActivity={submitFileActivity} submitExternalVideo={submitExternalVideo}
|
||||
chapterId={chapterId} course={course} />
|
||||
{selectedView === 'video' && (
|
||||
<VideoModal
|
||||
submitFileActivity={submitFileActivity}
|
||||
submitExternalVideo={submitExternalVideo}
|
||||
chapterId={chapterId}
|
||||
course={course}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedView === "documentpdf" && (
|
||||
<DocumentPdfModal submitFileActivity={submitFileActivity} chapterId={chapterId} course={course} />
|
||||
{selectedView === 'documentpdf' && (
|
||||
<DocumentPdfModal
|
||||
submitFileActivity={submitFileActivity}
|
||||
chapterId={chapterId}
|
||||
course={course}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const ActivityChooserWrapper = styled("div", {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "start",
|
||||
const ActivityChooserWrapper = styled('div', {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'start',
|
||||
marginTop: 10,
|
||||
});
|
||||
})
|
||||
|
||||
const ActivityOption = styled("div", {
|
||||
width: "180px",
|
||||
textAlign: "center",
|
||||
const ActivityOption = styled('div', {
|
||||
width: '180px',
|
||||
textAlign: 'center',
|
||||
borderRadius: 10,
|
||||
background: "#F6F6F6",
|
||||
border: "4px solid #F5F5F5",
|
||||
margin: "auto",
|
||||
background: '#F6F6F6',
|
||||
border: '4px solid #F5F5F5',
|
||||
margin: 'auto',
|
||||
|
||||
// hover
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
background: "#ededed",
|
||||
border: "4px solid #ededed",
|
||||
// hover
|
||||
'&:hover': {
|
||||
cursor: 'pointer',
|
||||
background: '#ededed',
|
||||
border: '4px solid #ededed',
|
||||
|
||||
transition: "background 0.2s ease-in-out, border 0.2s ease-in-out",
|
||||
transition: 'background 0.2s ease-in-out, border 0.2s ease-in-out',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const ActivityTypeImage = styled("div", {
|
||||
const ActivityTypeImage = styled('div', {
|
||||
height: 80,
|
||||
borderRadius: 8,
|
||||
margin: 2,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "end",
|
||||
textAlign: "center",
|
||||
background: "#ffffff",
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'end',
|
||||
textAlign: 'center',
|
||||
background: '#ffffff',
|
||||
|
||||
// hover
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
// hover
|
||||
'&:hover': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const ActivityTypeTitle = styled("div", {
|
||||
display: "flex",
|
||||
const ActivityTypeTitle = styled('div', {
|
||||
display: 'flex',
|
||||
fontSize: 12,
|
||||
height: "20px",
|
||||
height: '20px',
|
||||
fontWeight: 500,
|
||||
color: "rgba(0, 0, 0, 0.38);",
|
||||
color: 'rgba(0, 0, 0, 0.38);',
|
||||
|
||||
// center text vertically
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
textAlign: "center",
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
export default NewActivityModal;
|
||||
export default NewActivityModal
|
||||
|
|
|
|||
|
|
@ -1,40 +1,56 @@
|
|||
import FormLayout, { ButtonBlack, Flex, FormField, FormLabel, FormMessage, Input } from "@components/StyledElements/Form/Form";
|
||||
import React, { useState } from "react";
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
import BarLoader from "react-spinners/BarLoader";
|
||||
import FormLayout, {
|
||||
ButtonBlack,
|
||||
Flex,
|
||||
FormField,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
} from '@components/StyledElements/Form/Form'
|
||||
import React, { useState } from 'react'
|
||||
import * as Form from '@radix-ui/react-form'
|
||||
import BarLoader from 'react-spinners/BarLoader'
|
||||
|
||||
function DocumentPdfModal({ submitFileActivity, chapterId, course }: any) {
|
||||
const [documentpdf, setDocumentPdf] = React.useState(null) as any;
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [name, setName] = React.useState("");
|
||||
const [documentpdf, setDocumentPdf] = React.useState(null) as any
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [name, setName] = React.useState('')
|
||||
|
||||
const handleDocumentPdfChange = (event: React.ChangeEvent<any>) => {
|
||||
setDocumentPdf(event.target.files[0]);
|
||||
};
|
||||
setDocumentPdf(event.target.files[0])
|
||||
}
|
||||
|
||||
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(event.target.value);
|
||||
};
|
||||
setName(event.target.value)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
let status = await submitFileActivity(documentpdf, "documentpdf", { name: name,
|
||||
chapter_id: chapterId,
|
||||
activity_type: "TYPE_DOCUMENT",
|
||||
activity_sub_type:"SUBTYPE_DOCUMENT_PDF",
|
||||
published_version:1,
|
||||
version:1,
|
||||
course_id: course.id, }, chapterId);
|
||||
setIsSubmitting(false);
|
||||
};
|
||||
e.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
let status = await submitFileActivity(
|
||||
documentpdf,
|
||||
'documentpdf',
|
||||
{
|
||||
name: name,
|
||||
chapter_id: chapterId,
|
||||
activity_type: 'TYPE_DOCUMENT',
|
||||
activity_sub_type: 'SUBTYPE_DOCUMENT_PDF',
|
||||
published_version: 1,
|
||||
version: 1,
|
||||
course_id: course.id,
|
||||
},
|
||||
chapterId
|
||||
)
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<FormLayout onSubmit={handleSubmit}>
|
||||
<FormLayout onSubmit={handleSubmit}>
|
||||
<FormField name="documentpdf-activity-name">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>PDF Document name</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide a name for your PDF Document activity</FormMessage>
|
||||
<FormMessage match="valueMissing">
|
||||
Please provide a name for your PDF Document activity
|
||||
</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Input onChange={handleNameChange} type="text" required />
|
||||
|
|
@ -43,7 +59,9 @@ function DocumentPdfModal({ submitFileActivity, chapterId, course }: any) {
|
|||
<FormField name="documentpdf-activity-file">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>PDF Document file</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide a PDF Document for your activity</FormMessage>
|
||||
<FormMessage match="valueMissing">
|
||||
Please provide a PDF Document for your activity
|
||||
</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<input type="file" onChange={handleDocumentPdfChange} required />
|
||||
|
|
@ -52,13 +70,21 @@ function DocumentPdfModal({ submitFileActivity, chapterId, course }: any) {
|
|||
|
||||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Form.Submit asChild>
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
||||
{isSubmitting ? <BarLoader cssOverride={{borderRadius:60,}} width={60} color="#ffffff" /> : "Create activity"}
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
||||
{isSubmitting ? (
|
||||
<BarLoader
|
||||
cssOverride={{ borderRadius: 60 }}
|
||||
width={60}
|
||||
color="#ffffff"
|
||||
/>
|
||||
) : (
|
||||
'Create activity'
|
||||
)}
|
||||
</ButtonBlack>
|
||||
</Form.Submit>
|
||||
</Flex>
|
||||
</FormLayout>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default DocumentPdfModal;
|
||||
export default DocumentPdfModal
|
||||
|
|
|
|||
|
|
@ -1,41 +1,51 @@
|
|||
import FormLayout, { ButtonBlack, Flex, FormField, FormLabel, FormMessage, Input, Textarea } from "@components/StyledElements/Form/Form";
|
||||
import React, { useState } from "react";
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
import BarLoader from "react-spinners/BarLoader";
|
||||
import FormLayout, {
|
||||
ButtonBlack,
|
||||
Flex,
|
||||
FormField,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
Textarea,
|
||||
} from '@components/StyledElements/Form/Form'
|
||||
import React, { useState } from 'react'
|
||||
import * as Form from '@radix-ui/react-form'
|
||||
import BarLoader from 'react-spinners/BarLoader'
|
||||
|
||||
function DynamicCanvaModal({ submitActivity, chapterId, course }: any) {
|
||||
const [activityName, setActivityName] = useState("");
|
||||
const [activityDescription, setActivityDescription] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [activityName, setActivityName] = useState('')
|
||||
const [activityDescription, setActivityDescription] = useState('')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const handleActivityNameChange = (e: any) => {
|
||||
setActivityName(e.target.value);
|
||||
};
|
||||
setActivityName(e.target.value)
|
||||
}
|
||||
|
||||
const handleActivityDescriptionChange = (e: any) => {
|
||||
setActivityDescription(e.target.value);
|
||||
};
|
||||
setActivityDescription(e.target.value)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
e.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
await submitActivity({
|
||||
name: activityName,
|
||||
chapter_id: chapterId,
|
||||
activity_type: "TYPE_DYNAMIC",
|
||||
activity_sub_type:"SUBTYPE_DYNAMIC_PAGE",
|
||||
published_version:1,
|
||||
version:1,
|
||||
activity_type: 'TYPE_DYNAMIC',
|
||||
activity_sub_type: 'SUBTYPE_DYNAMIC_PAGE',
|
||||
published_version: 1,
|
||||
version: 1,
|
||||
course_id: course.id,
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
};
|
||||
})
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
return (
|
||||
<FormLayout onSubmit={handleSubmit}>
|
||||
<FormField name="dynamic-activity-name">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Activity name</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide a name for your activity</FormMessage>
|
||||
<FormMessage match="valueMissing">
|
||||
Please provide a name for your activity
|
||||
</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Input onChange={handleActivityNameChange} type="text" required />
|
||||
|
|
@ -44,7 +54,9 @@ function DynamicCanvaModal({ submitActivity, chapterId, course }: any) {
|
|||
<FormField name="dynamic-activity-desc">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Activity description</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide a description for your activity</FormMessage>
|
||||
<FormMessage match="valueMissing">
|
||||
Please provide a description for your activity
|
||||
</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Textarea onChange={handleActivityDescriptionChange} required />
|
||||
|
|
@ -53,14 +65,21 @@ function DynamicCanvaModal({ submitActivity, chapterId, course }: any) {
|
|||
|
||||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Form.Submit asChild>
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
||||
{isSubmitting ? <BarLoader cssOverride={{borderRadius:60,}} width={60} color="#ffffff" />
|
||||
: "Create activity"}
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
||||
{isSubmitting ? (
|
||||
<BarLoader
|
||||
cssOverride={{ borderRadius: 60 }}
|
||||
width={60}
|
||||
color="#ffffff"
|
||||
/>
|
||||
) : (
|
||||
'Create activity'
|
||||
)}
|
||||
</ButtonBlack>
|
||||
</Form.Submit>
|
||||
</Flex>
|
||||
</FormLayout>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default DynamicCanvaModal;
|
||||
export default DynamicCanvaModal
|
||||
|
|
|
|||
|
|
@ -1,67 +1,87 @@
|
|||
import FormLayout, { ButtonBlack, Flex, FormField, FormLabel, FormMessage, Input } from "@components/StyledElements/Form/Form";
|
||||
import React, { useState } from "react";
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
import BarLoader from "react-spinners/BarLoader";
|
||||
import { Youtube } from "lucide-react";
|
||||
import FormLayout, {
|
||||
ButtonBlack,
|
||||
Flex,
|
||||
FormField,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
} from '@components/StyledElements/Form/Form'
|
||||
import React, { useState } from 'react'
|
||||
import * as Form from '@radix-ui/react-form'
|
||||
import BarLoader from 'react-spinners/BarLoader'
|
||||
import { Youtube } from 'lucide-react'
|
||||
|
||||
interface ExternalVideoObject {
|
||||
name: string,
|
||||
type: string,
|
||||
name: string
|
||||
type: string
|
||||
uri: string
|
||||
chapter_id: string
|
||||
}
|
||||
|
||||
|
||||
function VideoModal({ submitFileActivity, submitExternalVideo, chapterId, course }: any) {
|
||||
const [video, setVideo] = React.useState(null) as any;
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [name, setName] = React.useState("");
|
||||
const [youtubeUrl, setYoutubeUrl] = React.useState("");
|
||||
const [selectedView, setSelectedView] = React.useState("file") as any;
|
||||
function VideoModal({
|
||||
submitFileActivity,
|
||||
submitExternalVideo,
|
||||
chapterId,
|
||||
course,
|
||||
}: any) {
|
||||
const [video, setVideo] = React.useState(null) as any
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [name, setName] = React.useState('')
|
||||
const [youtubeUrl, setYoutubeUrl] = React.useState('')
|
||||
const [selectedView, setSelectedView] = React.useState('file') as any
|
||||
|
||||
const handleVideoChange = (event: React.ChangeEvent<any>) => {
|
||||
setVideo(event.target.files[0]);
|
||||
};
|
||||
setVideo(event.target.files[0])
|
||||
}
|
||||
|
||||
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(event.target.value);
|
||||
};
|
||||
setName(event.target.value)
|
||||
}
|
||||
|
||||
const handleYoutubeUrlChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setYoutubeUrl(event.target.value);
|
||||
};
|
||||
const handleYoutubeUrlChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setYoutubeUrl(event.target.value)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
e.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
|
||||
if (selectedView === "file") {
|
||||
let status = await submitFileActivity(video, "video", {
|
||||
name: name,
|
||||
chapter_id: chapterId,
|
||||
activity_type: "TYPE_VIDEO",
|
||||
activity_sub_type: "SUBTYPE_VIDEO_HOSTED",
|
||||
published_version: 1,
|
||||
version: 1,
|
||||
course_id: course.id,
|
||||
}, chapterId);
|
||||
if (selectedView === 'file') {
|
||||
let status = await submitFileActivity(
|
||||
video,
|
||||
'video',
|
||||
{
|
||||
name: name,
|
||||
chapter_id: chapterId,
|
||||
activity_type: 'TYPE_VIDEO',
|
||||
activity_sub_type: 'SUBTYPE_VIDEO_HOSTED',
|
||||
published_version: 1,
|
||||
version: 1,
|
||||
course_id: course.id,
|
||||
},
|
||||
chapterId
|
||||
)
|
||||
|
||||
setIsSubmitting(false);
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
if (selectedView === "youtube") {
|
||||
if (selectedView === 'youtube') {
|
||||
let external_video_object: ExternalVideoObject = {
|
||||
name,
|
||||
type: "youtube",
|
||||
type: 'youtube',
|
||||
uri: youtubeUrl,
|
||||
chapter_id: chapterId
|
||||
chapter_id: chapterId,
|
||||
}
|
||||
|
||||
|
||||
let status = await submitExternalVideo(external_video_object, 'activity', chapterId);
|
||||
setIsSubmitting(false);
|
||||
let status = await submitExternalVideo(
|
||||
external_video_object,
|
||||
'activity',
|
||||
chapterId
|
||||
)
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
/* TODO : implement some sort of progress bar for file uploads, it is not possible yet because i'm not using axios.
|
||||
and the actual upload isn't happening here anyway, it's in the submitFileActivity function */
|
||||
|
|
@ -71,7 +91,9 @@ function VideoModal({ submitFileActivity, submitExternalVideo, chapterId, course
|
|||
<FormField name="video-activity-name">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Video name</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide a name for your video activity</FormMessage>
|
||||
<FormMessage match="valueMissing">
|
||||
Please provide a name for your video activity
|
||||
</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Input onChange={handleNameChange} type="text" required />
|
||||
|
|
@ -80,29 +102,67 @@ function VideoModal({ submitFileActivity, submitExternalVideo, chapterId, course
|
|||
<div className="flex flex-col rounded-md bg-gray-50 outline-dashed outline-gray-200">
|
||||
<div className="">
|
||||
<div className="flex m-4 justify-center space-x-2 mb-0">
|
||||
<div onClick={() => { setSelectedView("file") }} className="rounded-full bg-slate-900 text-zinc-50 py-2 px-4 text-sm drop-shadow-md hover:cursor-pointer hover:bg-slate-700 ">Video upload</div>
|
||||
<div onClick={() => { setSelectedView("youtube") }} className="rounded-full bg-slate-900 text-zinc-50 py-2 px-4 text-sm drop-shadow-md hover:cursor-pointer hover:bg-slate-700">YouTube Video</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
setSelectedView('file')
|
||||
}}
|
||||
className="rounded-full bg-slate-900 text-zinc-50 py-2 px-4 text-sm drop-shadow-md hover:cursor-pointer hover:bg-slate-700 "
|
||||
>
|
||||
Video upload
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
setSelectedView('youtube')
|
||||
}}
|
||||
className="rounded-full bg-slate-900 text-zinc-50 py-2 px-4 text-sm drop-shadow-md hover:cursor-pointer hover:bg-slate-700"
|
||||
>
|
||||
YouTube Video
|
||||
</div>
|
||||
</div>
|
||||
{selectedView === "file" && (<div className="p-4 justify-center m-auto align-middle">
|
||||
<FormField name="video-activity-file">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Video file</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide a video for your activity</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<input type="file" onChange={handleVideoChange} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
</div>)}
|
||||
{selectedView === "youtube" && (
|
||||
{selectedView === 'file' && (
|
||||
<div className="p-4 justify-center m-auto align-middle">
|
||||
<FormField name="video-activity-file">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel className="flex justify-center align-middle"><Youtube className="m-auto pr-1" /><span className="flex">YouTube URL</span></FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide a video for your activity</FormMessage>
|
||||
<Flex
|
||||
css={{
|
||||
alignItems: 'baseline',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<FormLabel>Video file</FormLabel>
|
||||
<FormMessage match="valueMissing">
|
||||
Please provide a video for your activity
|
||||
</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Input className="bg-white" onChange={handleYoutubeUrlChange} type="text" required />
|
||||
<input type="file" onChange={handleVideoChange} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
</div>
|
||||
)}
|
||||
{selectedView === 'youtube' && (
|
||||
<div className="p-4 justify-center m-auto align-middle">
|
||||
<FormField name="video-activity-file">
|
||||
<Flex
|
||||
css={{
|
||||
alignItems: 'baseline',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<FormLabel className="flex justify-center align-middle">
|
||||
<Youtube className="m-auto pr-1" />
|
||||
<span className="flex">YouTube URL</span>
|
||||
</FormLabel>
|
||||
<FormMessage match="valueMissing">
|
||||
Please provide a video for your activity
|
||||
</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Input
|
||||
className="bg-white"
|
||||
onChange={handleYoutubeUrlChange}
|
||||
type="text"
|
||||
required
|
||||
/>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
</div>
|
||||
|
|
@ -112,13 +172,25 @@ function VideoModal({ submitFileActivity, submitExternalVideo, chapterId, course
|
|||
|
||||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Form.Submit asChild>
|
||||
<ButtonBlack className="bg-black" type="submit" css={{ marginTop: 10 }}>
|
||||
{isSubmitting ? <BarLoader cssOverride={{ borderRadius: 60, }} width={60} color="#ffffff" /> : "Create activity"}
|
||||
<ButtonBlack
|
||||
className="bg-black"
|
||||
type="submit"
|
||||
css={{ marginTop: 10 }}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<BarLoader
|
||||
cssOverride={{ borderRadius: 60 }}
|
||||
width={60}
|
||||
color="#ffffff"
|
||||
/>
|
||||
) : (
|
||||
'Create activity'
|
||||
)}
|
||||
</ButtonBlack>
|
||||
</Form.Submit>
|
||||
</Flex>
|
||||
</FormLayout>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default VideoModal;
|
||||
export default VideoModal
|
||||
|
|
|
|||
|
|
@ -1,44 +1,52 @@
|
|||
import FormLayout, { Flex, FormField, Input, Textarea, FormLabel, ButtonBlack } from "@components/StyledElements/Form/Form";
|
||||
import { FormMessage } from "@radix-ui/react-form";
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
import React, { useState } from "react";
|
||||
import BarLoader from "react-spinners/BarLoader";
|
||||
import FormLayout, {
|
||||
Flex,
|
||||
FormField,
|
||||
Input,
|
||||
Textarea,
|
||||
FormLabel,
|
||||
ButtonBlack,
|
||||
} from '@components/StyledElements/Form/Form'
|
||||
import { FormMessage } from '@radix-ui/react-form'
|
||||
import * as Form from '@radix-ui/react-form'
|
||||
import React, { useState } from 'react'
|
||||
import BarLoader from 'react-spinners/BarLoader'
|
||||
|
||||
function NewChapterModal({ submitChapter, closeModal, course }: any) {
|
||||
const [chapterName, setChapterName] = useState("");
|
||||
const [chapterDescription, setChapterDescription] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [chapterName, setChapterName] = useState('')
|
||||
const [chapterDescription, setChapterDescription] = useState('')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const handleChapterNameChange = (e: any) => {
|
||||
setChapterName(e.target.value);
|
||||
};
|
||||
setChapterName(e.target.value)
|
||||
}
|
||||
|
||||
const handleChapterDescriptionChange = (e: any) => {
|
||||
setChapterDescription(e.target.value);
|
||||
};
|
||||
setChapterDescription(e.target.value)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
setIsSubmitting(true);
|
||||
setIsSubmitting(true)
|
||||
const chapter_object = {
|
||||
name: chapterName,
|
||||
description: chapterDescription,
|
||||
thumbnail_image: "",
|
||||
thumbnail_image: '',
|
||||
course_id: course.id,
|
||||
org_id: course.org_id
|
||||
|
||||
};
|
||||
await submitChapter(chapter_object);
|
||||
setIsSubmitting(false);
|
||||
};
|
||||
org_id: course.org_id,
|
||||
}
|
||||
await submitChapter(chapter_object)
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<FormLayout onSubmit={handleSubmit}>
|
||||
<FormField name="chapter-name">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Chapter name</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide a chapter name</FormMessage>
|
||||
<FormMessage match="valueMissing">
|
||||
Please provide a chapter name
|
||||
</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Input onChange={handleChapterNameChange} type="text" required />
|
||||
|
|
@ -47,7 +55,9 @@ function NewChapterModal({ submitChapter, closeModal, course }: any) {
|
|||
<FormField name="chapter-desc">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Chapter description</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide a chapter description</FormMessage>
|
||||
<FormMessage match="valueMissing">
|
||||
Please provide a chapter description
|
||||
</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Textarea onChange={handleChapterDescriptionChange} required />
|
||||
|
|
@ -57,13 +67,20 @@ function NewChapterModal({ submitChapter, closeModal, course }: any) {
|
|||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Form.Submit asChild>
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
||||
{isSubmitting ? <BarLoader cssOverride={{ borderRadius: 60, }} width={60} color="#ffffff" />
|
||||
: "Create Chapter"}
|
||||
{isSubmitting ? (
|
||||
<BarLoader
|
||||
cssOverride={{ borderRadius: 60 }}
|
||||
width={60}
|
||||
color="#ffffff"
|
||||
/>
|
||||
) : (
|
||||
'Create Chapter'
|
||||
)}
|
||||
</ButtonBlack>
|
||||
</Form.Submit>
|
||||
</Flex>
|
||||
</FormLayout>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default NewChapterModal;
|
||||
export default NewChapterModal
|
||||
|
|
|
|||
|
|
@ -1,151 +1,183 @@
|
|||
'use client';
|
||||
import FormLayout, { ButtonBlack, Flex, FormField, FormLabel, Input, Textarea } from '@components/StyledElements/Form/Form'
|
||||
'use client'
|
||||
import FormLayout, {
|
||||
ButtonBlack,
|
||||
Flex,
|
||||
FormField,
|
||||
FormLabel,
|
||||
Input,
|
||||
Textarea,
|
||||
} from '@components/StyledElements/Form/Form'
|
||||
import * as Form from '@radix-ui/react-form'
|
||||
import { FormMessage } from "@radix-ui/react-form";
|
||||
import { createNewCourse } from '@services/courses/courses';
|
||||
import { getOrganizationContextInfoWithoutCredentials } from '@services/organizations/orgs';
|
||||
import { FormMessage } from '@radix-ui/react-form'
|
||||
import { createNewCourse } from '@services/courses/courses'
|
||||
import { getOrganizationContextInfoWithoutCredentials } from '@services/organizations/orgs'
|
||||
import React, { useState } from 'react'
|
||||
import { BarLoader } from 'react-spinners'
|
||||
import { revalidateTags } from '@services/utils/ts/requests';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { revalidateTags } from '@services/utils/ts/requests'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
function CreateCourseModal({ closeModal, orgslug }: any) {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [name, setName] = React.useState("");
|
||||
const [description, setDescription] = React.useState("");
|
||||
const [learnings, setLearnings] = React.useState("");
|
||||
const [visibility, setVisibility] = React.useState(true);
|
||||
const [tags, setTags] = React.useState("");
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [thumbnail, setThumbnail] = React.useState(null) as any;
|
||||
const router = useRouter();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [name, setName] = React.useState('')
|
||||
const [description, setDescription] = React.useState('')
|
||||
const [learnings, setLearnings] = React.useState('')
|
||||
const [visibility, setVisibility] = React.useState(true)
|
||||
const [tags, setTags] = React.useState('')
|
||||
const [isLoading, setIsLoading] = React.useState(false)
|
||||
const [thumbnail, setThumbnail] = React.useState(null) as any
|
||||
const router = useRouter()
|
||||
|
||||
const [orgId, setOrgId] = React.useState(null) as any;
|
||||
const [org, setOrg] = React.useState(null) as any;
|
||||
const [orgId, setOrgId] = React.useState(null) as any
|
||||
const [org, setOrg] = React.useState(null) as any
|
||||
|
||||
const getOrgMetadata = async () => {
|
||||
const org = await getOrganizationContextInfoWithoutCredentials(orgslug, {
|
||||
revalidate: 360,
|
||||
tags: ['organizations'],
|
||||
})
|
||||
|
||||
const getOrgMetadata = async () => {
|
||||
const org = await getOrganizationContextInfoWithoutCredentials(orgslug, { revalidate: 360, tags: ['organizations'] });
|
||||
setOrgId(org.id)
|
||||
}
|
||||
|
||||
setOrgId(org.id);
|
||||
}
|
||||
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(event.target.value)
|
||||
}
|
||||
|
||||
const handleDescriptionChange = (event: React.ChangeEvent<any>) => {
|
||||
setDescription(event.target.value)
|
||||
}
|
||||
|
||||
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(event.target.value);
|
||||
};
|
||||
const handleLearningsChange = (event: React.ChangeEvent<any>) => {
|
||||
setLearnings(event.target.value)
|
||||
}
|
||||
|
||||
const handleDescriptionChange = (event: React.ChangeEvent<any>) => {
|
||||
setDescription(event.target.value);
|
||||
};
|
||||
const handleVisibilityChange = (event: React.ChangeEvent<any>) => {
|
||||
setVisibility(event.target.value)
|
||||
console.log(visibility)
|
||||
}
|
||||
|
||||
const handleLearningsChange = (event: React.ChangeEvent<any>) => {
|
||||
setLearnings(event.target.value);
|
||||
}
|
||||
const handleTagsChange = (event: React.ChangeEvent<any>) => {
|
||||
setTags(event.target.value)
|
||||
}
|
||||
|
||||
const handleVisibilityChange = (event: React.ChangeEvent<any>) => {
|
||||
setVisibility(event.target.value);
|
||||
console.log(visibility);
|
||||
}
|
||||
const handleThumbnailChange = (event: React.ChangeEvent<any>) => {
|
||||
setThumbnail(event.target.files[0])
|
||||
}
|
||||
|
||||
const handleTagsChange = (event: React.ChangeEvent<any>) => {
|
||||
setTags(event.target.value);
|
||||
}
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
|
||||
|
||||
const handleThumbnailChange = (event: React.ChangeEvent<any>) => {
|
||||
setThumbnail(event.target.files[0]);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
|
||||
|
||||
let status = await createNewCourse(orgId, { name, description, tags, visibility }, thumbnail);
|
||||
await revalidateTags(['courses'], orgslug);
|
||||
setIsSubmitting(false);
|
||||
|
||||
if (status.org_id == orgId) {
|
||||
closeModal();
|
||||
router.refresh();
|
||||
await revalidateTags(['courses'], orgslug);
|
||||
|
||||
// refresh page (FIX for Next.js BUG)
|
||||
// window.location.reload();
|
||||
} else {
|
||||
alert("Error creating course, please see console logs");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (orgslug) {
|
||||
getOrgMetadata();
|
||||
}
|
||||
}, [isLoading, orgslug]);
|
||||
|
||||
return (
|
||||
<FormLayout onSubmit={handleSubmit}>
|
||||
<FormField name="course-name">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Course name</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide a course name</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Input onChange={handleNameChange} type="text" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
<FormField name="course-desc">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Course description</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide a course description</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Textarea onChange={handleDescriptionChange} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
<FormField name="course-thumbnail">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Course thumbnail</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide a thumbnail for your course</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Input onChange={handleThumbnailChange} type="file" />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
<FormField name="course-tags">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Course Learnings (separated by comma)</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide learning elements, separated by comma (,)</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Textarea onChange={handleTagsChange} />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
<FormField name="course-visibility">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Course Visibility</FormLabel>
|
||||
<FormMessage match="valueMissing">Please choose course visibility</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<select onChange={handleVisibilityChange} className='border border-gray-300 rounded-md p-2' required>
|
||||
<option value="true">Public (Available to see on the internet) </option>
|
||||
<option value="false">Private (Private to users) </option>
|
||||
</select>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Form.Submit asChild>
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
||||
{isSubmitting ? <BarLoader cssOverride={{ borderRadius: 60, }} width={60} color="#ffffff" />
|
||||
: "Create Course"}
|
||||
</ButtonBlack>
|
||||
</Form.Submit>
|
||||
</Flex>
|
||||
</FormLayout>
|
||||
let status = await createNewCourse(
|
||||
orgId,
|
||||
{ name, description, tags, visibility },
|
||||
thumbnail
|
||||
)
|
||||
await revalidateTags(['courses'], orgslug)
|
||||
setIsSubmitting(false)
|
||||
|
||||
if (status.org_id == orgId) {
|
||||
closeModal()
|
||||
router.refresh()
|
||||
await revalidateTags(['courses'], orgslug)
|
||||
|
||||
// refresh page (FIX for Next.js BUG)
|
||||
// window.location.reload();
|
||||
} else {
|
||||
alert('Error creating course, please see console logs')
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (orgslug) {
|
||||
getOrgMetadata()
|
||||
}
|
||||
}, [isLoading, orgslug])
|
||||
|
||||
return (
|
||||
<FormLayout onSubmit={handleSubmit}>
|
||||
<FormField name="course-name">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Course name</FormLabel>
|
||||
<FormMessage match="valueMissing">
|
||||
Please provide a course name
|
||||
</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Input onChange={handleNameChange} type="text" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
<FormField name="course-desc">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Course description</FormLabel>
|
||||
<FormMessage match="valueMissing">
|
||||
Please provide a course description
|
||||
</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Textarea onChange={handleDescriptionChange} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
<FormField name="course-thumbnail">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Course thumbnail</FormLabel>
|
||||
<FormMessage match="valueMissing">
|
||||
Please provide a thumbnail for your course
|
||||
</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Input onChange={handleThumbnailChange} type="file" />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
<FormField name="course-tags">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Course Learnings (separated by comma)</FormLabel>
|
||||
<FormMessage match="valueMissing">
|
||||
Please provide learning elements, separated by comma (,)
|
||||
</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Textarea onChange={handleTagsChange} />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
<FormField name="course-visibility">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Course Visibility</FormLabel>
|
||||
<FormMessage match="valueMissing">
|
||||
Please choose course visibility
|
||||
</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<select
|
||||
onChange={handleVisibilityChange}
|
||||
className="border border-gray-300 rounded-md p-2"
|
||||
required
|
||||
>
|
||||
<option value="true">
|
||||
Public (Available to see on the internet){' '}
|
||||
</option>
|
||||
<option value="false">Private (Private to users) </option>
|
||||
</select>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Form.Submit asChild>
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
||||
{isSubmitting ? (
|
||||
<BarLoader
|
||||
cssOverride={{ borderRadius: 60 }}
|
||||
width={60}
|
||||
color="#ffffff"
|
||||
/>
|
||||
) : (
|
||||
'Create Course'
|
||||
)}
|
||||
</ButtonBlack>
|
||||
</Form.Submit>
|
||||
</Flex>
|
||||
</FormLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateCourseModal
|
||||
export default CreateCourseModal
|
||||
|
|
|
|||
|
|
@ -1,81 +1,105 @@
|
|||
'use client';
|
||||
import { useOrg } from '@components/Contexts/OrgContext';
|
||||
import FormLayout, { ButtonBlack, Flex, FormField, FormLabel } from '@components/StyledElements/Form/Form'
|
||||
'use client'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import FormLayout, {
|
||||
ButtonBlack,
|
||||
Flex,
|
||||
FormField,
|
||||
FormLabel,
|
||||
} from '@components/StyledElements/Form/Form'
|
||||
import * as Form from '@radix-ui/react-form'
|
||||
import { FormMessage } from "@radix-ui/react-form";
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
import { updateUserRole } from '@services/organizations/orgs';
|
||||
import { FormMessage } from '@radix-ui/react-form'
|
||||
import { getAPIUrl } from '@services/config/config'
|
||||
import { updateUserRole } from '@services/organizations/orgs'
|
||||
import React, { useEffect } from 'react'
|
||||
import { BarLoader } from 'react-spinners';
|
||||
import { mutate } from 'swr';
|
||||
import { BarLoader } from 'react-spinners'
|
||||
import { mutate } from 'swr'
|
||||
|
||||
interface Props {
|
||||
user: any
|
||||
setRolesModal: any
|
||||
alreadyAssignedRole: any
|
||||
user: any
|
||||
setRolesModal: any
|
||||
alreadyAssignedRole: any
|
||||
}
|
||||
|
||||
function RolesUpdate(props: Props) {
|
||||
const org = useOrg() as any;
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
const [assignedRole, setAssignedRole] = React.useState(props.alreadyAssignedRole);
|
||||
const [error, setError] = React.useState(null) as any;
|
||||
const org = useOrg() as any
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
||||
const [assignedRole, setAssignedRole] = React.useState(
|
||||
props.alreadyAssignedRole
|
||||
)
|
||||
const [error, setError] = React.useState(null) as any
|
||||
|
||||
const handleAssignedRole = (event: React.ChangeEvent<any>) => {
|
||||
setError(null);
|
||||
setAssignedRole(event.target.value);
|
||||
const handleAssignedRole = (event: React.ChangeEvent<any>) => {
|
||||
setError(null)
|
||||
setAssignedRole(event.target.value)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
const res = await updateUserRole(org.id, props.user.user.id, assignedRole)
|
||||
|
||||
if (res.status === 200) {
|
||||
await mutate(`${getAPIUrl()}orgs/${org.id}/users`)
|
||||
props.setRolesModal(false)
|
||||
} else {
|
||||
setIsSubmitting(false)
|
||||
setError('Error ' + res.status + ': ' + res.data.detail)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
const res = await updateUserRole(org.id, props.user.user.id, assignedRole);
|
||||
useEffect(() => {}, [assignedRole])
|
||||
|
||||
if (res.status === 200) {
|
||||
await mutate(`${getAPIUrl()}orgs/${org.id}/users`);
|
||||
props.setRolesModal(false);
|
||||
}
|
||||
else {
|
||||
setIsSubmitting(false);
|
||||
setError('Error ' + res.status + ': ' + res.data.detail);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
}
|
||||
, [assignedRole])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormLayout onSubmit={handleSubmit}>
|
||||
<FormField name="course-visibility">
|
||||
{error ? <div className='text-red-500 font-bold text-xs px-3 py-2 bg-red-100 rounded-md'>{error}</div> : ''}
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Roles</FormLabel>
|
||||
<FormMessage match="valueMissing">Please choose a role for the user</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<select onChange={handleAssignedRole} defaultValue={assignedRole} className='border border-gray-300 rounded-md p-2' required>
|
||||
<option value="role_global_admin">Admin </option>
|
||||
<option value="role_global_maintainer">Maintainer</option>
|
||||
<option value="role_global_user">User</option>
|
||||
</select>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
<div className='h-full'></div>
|
||||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Form.Submit asChild>
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
||||
{isSubmitting ? <BarLoader cssOverride={{ borderRadius: 60, }} width={60} color="#ffffff" />
|
||||
: "Update user role"}
|
||||
</ButtonBlack>
|
||||
</Form.Submit>
|
||||
</Flex>
|
||||
</FormLayout>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
<FormLayout onSubmit={handleSubmit}>
|
||||
<FormField name="course-visibility">
|
||||
{error ? (
|
||||
<div className="text-red-500 font-bold text-xs px-3 py-2 bg-red-100 rounded-md">
|
||||
{error}
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<Flex
|
||||
css={{ alignItems: 'baseline', justifyContent: 'space-between' }}
|
||||
>
|
||||
<FormLabel>Roles</FormLabel>
|
||||
<FormMessage match="valueMissing">
|
||||
Please choose a role for the user
|
||||
</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<select
|
||||
onChange={handleAssignedRole}
|
||||
defaultValue={assignedRole}
|
||||
className="border border-gray-300 rounded-md p-2"
|
||||
required
|
||||
>
|
||||
<option value="role_global_admin">Admin </option>
|
||||
<option value="role_global_maintainer">Maintainer</option>
|
||||
<option value="role_global_user">User</option>
|
||||
</select>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
<div className="h-full"></div>
|
||||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Form.Submit asChild>
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
||||
{isSubmitting ? (
|
||||
<BarLoader
|
||||
cssOverride={{ borderRadius: 60 }}
|
||||
width={60}
|
||||
color="#ffffff"
|
||||
/>
|
||||
) : (
|
||||
'Update user role'
|
||||
)}
|
||||
</ButtonBlack>
|
||||
</Form.Submit>
|
||||
</Flex>
|
||||
</FormLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RolesUpdate
|
||||
export default RolesUpdate
|
||||
|
|
|
|||
|
|
@ -1,87 +1,111 @@
|
|||
import FormLayout, { ButtonBlack, Flex, FormField, FormLabel, FormMessage, Textarea } from "@components/StyledElements/Form/Form"
|
||||
import { BarLoader } from "react-spinners"
|
||||
import FormLayout, {
|
||||
ButtonBlack,
|
||||
Flex,
|
||||
FormField,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Textarea,
|
||||
} from '@components/StyledElements/Form/Form'
|
||||
import { BarLoader } from 'react-spinners'
|
||||
import * as Form from '@radix-ui/react-form'
|
||||
import React, { useState } from "react";
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { CheckCircleIcon } from "lucide-react";
|
||||
import { useSession } from "@components/Contexts/SessionContext";
|
||||
import React, { useState } from 'react'
|
||||
import * as Sentry from '@sentry/browser'
|
||||
import { CheckCircleIcon } from 'lucide-react'
|
||||
import { useSession } from '@components/Contexts/SessionContext'
|
||||
|
||||
export const FeedbackModal = (user: any) => {
|
||||
const session = useSession() as any;
|
||||
const session = useSession() as any
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [view, setView] = useState<"feedbackForm" | "success">("feedbackForm")
|
||||
const [feedbackMessage, setFeedbackMessage] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [view, setView] = useState<'feedbackForm' | 'success'>('feedbackForm')
|
||||
const [feedbackMessage, setFeedbackMessage] = useState('')
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
const user = session.user ? session.user : null
|
||||
const eventId = Sentry.captureMessage(
|
||||
`Feedback from ${user ? user.email : 'Anonymous'} - ${feedbackMessage}`
|
||||
)
|
||||
|
||||
const user = session.user ? session.user : null;
|
||||
const eventId = Sentry.captureMessage(`Feedback from ${user ? user.email : 'Anonymous'} - ${feedbackMessage}`);
|
||||
|
||||
const userFeedback = {
|
||||
event_id: eventId,
|
||||
name: user ? user.full_name : 'Anonymous',
|
||||
email: user ? user.email : 'Anonymous',
|
||||
comments: feedbackMessage,
|
||||
}
|
||||
Sentry.captureUserFeedback(userFeedback);
|
||||
setIsSubmitting(false);
|
||||
setView("success");
|
||||
};
|
||||
|
||||
const handleFeedbackMessage = (event: React.ChangeEvent<any>) => {
|
||||
setFeedbackMessage(event.target.value)
|
||||
};
|
||||
|
||||
if (view == "feedbackForm") {
|
||||
return (
|
||||
<FormLayout onSubmit={handleSubmit}>
|
||||
<FormField name="feedback-message">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Feedback message</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide learning elements, separated by comma (,)</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Textarea style={{ height: 150, }} onChange={handleFeedbackMessage} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Form.Submit asChild>
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
||||
{isSubmitting ? <BarLoader cssOverride={{ borderRadius: 60, }} width={60} color="#ffffff" />
|
||||
: "Submit Feedback"}
|
||||
</ButtonBlack>
|
||||
</Form.Submit>
|
||||
</Flex>
|
||||
</FormLayout>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div className="flex flex-col items-center space-y-5">
|
||||
<div className="flex flex-col items-center space-y-5 pt-10">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="text-9xl text-green-500">
|
||||
<CheckCircleIcon></CheckCircleIcon>
|
||||
</div>
|
||||
<div className="text-3xl text-green-500">
|
||||
<div>Thank you for your feedback!</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xl text-gray-500">
|
||||
<div>We will take it into account.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<ButtonBlack onClick={() => setView("feedbackForm")}>Send another feedback</ButtonBlack>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
const userFeedback = {
|
||||
event_id: eventId,
|
||||
name: user ? user.full_name : 'Anonymous',
|
||||
email: user ? user.email : 'Anonymous',
|
||||
comments: feedbackMessage,
|
||||
}
|
||||
Sentry.captureUserFeedback(userFeedback)
|
||||
setIsSubmitting(false)
|
||||
setView('success')
|
||||
}
|
||||
|
||||
const handleFeedbackMessage = (event: React.ChangeEvent<any>) => {
|
||||
setFeedbackMessage(event.target.value)
|
||||
}
|
||||
|
||||
if (view == 'feedbackForm') {
|
||||
return (
|
||||
<FormLayout onSubmit={handleSubmit}>
|
||||
<FormField name="feedback-message">
|
||||
<Flex
|
||||
css={{ alignItems: 'baseline', justifyContent: 'space-between' }}
|
||||
>
|
||||
<FormLabel>Feedback message</FormLabel>
|
||||
<FormMessage match="valueMissing">
|
||||
Please provide learning elements, separated by comma (,)
|
||||
</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Textarea
|
||||
style={{ height: 150 }}
|
||||
onChange={handleFeedbackMessage}
|
||||
required
|
||||
/>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Form.Submit asChild>
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
||||
{isSubmitting ? (
|
||||
<BarLoader
|
||||
cssOverride={{ borderRadius: 60 }}
|
||||
width={60}
|
||||
color="#ffffff"
|
||||
/>
|
||||
) : (
|
||||
'Submit Feedback'
|
||||
)}
|
||||
</ButtonBlack>
|
||||
</Form.Submit>
|
||||
</Flex>
|
||||
</FormLayout>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div className="flex flex-col items-center space-y-5">
|
||||
<div className="flex flex-col items-center space-y-5 pt-10">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="text-9xl text-green-500">
|
||||
<CheckCircleIcon></CheckCircleIcon>
|
||||
</div>
|
||||
<div className="text-3xl text-green-500">
|
||||
<div>Thank you for your feedback!</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xl text-gray-500">
|
||||
<div>We will take it into account.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<ButtonBlack onClick={() => setView('feedbackForm')}>
|
||||
Send another feedback
|
||||
</ButtonBlack>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default FeedbackModal
|
||||
export default FeedbackModal
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"use client";
|
||||
import { useOrg } from '@components/Contexts/OrgContext';
|
||||
'use client'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
|
||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
|
||||
import { getUriWithOrg } from '@services/config/config'
|
||||
|
|
@ -12,71 +12,102 @@ import { useRouter } from 'next/navigation'
|
|||
import React from 'react'
|
||||
|
||||
type PropsType = {
|
||||
collection: any,
|
||||
orgslug: string,
|
||||
org_id: string
|
||||
collection: any
|
||||
orgslug: string
|
||||
org_id: string
|
||||
}
|
||||
|
||||
const removeCollectionPrefix = (collectionid: string) => {
|
||||
return collectionid.replace("collection_", "")
|
||||
return collectionid.replace('collection_', '')
|
||||
}
|
||||
|
||||
function CollectionThumbnail(props: PropsType) {
|
||||
const org = useOrg() as any;
|
||||
return (
|
||||
<div className=''>
|
||||
<div className="flex flex-row space-x-4 inset-0 ring-1 ring-inset my-auto ring-black/10 rounded-xl shadow-xl relative w-[300px] h-[80px] bg-cover items-center justify-center bg-indigo-600 font-bold text-zinc-50" >
|
||||
<div className="flex -space-x-5">
|
||||
{props.collection.courses.slice(0, 2).map((course: any) => (
|
||||
<>
|
||||
<Link href={getUriWithOrg(props.orgslug, "/collection/" + removeCollectionPrefix(props.collection.collection_uuid))}>
|
||||
<div className="inset-0 rounded-full shadow-2xl bg-cover w-12 h-8 justify-center ring-indigo-800 ring-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org?.org_uuid, course.course_uuid, course.thumbnail_image)})` }}>
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
<Link href={getUriWithOrg(props.orgslug, "/collection/" + removeCollectionPrefix(props.collection.collection_uuid))}>
|
||||
<h1 className="font-bold text-md justify-center">{props.collection.name}</h1>
|
||||
</Link>
|
||||
<CollectionAdminEditsArea orgslug={props.orgslug} org_id={props.org_id} collection_uuid={props.collection.collection_uuid} collection={props.collection} />
|
||||
</div>
|
||||
const org = useOrg() as any
|
||||
return (
|
||||
<div className="">
|
||||
<div className="flex flex-row space-x-4 inset-0 ring-1 ring-inset my-auto ring-black/10 rounded-xl shadow-xl relative w-[300px] h-[80px] bg-cover items-center justify-center bg-indigo-600 font-bold text-zinc-50">
|
||||
<div className="flex -space-x-5">
|
||||
{props.collection.courses.slice(0, 2).map((course: any) => (
|
||||
<>
|
||||
<Link
|
||||
href={getUriWithOrg(
|
||||
props.orgslug,
|
||||
'/collection/' +
|
||||
removeCollectionPrefix(props.collection.collection_uuid)
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="inset-0 rounded-full shadow-2xl bg-cover w-12 h-8 justify-center ring-indigo-800 ring-4"
|
||||
style={{
|
||||
backgroundImage: `url(${getCourseThumbnailMediaDirectory(
|
||||
org?.org_uuid,
|
||||
course.course_uuid,
|
||||
course.thumbnail_image
|
||||
)})`,
|
||||
}}
|
||||
></div>
|
||||
</Link>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
<Link
|
||||
href={getUriWithOrg(
|
||||
props.orgslug,
|
||||
'/collection/' +
|
||||
removeCollectionPrefix(props.collection.collection_uuid)
|
||||
)}
|
||||
>
|
||||
<h1 className="font-bold text-md justify-center">
|
||||
{props.collection.name}
|
||||
</h1>
|
||||
</Link>
|
||||
<CollectionAdminEditsArea
|
||||
orgslug={props.orgslug}
|
||||
org_id={props.org_id}
|
||||
collection_uuid={props.collection.collection_uuid}
|
||||
collection={props.collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CollectionAdminEditsArea = (props: any) => {
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
|
||||
const deleteCollectionUI = async (collectionId: number) => {
|
||||
await deleteCollection(collectionId);
|
||||
await revalidateTags(["collections"], props.orgslug);
|
||||
// reload the page
|
||||
router.refresh();
|
||||
}
|
||||
const deleteCollectionUI = async (collectionId: number) => {
|
||||
await deleteCollection(collectionId)
|
||||
await revalidateTags(['collections'], props.orgslug)
|
||||
// reload the page
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthenticatedClientElement
|
||||
action="delete"
|
||||
ressourceType="collections"
|
||||
orgId={props.org_id} checkMethod='roles'>
|
||||
<div className="flex space-x-1 justify-center mx-auto z-20 ">
|
||||
<ConfirmationModal
|
||||
confirmationMessage="Are you sure you want to delete this collection?"
|
||||
confirmationButtonText="Delete Collection"
|
||||
dialogTitle={"Delete " + props.collection.name + " ?"}
|
||||
dialogTrigger={
|
||||
<div
|
||||
className="hover:cursor-pointer p-1 px-2 bg-red-600 rounded-xl items-center justify-center flex shadow-xl"
|
||||
rel="noopener noreferrer">
|
||||
<X size={10} className="text-rose-200 font-bold" />
|
||||
</div>}
|
||||
functionToExecute={() => deleteCollectionUI(props.collection_uuid)}
|
||||
status='warning'
|
||||
></ConfirmationModal>
|
||||
return (
|
||||
<AuthenticatedClientElement
|
||||
action="delete"
|
||||
ressourceType="collections"
|
||||
orgId={props.org_id}
|
||||
checkMethod="roles"
|
||||
>
|
||||
<div className="flex space-x-1 justify-center mx-auto z-20 ">
|
||||
<ConfirmationModal
|
||||
confirmationMessage="Are you sure you want to delete this collection?"
|
||||
confirmationButtonText="Delete Collection"
|
||||
dialogTitle={'Delete ' + props.collection.name + ' ?'}
|
||||
dialogTrigger={
|
||||
<div
|
||||
className="hover:cursor-pointer p-1 px-2 bg-red-600 rounded-xl items-center justify-center flex shadow-xl"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<X size={10} className="text-rose-200 font-bold" />
|
||||
</div>
|
||||
</AuthenticatedClientElement>
|
||||
)
|
||||
}
|
||||
functionToExecute={() => deleteCollectionUI(props.collection_uuid)}
|
||||
status="warning"
|
||||
></ConfirmationModal>
|
||||
</div>
|
||||
</AuthenticatedClientElement>
|
||||
)
|
||||
}
|
||||
|
||||
export default CollectionThumbnail
|
||||
export default CollectionThumbnail
|
||||
|
|
|
|||
|
|
@ -1,85 +1,126 @@
|
|||
"use client";
|
||||
import { useOrg } from '@components/Contexts/OrgContext';
|
||||
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
|
||||
import { getUriWithOrg } from '@services/config/config';
|
||||
import { deleteCourseFromBackend } from '@services/courses/courses';
|
||||
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
|
||||
import { revalidateTags } from '@services/utils/ts/requests';
|
||||
import { Settings, X } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
'use client'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
|
||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
|
||||
import { getUriWithOrg } from '@services/config/config'
|
||||
import { deleteCourseFromBackend } from '@services/courses/courses'
|
||||
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
|
||||
import { revalidateTags } from '@services/utils/ts/requests'
|
||||
import { Settings, X } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
type PropsType = {
|
||||
course: any,
|
||||
orgslug: string
|
||||
course: any
|
||||
orgslug: string
|
||||
}
|
||||
|
||||
// function to remove "course_" from the course_uuid
|
||||
function removeCoursePrefix(course_uuid: string) {
|
||||
return course_uuid.replace("course_", "");
|
||||
return course_uuid.replace('course_', '')
|
||||
}
|
||||
|
||||
function CourseThumbnail(props: PropsType) {
|
||||
const router = useRouter();
|
||||
const org = useOrg() as any;
|
||||
const router = useRouter()
|
||||
const org = useOrg() as any
|
||||
|
||||
async function deleteCourses(course_uuid: any) {
|
||||
await deleteCourseFromBackend(course_uuid);
|
||||
await revalidateTags(['courses'], props.orgslug);
|
||||
async function deleteCourses(course_uuid: any) {
|
||||
await deleteCourseFromBackend(course_uuid)
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
|
||||
router.refresh();
|
||||
}
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
useEffect(() => {}, [org])
|
||||
|
||||
}, [org]);
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<AdminEditsArea course={props.course} orgSlug={props.orgslug} courseId={props.course.course_uuid} deleteCourses={deleteCourses} />
|
||||
<Link href={getUriWithOrg(props.orgslug, "/course/" + removeCoursePrefix(props.course.course_uuid))}>
|
||||
|
||||
{props.course.thumbnail_image ? <div className="inset-0 ring-1 ring-inset ring-black/10 rounded-xl shadow-xl w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org?.org_uuid, props.course.course_uuid, props.course.thumbnail_image)})` }} />
|
||||
: <div className="inset-0 ring-1 ring-inset ring-black/10 rounded-xl shadow-xl w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url('../empty_thumbnail.png')` , backgroundSize:'contain' }} />}
|
||||
|
||||
</Link>
|
||||
<h2 className="font-bold text-lg w-[250px] py-2">{props.course.name}</h2>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="relative">
|
||||
<AdminEditsArea
|
||||
course={props.course}
|
||||
orgSlug={props.orgslug}
|
||||
courseId={props.course.course_uuid}
|
||||
deleteCourses={deleteCourses}
|
||||
/>
|
||||
<Link
|
||||
href={getUriWithOrg(
|
||||
props.orgslug,
|
||||
'/course/' + removeCoursePrefix(props.course.course_uuid)
|
||||
)}
|
||||
>
|
||||
{props.course.thumbnail_image ? (
|
||||
<div
|
||||
className="inset-0 ring-1 ring-inset ring-black/10 rounded-xl shadow-xl w-[249px] h-[131px] bg-cover"
|
||||
style={{
|
||||
backgroundImage: `url(${getCourseThumbnailMediaDirectory(
|
||||
org?.org_uuid,
|
||||
props.course.course_uuid,
|
||||
props.course.thumbnail_image
|
||||
)})`,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="inset-0 ring-1 ring-inset ring-black/10 rounded-xl shadow-xl w-[249px] h-[131px] bg-cover"
|
||||
style={{
|
||||
backgroundImage: `url('../empty_thumbnail.png')`,
|
||||
backgroundSize: 'contain',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
<h2 className="font-bold text-lg w-[250px] py-2">{props.course.name}</h2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const AdminEditsArea = (props: { orgSlug: string, courseId: string, course: any, deleteCourses: any }) => {
|
||||
return (
|
||||
<AuthenticatedClientElement
|
||||
action="update"
|
||||
ressourceType="courses"
|
||||
checkMethod='roles' orgId={props.course.org_id}>
|
||||
<div className="flex space-x-2 absolute z-20 bottom-14 right-[15px] transform">
|
||||
<Link href={getUriWithOrg(props.orgSlug, "/dash/courses/course/" + removeCoursePrefix(props.courseId) + "/general")}>
|
||||
<div
|
||||
className=" hover:cursor-pointer p-1 px-4 bg-slate-700 rounded-xl items-center flex shadow-xl"
|
||||
rel="noopener noreferrer">
|
||||
<Settings size={14} className="text-slate-200 font-bold" />
|
||||
</div>
|
||||
</Link>
|
||||
<ConfirmationModal
|
||||
confirmationButtonText='Delete Course'
|
||||
confirmationMessage='Are you sure you want to delete this course?'
|
||||
dialogTitle={'Delete ' + props.course.name + ' ?'}
|
||||
dialogTrigger={
|
||||
<div
|
||||
className=" hover:cursor-pointer p-1 px-4 bg-red-600 rounded-xl items-center justify-center flex shadow-xl"
|
||||
rel="noopener noreferrer">
|
||||
<X size={14} className="text-rose-200 font-bold" />
|
||||
</div>}
|
||||
functionToExecute={() => props.deleteCourses(props.courseId)}
|
||||
status='warning'
|
||||
></ConfirmationModal>
|
||||
const AdminEditsArea = (props: {
|
||||
orgSlug: string
|
||||
courseId: string
|
||||
course: any
|
||||
deleteCourses: any
|
||||
}) => {
|
||||
return (
|
||||
<AuthenticatedClientElement
|
||||
action="update"
|
||||
ressourceType="courses"
|
||||
checkMethod="roles"
|
||||
orgId={props.course.org_id}
|
||||
>
|
||||
<div className="flex space-x-2 absolute z-20 bottom-14 right-[15px] transform">
|
||||
<Link
|
||||
href={getUriWithOrg(
|
||||
props.orgSlug,
|
||||
'/dash/courses/course/' +
|
||||
removeCoursePrefix(props.courseId) +
|
||||
'/general'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className=" hover:cursor-pointer p-1 px-4 bg-slate-700 rounded-xl items-center flex shadow-xl"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Settings size={14} className="text-slate-200 font-bold" />
|
||||
</div>
|
||||
</Link>
|
||||
<ConfirmationModal
|
||||
confirmationButtonText="Delete Course"
|
||||
confirmationMessage="Are you sure you want to delete this course?"
|
||||
dialogTitle={'Delete ' + props.course.name + ' ?'}
|
||||
dialogTrigger={
|
||||
<div
|
||||
className=" hover:cursor-pointer p-1 px-4 bg-red-600 rounded-xl items-center justify-center flex shadow-xl"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<X size={14} className="text-rose-200 font-bold" />
|
||||
</div>
|
||||
</AuthenticatedClientElement>
|
||||
)
|
||||
}
|
||||
functionToExecute={() => props.deleteCourses(props.courseId)}
|
||||
status="warning"
|
||||
></ConfirmationModal>
|
||||
</div>
|
||||
</AuthenticatedClientElement>
|
||||
)
|
||||
}
|
||||
|
||||
export default CourseThumbnail
|
||||
export default CourseThumbnail
|
||||
|
|
|
|||
|
|
@ -1,61 +1,74 @@
|
|||
import { useSession } from '@components/Contexts/SessionContext';
|
||||
import { useSession } from '@components/Contexts/SessionContext'
|
||||
import React, { useEffect } from 'react'
|
||||
import { getUriWithOrg } from '@services/config/config';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { getUserAvatarMediaDirectory } from '@services/media/media';
|
||||
import { getUriWithOrg } from '@services/config/config'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { getUserAvatarMediaDirectory } from '@services/media/media'
|
||||
|
||||
type UserAvatarProps = {
|
||||
width?: number
|
||||
avatar_url?: string
|
||||
use_with_session?: boolean
|
||||
rounded?: 'rounded-md' | 'rounded-xl' | 'rounded-lg' | 'rounded-full' | 'rounded'
|
||||
border?: 'border-2' | 'border-4' | 'border-8'
|
||||
borderColor? : string
|
||||
predefined_avatar?: 'ai'
|
||||
width?: number
|
||||
avatar_url?: string
|
||||
use_with_session?: boolean
|
||||
rounded?:
|
||||
| 'rounded-md'
|
||||
| 'rounded-xl'
|
||||
| 'rounded-lg'
|
||||
| 'rounded-full'
|
||||
| 'rounded'
|
||||
border?: 'border-2' | 'border-4' | 'border-8'
|
||||
borderColor?: string
|
||||
predefined_avatar?: 'ai'
|
||||
}
|
||||
|
||||
function UserAvatar(props: UserAvatarProps) {
|
||||
const session = useSession() as any;
|
||||
const params = useParams() as any;
|
||||
|
||||
const predefinedAvatar = props.predefined_avatar === 'ai' ? getUriWithOrg(params.orgslug,'/ai_avatar.png') : null;
|
||||
const emptyAvatar = getUriWithOrg(params.orgslug,'/empty_avatar.png') as any;
|
||||
const uploadedAvatar = getUserAvatarMediaDirectory(session.user.user_uuid,session.user.avatar_image) as any;
|
||||
const session = useSession() as any
|
||||
const params = useParams() as any
|
||||
|
||||
const useAvatar = () => {
|
||||
if (props.predefined_avatar) {
|
||||
return predefinedAvatar
|
||||
const predefinedAvatar =
|
||||
props.predefined_avatar === 'ai'
|
||||
? getUriWithOrg(params.orgslug, '/ai_avatar.png')
|
||||
: null
|
||||
const emptyAvatar = getUriWithOrg(params.orgslug, '/empty_avatar.png') as any
|
||||
const uploadedAvatar = getUserAvatarMediaDirectory(
|
||||
session.user.user_uuid,
|
||||
session.user.avatar_image
|
||||
) as any
|
||||
|
||||
const useAvatar = () => {
|
||||
if (props.predefined_avatar) {
|
||||
return predefinedAvatar
|
||||
} else {
|
||||
if (props.avatar_url) {
|
||||
console.log('avatar_url', props.avatar_url)
|
||||
return props.avatar_url
|
||||
} else {
|
||||
if (session.user.avatar_image) {
|
||||
return uploadedAvatar
|
||||
} else {
|
||||
if (props.avatar_url) {
|
||||
console.log('avatar_url',props.avatar_url)
|
||||
return props.avatar_url
|
||||
}
|
||||
else {
|
||||
if (session.user.avatar_image) {
|
||||
return uploadedAvatar
|
||||
}
|
||||
else {
|
||||
return emptyAvatar
|
||||
}
|
||||
}
|
||||
return emptyAvatar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log('params', params)
|
||||
}, [session])
|
||||
|
||||
useEffect(() => {
|
||||
console.log('params', params)
|
||||
}
|
||||
, [session])
|
||||
|
||||
return (
|
||||
<img
|
||||
alt='User Avatar'
|
||||
width={props.width ? props.width : 50}
|
||||
height={props.width ? props.width : 50}
|
||||
src={useAvatar()}
|
||||
className={`${props.avatar_url && session.user.avatar_image ? '' : 'bg-gray-700'} ${props.border ? 'border ' + props.border : ''} ${props.borderColor ? props.borderColor : 'border-white'} shadow-xl aspect-square w-[${props.width ? props.width : 50}px] h-[${props.width ? props.width : 50}px] ${props.rounded ? props.rounded : 'rounded-xl'}`}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<img
|
||||
alt="User Avatar"
|
||||
width={props.width ? props.width : 50}
|
||||
height={props.width ? props.width : 50}
|
||||
src={useAvatar()}
|
||||
className={`${
|
||||
props.avatar_url && session.user.avatar_image ? '' : 'bg-gray-700'
|
||||
} ${props.border ? 'border ' + props.border : ''} ${
|
||||
props.borderColor ? props.borderColor : 'border-white'
|
||||
} shadow-xl aspect-square w-[${props.width ? props.width : 50}px] h-[${
|
||||
props.width ? props.width : 50
|
||||
}px] ${props.rounded ? props.rounded : 'rounded-xl'}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserAvatar
|
||||
export default UserAvatar
|
||||
|
|
|
|||
|
|
@ -1,34 +1,50 @@
|
|||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { Draggable } from "react-beautiful-dnd";
|
||||
import { getAPIUrl, getUriWithOrg } from "@services/config/config";
|
||||
import { Video, Sparkles, X, Pencil, MoreVertical, Eye, Save, File } from "lucide-react";
|
||||
import { mutate } from "swr";
|
||||
import { revalidateTags } from "@services/utils/ts/requests";
|
||||
import { useRouter } from "next/navigation";
|
||||
import ConfirmationModal from "@components/StyledElements/ConfirmationModal/ConfirmationModal";
|
||||
import { deleteActivity, updateActivity } from "@services/courses/activities";
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Draggable } from 'react-beautiful-dnd'
|
||||
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
|
||||
import {
|
||||
Video,
|
||||
Sparkles,
|
||||
X,
|
||||
Pencil,
|
||||
MoreVertical,
|
||||
Eye,
|
||||
Save,
|
||||
File,
|
||||
} from 'lucide-react'
|
||||
import { mutate } from 'swr'
|
||||
import { revalidateTags } from '@services/utils/ts/requests'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
|
||||
import { deleteActivity, updateActivity } from '@services/courses/activities'
|
||||
|
||||
interface ModifiedActivityInterface {
|
||||
activityId: string;
|
||||
activityName: string;
|
||||
activityId: string
|
||||
activityName: string
|
||||
}
|
||||
|
||||
function Activity(props: any) {
|
||||
const router = useRouter();
|
||||
const [modifiedActivity, setModifiedActivity] = React.useState<ModifiedActivityInterface | undefined>(undefined);
|
||||
const [selectedActivity, setSelectedActivity] = React.useState<string | undefined>(undefined);
|
||||
const router = useRouter()
|
||||
const [modifiedActivity, setModifiedActivity] = React.useState<
|
||||
ModifiedActivityInterface | undefined
|
||||
>(undefined)
|
||||
const [selectedActivity, setSelectedActivity] = React.useState<
|
||||
string | undefined
|
||||
>(undefined)
|
||||
|
||||
async function removeActivity() {
|
||||
await deleteActivity(props.activity.id);
|
||||
mutate(`${getAPIUrl()}chapters/meta/course_${props.courseid}`);
|
||||
await revalidateTags(['courses'], props.orgslug);
|
||||
router.refresh();
|
||||
await deleteActivity(props.activity.id)
|
||||
mutate(`${getAPIUrl()}chapters/meta/course_${props.courseid}`)
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
async function updateActivityName(activityId: string) {
|
||||
if ((modifiedActivity?.activityId === activityId) && selectedActivity !== undefined) {
|
||||
setSelectedActivity(undefined);
|
||||
if (
|
||||
modifiedActivity?.activityId === activityId &&
|
||||
selectedActivity !== undefined
|
||||
) {
|
||||
setSelectedActivity(undefined)
|
||||
let modifiedActivityCopy = {
|
||||
name: modifiedActivity.activityName,
|
||||
description: '',
|
||||
|
|
@ -39,74 +55,153 @@ function Activity(props: any) {
|
|||
await updateActivity(modifiedActivityCopy, activityId)
|
||||
await mutate(`${getAPIUrl()}chapters/meta/course_${props.courseid}`)
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
router.refresh();
|
||||
router.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Draggable key={props.activity.uuid} draggableId={String(props.activity.uuid)} index={props.index}>
|
||||
<Draggable
|
||||
key={props.activity.uuid}
|
||||
draggableId={String(props.activity.uuid)}
|
||||
index={props.index}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
className="flex flex-row py-2 my-2 rounded-md bg-gray-50 text-gray-500 hover:bg-gray-100 hover:scale-102 hover:shadow space-x-1 w-auto items-center ring-1 ring-inset ring-gray-400/10 shadow-sm transition-all delay-100 duration-75 ease-linear" key={props.activity.id} {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef}>
|
||||
<div className="px-3 text-gray-300 space-x-1 w-28" >
|
||||
{props.activity.type === "video" && <>
|
||||
<div className="flex space-x-2 items-center"><Video size={16} /> <div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full mx-auto justify-center align-middle">Video</div> </div></>}
|
||||
{props.activity.type === "documentpdf" && <><div className="flex space-x-2 items-center"><div className="w-[30px]"><File size={16} /> </div><div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full">Document</div> </div></>}
|
||||
{props.activity.type === "dynamic" && <><div className="flex space-x-2 items-center"><Sparkles size={16} /> <div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full">Dynamic</div> </div></>}
|
||||
className="flex flex-row py-2 my-2 rounded-md bg-gray-50 text-gray-500 hover:bg-gray-100 hover:scale-102 hover:shadow space-x-1 w-auto items-center ring-1 ring-inset ring-gray-400/10 shadow-sm transition-all delay-100 duration-75 ease-linear"
|
||||
key={props.activity.id}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<div className="px-3 text-gray-300 space-x-1 w-28">
|
||||
{props.activity.type === 'video' && (
|
||||
<>
|
||||
<div className="flex space-x-2 items-center">
|
||||
<Video size={16} />{' '}
|
||||
<div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full mx-auto justify-center align-middle">
|
||||
Video
|
||||
</div>{' '}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{props.activity.type === 'documentpdf' && (
|
||||
<>
|
||||
<div className="flex space-x-2 items-center">
|
||||
<div className="w-[30px]">
|
||||
<File size={16} />{' '}
|
||||
</div>
|
||||
<div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full">
|
||||
Document
|
||||
</div>{' '}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{props.activity.type === 'dynamic' && (
|
||||
<>
|
||||
<div className="flex space-x-2 items-center">
|
||||
<Sparkles size={16} />{' '}
|
||||
<div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full">
|
||||
Dynamic
|
||||
</div>{' '}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grow items-center space-x-2 flex mx-auto justify-center">
|
||||
|
||||
{selectedActivity === props.activity.id ?
|
||||
(<div className="chapter-modification-zone text-[7px] text-gray-600 shadow-inner bg-gray-200/60 py-1 px-4 rounded-lg space-x-3">
|
||||
<input type="text" className="bg-transparent outline-none text-xs text-gray-500" placeholder="Activity name" value={modifiedActivity ? modifiedActivity?.activityName : props.activity.name} onChange={(e) => setModifiedActivity({ activityId: props.activity.id, activityName: e.target.value })} />
|
||||
<button onClick={() => updateActivityName(props.activity.id)} className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900">
|
||||
<Save size={11} onClick={() => updateActivityName(props.activity.id)} />
|
||||
{selectedActivity === props.activity.id ? (
|
||||
<div className="chapter-modification-zone text-[7px] text-gray-600 shadow-inner bg-gray-200/60 py-1 px-4 rounded-lg space-x-3">
|
||||
<input
|
||||
type="text"
|
||||
className="bg-transparent outline-none text-xs text-gray-500"
|
||||
placeholder="Activity name"
|
||||
value={
|
||||
modifiedActivity
|
||||
? modifiedActivity?.activityName
|
||||
: props.activity.name
|
||||
}
|
||||
onChange={(e) =>
|
||||
setModifiedActivity({
|
||||
activityId: props.activity.id,
|
||||
activityName: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<button
|
||||
onClick={() => updateActivityName(props.activity.id)}
|
||||
className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900"
|
||||
>
|
||||
<Save
|
||||
size={11}
|
||||
onClick={() => updateActivityName(props.activity.id)}
|
||||
/>
|
||||
</button>
|
||||
</div>) : (<p className="first-letter:uppercase"> {props.activity.name} </p>)}
|
||||
<Pencil onClick={() => setSelectedActivity(props.activity.id)}
|
||||
size={12} className="text-neutral-400 hover:cursor-pointer" />
|
||||
</div>
|
||||
) : (
|
||||
<p className="first-letter:uppercase"> {props.activity.name} </p>
|
||||
)}
|
||||
<Pencil
|
||||
onClick={() => setSelectedActivity(props.activity.id)}
|
||||
size={12}
|
||||
className="text-neutral-400 hover:cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row space-x-2">
|
||||
{props.activity.type === "TYPE_DYNAMIC" && <>
|
||||
<Link
|
||||
href={getUriWithOrg(props.orgslug, "") + `/course/${props.courseid}/activity/${props.activity.uuid.replace("activity_", "")}/edit`}
|
||||
className=" hover:cursor-pointer p-1 px-3 bg-sky-700 rounded-md items-center"
|
||||
rel="noopener noreferrer">
|
||||
<div className="text-sky-100 font-bold text-xs" >Edit </div>
|
||||
</Link>
|
||||
</>}
|
||||
{props.activity.type === 'TYPE_DYNAMIC' && (
|
||||
<>
|
||||
<Link
|
||||
href={
|
||||
getUriWithOrg(props.orgslug, '') +
|
||||
`/course/${
|
||||
props.courseid
|
||||
}/activity/${props.activity.uuid.replace(
|
||||
'activity_',
|
||||
''
|
||||
)}/edit`
|
||||
}
|
||||
className=" hover:cursor-pointer p-1 px-3 bg-sky-700 rounded-md items-center"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="text-sky-100 font-bold text-xs">Edit </div>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<Link
|
||||
href={getUriWithOrg(props.orgslug, "") + `/course/${props.courseid}/activity/${props.activity.uuid.replace("activity_", "")}`}
|
||||
href={
|
||||
getUriWithOrg(props.orgslug, '') +
|
||||
`/course/${
|
||||
props.courseid
|
||||
}/activity/${props.activity.uuid.replace('activity_', '')}`
|
||||
}
|
||||
className=" hover:cursor-pointer p-1 px-3 bg-gray-200 rounded-md"
|
||||
rel="noopener noreferrer">
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Eye strokeWidth={2} size={15} className="text-gray-600" />
|
||||
</Link>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div className="flex flex-row pr-3 space-x-1 items-center">
|
||||
<MoreVertical size={15} className="text-gray-300" />
|
||||
<ConfirmationModal
|
||||
confirmationMessage="Are you sure you want to delete this activity ?"
|
||||
confirmationButtonText="Delete Activity"
|
||||
dialogTitle={"Delete " + props.activity.name + " ?"}
|
||||
dialogTitle={'Delete ' + props.activity.name + ' ?'}
|
||||
dialogTrigger={
|
||||
<div
|
||||
className=" hover:cursor-pointer p-1 px-5 bg-red-600 rounded-md"
|
||||
rel="noopener noreferrer">
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<X size={15} className="text-rose-200 font-bold" />
|
||||
</div>}
|
||||
</div>
|
||||
}
|
||||
functionToExecute={() => removeActivity()}
|
||||
status='warning'
|
||||
></ConfirmationModal></div>
|
||||
status="warning"
|
||||
></ConfirmationModal>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default Activity;
|
||||
export default Activity
|
||||
|
|
|
|||
|
|
@ -1,40 +1,48 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { Droppable, Draggable } from "react-beautiful-dnd";
|
||||
import Activity from "./Activity";
|
||||
import { Hexagon, MoreVertical, Pencil, Save, Sparkles, X } from "lucide-react";
|
||||
import ConfirmationModal from "@components/StyledElements/ConfirmationModal/ConfirmationModal";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { updateChapter } from "@services/courses/chapters";
|
||||
import { mutate } from "swr";
|
||||
import { getAPIUrl } from "@services/config/config";
|
||||
import { revalidateTags } from "@services/utils/ts/requests";
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Droppable, Draggable } from 'react-beautiful-dnd'
|
||||
import Activity from './Activity'
|
||||
import { Hexagon, MoreVertical, Pencil, Save, Sparkles, X } from 'lucide-react'
|
||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { updateChapter } from '@services/courses/chapters'
|
||||
import { mutate } from 'swr'
|
||||
import { getAPIUrl } from '@services/config/config'
|
||||
import { revalidateTags } from '@services/utils/ts/requests'
|
||||
|
||||
interface ModifiedChapterInterface {
|
||||
chapterId: string;
|
||||
chapterName: string;
|
||||
chapterId: string
|
||||
chapterName: string
|
||||
}
|
||||
|
||||
function Chapter(props: any) {
|
||||
const router = useRouter();
|
||||
const [modifiedChapter, setModifiedChapter] = React.useState<ModifiedChapterInterface | undefined>(undefined);
|
||||
const [selectedChapter, setSelectedChapter] = React.useState<string | undefined>(undefined);
|
||||
const router = useRouter()
|
||||
const [modifiedChapter, setModifiedChapter] = React.useState<
|
||||
ModifiedChapterInterface | undefined
|
||||
>(undefined)
|
||||
const [selectedChapter, setSelectedChapter] = React.useState<
|
||||
string | undefined
|
||||
>(undefined)
|
||||
|
||||
async function updateChapterName(chapterId: string) {
|
||||
if (modifiedChapter?.chapterId === chapterId) {
|
||||
setSelectedChapter(undefined);
|
||||
setSelectedChapter(undefined)
|
||||
let modifiedChapterCopy = {
|
||||
name: modifiedChapter.chapterName,
|
||||
}
|
||||
await updateChapter(chapterId, modifiedChapterCopy)
|
||||
await mutate(`${getAPIUrl()}chapters/course/${props.course_uuid}/meta`)
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
router.refresh();
|
||||
router.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Draggable key={props.info.list.chapter.uuid} draggableId={String(props.info.list.chapter.uuid)} index={props.index}>
|
||||
<Draggable
|
||||
key={props.info.list.chapter.uuid}
|
||||
draggableId={String(props.info.list.chapter.uuid)}
|
||||
index={props.index}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<ChapterWrapper
|
||||
{...provided.dragHandleProps}
|
||||
|
|
@ -47,63 +55,121 @@ function Chapter(props: any) {
|
|||
<div className="flex pt-3 pr-3 font-bold text-md items-center space-x-2">
|
||||
<div className="flex grow text-lg space-x-3 items-center rounded-md px-3 py-1">
|
||||
<div className="bg-neutral-100 rounded-md p-2">
|
||||
<Hexagon strokeWidth={3} size={16} className="text-neutral-600 " />
|
||||
<Hexagon
|
||||
strokeWidth={3}
|
||||
size={16}
|
||||
className="text-neutral-600 "
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2 items-center">
|
||||
|
||||
{selectedChapter === props.info.list.chapter.id ?
|
||||
(<div className="chapter-modification-zone bg-neutral-100 py-1 px-4 rounded-lg space-x-3">
|
||||
<input type="text" className="bg-transparent outline-none text-sm text-neutral-700" placeholder="Chapter name" value={modifiedChapter ? modifiedChapter?.chapterName : props.info.list.chapter.name} onChange={(e) => setModifiedChapter({ chapterId: props.info.list.chapter.id, chapterName: e.target.value })} />
|
||||
<button onClick={() => updateChapterName(props.info.list.chapter.id)} className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900">
|
||||
<Save size={15} onClick={() => updateChapterName(props.info.list.chapter.id)} />
|
||||
{selectedChapter === props.info.list.chapter.id ? (
|
||||
<div className="chapter-modification-zone bg-neutral-100 py-1 px-4 rounded-lg space-x-3">
|
||||
<input
|
||||
type="text"
|
||||
className="bg-transparent outline-none text-sm text-neutral-700"
|
||||
placeholder="Chapter name"
|
||||
value={
|
||||
modifiedChapter
|
||||
? modifiedChapter?.chapterName
|
||||
: props.info.list.chapter.name
|
||||
}
|
||||
onChange={(e) =>
|
||||
setModifiedChapter({
|
||||
chapterId: props.info.list.chapter.id,
|
||||
chapterName: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
updateChapterName(props.info.list.chapter.id)
|
||||
}
|
||||
className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900"
|
||||
>
|
||||
<Save
|
||||
size={15}
|
||||
onClick={() =>
|
||||
updateChapterName(props.info.list.chapter.id)
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
</div>) : (<p className="text-neutral-700 first-letter:uppercase">{props.info.list.chapter.name}</p>)}
|
||||
<Pencil size={15} className="text-neutral-600 hover:cursor-pointer" onClick={() => setSelectedChapter(props.info.list.chapter.id)} />
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-neutral-700 first-letter:uppercase">
|
||||
{props.info.list.chapter.name}
|
||||
</p>
|
||||
)}
|
||||
<Pencil
|
||||
size={15}
|
||||
className="text-neutral-600 hover:cursor-pointer"
|
||||
onClick={() => setSelectedChapter(props.info.list.chapter.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<MoreVertical size={15} className="text-gray-300" />
|
||||
<ConfirmationModal
|
||||
confirmationButtonText="Delete Chapter"
|
||||
confirmationMessage="Are you sure you want to delete this chapter?"
|
||||
dialogTitle={"Delete " + props.info.list.chapter.name + " ?"}
|
||||
dialogTitle={'Delete ' + props.info.list.chapter.name + ' ?'}
|
||||
dialogTrigger={
|
||||
<div
|
||||
className=" hover:cursor-pointer p-1 px-4 bg-red-600 rounded-md shadow flex space-x-1 items-center text-rose-100 text-sm"
|
||||
rel="noopener noreferrer">
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<X size={15} className="text-rose-200 font-bold" />
|
||||
<p>Delete Chapter</p>
|
||||
</div>}
|
||||
functionToExecute={() => props.deleteChapter(props.info.list.chapter.id)}
|
||||
status='warning'
|
||||
</div>
|
||||
}
|
||||
functionToExecute={() =>
|
||||
props.deleteChapter(props.info.list.chapter.id)
|
||||
}
|
||||
status="warning"
|
||||
></ConfirmationModal>
|
||||
|
||||
</div>
|
||||
<Droppable key={props.info.list.chapter.id} droppableId={String(props.info.list.chapter.id)} type="activity">
|
||||
<Droppable
|
||||
key={props.info.list.chapter.id}
|
||||
droppableId={String(props.info.list.chapter.id)}
|
||||
type="activity"
|
||||
>
|
||||
{(provided) => (
|
||||
<ActivitiesList {...provided.droppableProps} ref={provided.innerRef}>
|
||||
<ActivitiesList
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
{props.info.list.activities.map((activity: any, index: any) => (
|
||||
<Activity orgslug={props.orgslug} courseid={props.courseid} key={activity.id} activity={activity} index={index}></Activity>
|
||||
))}
|
||||
{props.info.list.activities.map(
|
||||
(activity: any, index: any) => (
|
||||
<Activity
|
||||
orgslug={props.orgslug}
|
||||
courseid={props.courseid}
|
||||
key={activity.id}
|
||||
activity={activity}
|
||||
index={index}
|
||||
></Activity>
|
||||
)
|
||||
)}
|
||||
{provided.placeholder}
|
||||
|
||||
<div onClick={() => {
|
||||
props.openNewActivityModal(props.info.list.chapter.id);
|
||||
}} className="flex space-x-2 items-center py-2 my-3 rounded-md justify-center text-white bg-black hover:cursor-pointer">
|
||||
<div
|
||||
onClick={() => {
|
||||
props.openNewActivityModal(props.info.list.chapter.id)
|
||||
}}
|
||||
className="flex space-x-2 items-center py-2 my-3 rounded-md justify-center text-white bg-black hover:cursor-pointer"
|
||||
>
|
||||
<Sparkles className="" size={17} />
|
||||
<div className="text-sm mx-auto my-auto items-center font-bold">Add Activity + </div>
|
||||
<div className="text-sm mx-auto my-auto items-center font-bold">
|
||||
Add Activity +{' '}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ActivitiesList>
|
||||
|
||||
)}
|
||||
</Droppable>
|
||||
|
||||
</ChapterWrapper>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const ChapterWrapper = styled.div`
|
||||
|
|
@ -115,14 +181,14 @@ const ChapterWrapper = styled.div`
|
|||
border: 1px solid rgba(255, 255, 255, 0.19);
|
||||
box-shadow: 0px 13px 33px -13px rgb(0 0 0 / 12%);
|
||||
transition: all 0.2s ease;
|
||||
h3{
|
||||
h3 {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
const ActivitiesList = styled.div`
|
||||
padding: 10px;
|
||||
`;
|
||||
`
|
||||
|
||||
export default Chapter;
|
||||
export default Chapter
|
||||
|
|
|
|||
|
|
@ -1,20 +1,30 @@
|
|||
export const initialData = {
|
||||
activities: {
|
||||
"activity-1": { id: "activity-1", content: "First activity" },
|
||||
"activity-2": { id: "activity-2", content: "Second activity" },
|
||||
"activity-3": { id: "activity-3", content: "Third activity" },
|
||||
"activity-4": { id: "activity-4", content: "Fourth activity" },
|
||||
"activity-5": { id: "activity-5", content: "Fifth activity" },
|
||||
'activity-1': { id: 'activity-1', content: 'First activity' },
|
||||
'activity-2': { id: 'activity-2', content: 'Second activity' },
|
||||
'activity-3': { id: 'activity-3', content: 'Third activity' },
|
||||
'activity-4': { id: 'activity-4', content: 'Fourth activity' },
|
||||
'activity-5': { id: 'activity-5', content: 'Fifth activity' },
|
||||
},
|
||||
chapters: {
|
||||
"chapter-1": { id: "chapter-1", name: "Chapter 1", activityIds: ["activity-1", "activity-2", "activity-3"] },
|
||||
"chapter-2": { id: "chapter-2", name: "Chapter 2", activityIds: ["activity-4"] },
|
||||
"chapter-3": { id: "chapter-3", name: "Chapter 3", activityIds: ["activity-5"] },
|
||||
'chapter-1': {
|
||||
id: 'chapter-1',
|
||||
name: 'Chapter 1',
|
||||
activityIds: ['activity-1', 'activity-2', 'activity-3'],
|
||||
},
|
||||
'chapter-2': {
|
||||
id: 'chapter-2',
|
||||
name: 'Chapter 2',
|
||||
activityIds: ['activity-4'],
|
||||
},
|
||||
'chapter-3': {
|
||||
id: 'chapter-3',
|
||||
name: 'Chapter 3',
|
||||
activityIds: ['activity-5'],
|
||||
},
|
||||
},
|
||||
|
||||
chapterOrder: ["chapter-1", "chapter-2", "chapter-3"],
|
||||
};
|
||||
|
||||
export const initialData2 = {
|
||||
};
|
||||
chapterOrder: ['chapter-1', 'chapter-2', 'chapter-3'],
|
||||
}
|
||||
|
||||
export const initialData2 = {}
|
||||
|
|
|
|||
|
|
@ -3,77 +3,91 @@ import { getUriWithOrg } from '@services/config/config'
|
|||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
|
||||
interface Props {
|
||||
course: any
|
||||
orgslug: string
|
||||
course_uuid: string
|
||||
current_activity?: any
|
||||
course: any
|
||||
orgslug: string
|
||||
course_uuid: string
|
||||
current_activity?: any
|
||||
}
|
||||
|
||||
function ActivityIndicators(props: Props) {
|
||||
const course = props.course
|
||||
const orgslug = props.orgslug
|
||||
const courseid = props.course_uuid.replace("course_", "")
|
||||
const course = props.course
|
||||
const orgslug = props.orgslug
|
||||
const courseid = props.course_uuid.replace('course_', '')
|
||||
|
||||
const done_activity_style = 'bg-teal-600 hover:bg-teal-700'
|
||||
const black_activity_style = 'bg-black hover:bg-gray-700'
|
||||
const current_activity_style = 'bg-gray-600 animate-pulse hover:bg-gray-700'
|
||||
const done_activity_style = 'bg-teal-600 hover:bg-teal-700'
|
||||
const black_activity_style = 'bg-black hover:bg-gray-700'
|
||||
const current_activity_style = 'bg-gray-600 animate-pulse hover:bg-gray-700'
|
||||
|
||||
const trail = props.course.trail
|
||||
const trail = props.course.trail
|
||||
|
||||
|
||||
function isActivityDone(activity: any) {
|
||||
let run = props.course.trail?.runs.find((run: any) => run.course_id == props.course.id);
|
||||
if (run) {
|
||||
return run.steps.find((step: any) => step.activity_id == activity.id);
|
||||
}
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function isActivityCurrent(activity: any) {
|
||||
let activity_uuid = activity.activity_uuid.replace("activity_", "")
|
||||
if (props.current_activity && props.current_activity == activity_uuid) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function getActivityClass(activity: any) {
|
||||
if (isActivityDone(activity)) {
|
||||
return done_activity_style
|
||||
}
|
||||
if (isActivityCurrent(activity)) {
|
||||
return current_activity_style
|
||||
}
|
||||
return black_activity_style
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='grid grid-flow-col justify-stretch space-x-6'>
|
||||
{course.chapters.map((chapter: any) => {
|
||||
return (
|
||||
<>
|
||||
<div className='grid grid-flow-col justify-stretch space-x-2'>
|
||||
{chapter.activities.map((activity: any) => {
|
||||
return (
|
||||
<ToolTip sideOffset={8} slateBlack content={activity.name} key={activity.activity_uuid}>
|
||||
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.activity_uuid.replace("activity_", "")}`}>
|
||||
<div className={`h-[7px] w-auto ${getActivityClass(activity)} rounded-lg shadow-md`}></div>
|
||||
|
||||
</Link>
|
||||
</ToolTip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
function isActivityDone(activity: any) {
|
||||
let run = props.course.trail?.runs.find(
|
||||
(run: any) => run.course_id == props.course.id
|
||||
)
|
||||
if (run) {
|
||||
return run.steps.find((step: any) => step.activity_id == activity.id)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isActivityCurrent(activity: any) {
|
||||
let activity_uuid = activity.activity_uuid.replace('activity_', '')
|
||||
if (props.current_activity && props.current_activity == activity_uuid) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function getActivityClass(activity: any) {
|
||||
if (isActivityDone(activity)) {
|
||||
return done_activity_style
|
||||
}
|
||||
if (isActivityCurrent(activity)) {
|
||||
return current_activity_style
|
||||
}
|
||||
return black_activity_style
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-col justify-stretch space-x-6">
|
||||
{course.chapters.map((chapter: any) => {
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-flow-col justify-stretch space-x-2">
|
||||
{chapter.activities.map((activity: any) => {
|
||||
return (
|
||||
<ToolTip
|
||||
sideOffset={8}
|
||||
slateBlack
|
||||
content={activity.name}
|
||||
key={activity.activity_uuid}
|
||||
>
|
||||
<Link
|
||||
href={
|
||||
getUriWithOrg(orgslug, '') +
|
||||
`/course/${courseid}/activity/${activity.activity_uuid.replace(
|
||||
'activity_',
|
||||
''
|
||||
)}`
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`h-[7px] w-auto ${getActivityClass(
|
||||
activity
|
||||
)} rounded-lg shadow-md`}
|
||||
></div>
|
||||
</Link>
|
||||
</ToolTip>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActivityIndicators
|
||||
export default ActivityIndicators
|
||||
|
|
|
|||
|
|
@ -1,76 +1,95 @@
|
|||
'use client';
|
||||
import { useOrg } from '@components/Contexts/OrgContext';
|
||||
import { getAPIUrl, getUriWithOrg } from '@services/config/config';
|
||||
import { removeCourse } from '@services/courses/activity';
|
||||
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
|
||||
import { revalidateTags } from '@services/utils/ts/requests';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { mutate } from 'swr';
|
||||
'use client'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
|
||||
import { removeCourse } from '@services/courses/activity'
|
||||
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
|
||||
import { revalidateTags } from '@services/utils/ts/requests'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useEffect } from 'react'
|
||||
import { mutate } from 'swr'
|
||||
|
||||
interface TrailCourseElementProps {
|
||||
course: any
|
||||
run: any
|
||||
orgslug: string
|
||||
course: any
|
||||
run: any
|
||||
orgslug: string
|
||||
}
|
||||
|
||||
function TrailCourseElement(props: TrailCourseElementProps) {
|
||||
const org = useOrg() as any;
|
||||
const courseid = props.course.course_uuid.replace("course_", "")
|
||||
const course = props.course
|
||||
const router = useRouter();
|
||||
const course_total_steps = props.run.course_total_steps
|
||||
const course_completed_steps = props.run.steps.length
|
||||
const orgID = org?.id;
|
||||
const course_progress = Math.round((course_completed_steps / course_total_steps) * 100)
|
||||
const org = useOrg() as any
|
||||
const courseid = props.course.course_uuid.replace('course_', '')
|
||||
const course = props.course
|
||||
const router = useRouter()
|
||||
const course_total_steps = props.run.course_total_steps
|
||||
const course_completed_steps = props.run.steps.length
|
||||
const orgID = org?.id
|
||||
const course_progress = Math.round(
|
||||
(course_completed_steps / course_total_steps) * 100
|
||||
)
|
||||
|
||||
async function quitCourse(course_uuid: string) {
|
||||
// Close activity
|
||||
let activity = await removeCourse(course_uuid, props.orgslug);
|
||||
// Mutate course
|
||||
await revalidateTags(['courses'], props.orgslug);
|
||||
router.refresh();
|
||||
async function quitCourse(course_uuid: string) {
|
||||
// Close activity
|
||||
let activity = await removeCourse(course_uuid, props.orgslug)
|
||||
// Mutate course
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
router.refresh()
|
||||
|
||||
// Mutate
|
||||
mutate(`${getAPIUrl()}trail/org/${orgID}/trail`);
|
||||
}
|
||||
// Mutate
|
||||
mutate(`${getAPIUrl()}trail/org/${orgID}/trail`)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
}
|
||||
, [props.course, org]);
|
||||
useEffect(() => {}, [props.course, org])
|
||||
|
||||
return (
|
||||
<div className='trailcoursebox flex p-3 bg-white rounded-xl' style={{ boxShadow: '0px 4px 7px 0px rgba(0, 0, 0, 0.03)' }}>
|
||||
|
||||
<Link href={getUriWithOrg(props.orgslug, "/course/" + courseid)}>
|
||||
<div className="course_tumbnail inset-0 ring-1 ring-inset ring-black/10 rounded-lg relative h-[50px] w-[72px] bg-cover bg-center" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org.org_uuid, props.course.course_uuid, props.course.thumbnail_image)})`, boxShadow: '0px 4px 7px 0px rgba(0, 0, 0, 0.03)' }}></div>
|
||||
</Link>
|
||||
<div className="course_meta pl-5 flex-grow space-y-1">
|
||||
<div className="course_top">
|
||||
<div className="course_info flex">
|
||||
<div className="course_basic flex flex-col flex-end -space-y-2">
|
||||
<p className='p-0 font-bold text-sm text-gray-700'>Course</p>
|
||||
<div className="course_progress flex items-center space-x-2">
|
||||
<h2 className='font-bold text-xl'>{course.name}</h2>
|
||||
<div className='bg-slate-300 rounded-full w-[10px] h-[5px]'></div>
|
||||
<h2>{course_progress}%</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="course_actions flex-grow flex flex-row-reverse">
|
||||
<button onClick={() => quitCourse(course.course_uuid)} className="bg-red-200 text-red-700 hover:bg-red-300 rounded-full text-xs h-5 px-2 font-bold">Quit Course</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="course_progress indicator w-full">
|
||||
<div className="w-full bg-gray-200 rounded-full h-1.5 ">
|
||||
<div className={`bg-teal-600 h-1.5 rounded-full`} style={{ width: `${course_progress}%` }} ></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
className="trailcoursebox flex p-3 bg-white rounded-xl"
|
||||
style={{ boxShadow: '0px 4px 7px 0px rgba(0, 0, 0, 0.03)' }}
|
||||
>
|
||||
<Link href={getUriWithOrg(props.orgslug, '/course/' + courseid)}>
|
||||
<div
|
||||
className="course_tumbnail inset-0 ring-1 ring-inset ring-black/10 rounded-lg relative h-[50px] w-[72px] bg-cover bg-center"
|
||||
style={{
|
||||
backgroundImage: `url(${getCourseThumbnailMediaDirectory(
|
||||
org.org_uuid,
|
||||
props.course.course_uuid,
|
||||
props.course.thumbnail_image
|
||||
)})`,
|
||||
boxShadow: '0px 4px 7px 0px rgba(0, 0, 0, 0.03)',
|
||||
}}
|
||||
></div>
|
||||
</Link>
|
||||
<div className="course_meta pl-5 flex-grow space-y-1">
|
||||
<div className="course_top">
|
||||
<div className="course_info flex">
|
||||
<div className="course_basic flex flex-col flex-end -space-y-2">
|
||||
<p className="p-0 font-bold text-sm text-gray-700">Course</p>
|
||||
<div className="course_progress flex items-center space-x-2">
|
||||
<h2 className="font-bold text-xl">{course.name}</h2>
|
||||
<div className="bg-slate-300 rounded-full w-[10px] h-[5px]"></div>
|
||||
<h2>{course_progress}%</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="course_actions flex-grow flex flex-row-reverse">
|
||||
<button
|
||||
onClick={() => quitCourse(course.course_uuid)}
|
||||
className="bg-red-200 text-red-700 hover:bg-red-300 rounded-full text-xs h-5 px-2 font-bold"
|
||||
>
|
||||
Quit Course
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
<div className="course_progress indicator w-full">
|
||||
<div className="w-full bg-gray-200 rounded-full h-1.5 ">
|
||||
<div
|
||||
className={`bg-teal-600 h-1.5 rounded-full`}
|
||||
style={{ width: `${course_progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TrailCourseElement
|
||||
export default TrailCourseElement
|
||||
|
|
|
|||
|
|
@ -1,139 +1,135 @@
|
|||
'use client';
|
||||
import { useOrg } from '@components/Contexts/OrgContext';
|
||||
import { useSession } from '@components/Contexts/SessionContext';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
'use client'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import { useSession } from '@components/Contexts/SessionContext'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import React from 'react'
|
||||
|
||||
type AuthorizationProps = {
|
||||
children: React.ReactNode;
|
||||
// Authorize components rendering or page rendering
|
||||
authorizationMode: 'component' | 'page';
|
||||
children: React.ReactNode
|
||||
// Authorize components rendering or page rendering
|
||||
authorizationMode: 'component' | 'page'
|
||||
}
|
||||
|
||||
const ADMIN_PATHS = [
|
||||
'/dash/org/*',
|
||||
'/dash/org',
|
||||
'/dash/users/*',
|
||||
'/dash/users',
|
||||
'/dash/courses/*',
|
||||
'/dash/courses',
|
||||
'/dash/org/settings/general',
|
||||
'/dash/org/*',
|
||||
'/dash/org',
|
||||
'/dash/users/*',
|
||||
'/dash/users',
|
||||
'/dash/courses/*',
|
||||
'/dash/courses',
|
||||
'/dash/org/settings/general',
|
||||
]
|
||||
|
||||
function AdminAuthorization(props: AuthorizationProps) {
|
||||
const session = useSession() as any;
|
||||
const org = useOrg() as any;
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const session = useSession() as any
|
||||
const org = useOrg() as any
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
|
||||
// States
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [isAuthorized, setIsAuthorized] = React.useState(false);
|
||||
// States
|
||||
const [isLoading, setIsLoading] = React.useState(true)
|
||||
const [isAuthorized, setIsAuthorized] = React.useState(false)
|
||||
|
||||
|
||||
// Verify if the user is authenticated
|
||||
const isUserAuthenticated = () => {
|
||||
if (session.isAuthenticated === true) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
// Verify if the user is authenticated
|
||||
const isUserAuthenticated = () => {
|
||||
if (session.isAuthenticated === true) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Verify if the user is an Admin (1), Maintainer (2) or Member (3) of the organization
|
||||
const isUserAdmin = () => {
|
||||
const isAdmin = session.roles.some((role: any) => {
|
||||
return (
|
||||
role.org.id === org.id &&
|
||||
(role.role.id === 1 ||
|
||||
role.role.id === 2 ||
|
||||
role.role.role_uuid === 'role_global_admin' ||
|
||||
role.role.role_uuid === 'role_global_maintainer'
|
||||
)
|
||||
);
|
||||
});
|
||||
return isAdmin;
|
||||
};
|
||||
// Verify if the user is an Admin (1), Maintainer (2) or Member (3) of the organization
|
||||
const isUserAdmin = () => {
|
||||
const isAdmin = session.roles.some((role: any) => {
|
||||
return (
|
||||
role.org.id === org.id &&
|
||||
(role.role.id === 1 ||
|
||||
role.role.id === 2 ||
|
||||
role.role.role_uuid === 'role_global_admin' ||
|
||||
role.role.role_uuid === 'role_global_maintainer')
|
||||
)
|
||||
})
|
||||
return isAdmin
|
||||
}
|
||||
|
||||
function checkPathname(pattern: string, pathname: string) {
|
||||
// Escape special characters in the pattern and replace '*' with a regex pattern
|
||||
const regexPattern = new RegExp(`^${pattern.replace(/\//g, '\\/').replace(/\*/g, '.*')}$`);
|
||||
|
||||
// Test if the pathname matches the regex pattern
|
||||
const isMatch = regexPattern.test(pathname);
|
||||
|
||||
return isMatch;
|
||||
}
|
||||
|
||||
|
||||
const Authorize = () => {
|
||||
if (props.authorizationMode === 'page') {
|
||||
|
||||
// Check if user is in an admin path
|
||||
if (ADMIN_PATHS.some((path) => checkPathname(path, pathname))) {
|
||||
console.log('Admin path')
|
||||
if (isUserAuthenticated()) {
|
||||
// Check if the user is an Admin
|
||||
if (isUserAdmin()) {
|
||||
setIsAuthorized(true);
|
||||
}
|
||||
else {
|
||||
setIsAuthorized(false);
|
||||
router.push('/dash');
|
||||
}
|
||||
}
|
||||
else {
|
||||
router.push('/login');
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
if (isUserAuthenticated()) {
|
||||
setIsAuthorized(true);
|
||||
}
|
||||
else {
|
||||
setIsAuthorized(false);
|
||||
router.push('/login');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (props.authorizationMode === 'component') {
|
||||
// Component mode
|
||||
if (isUserAuthenticated() && isUserAdmin()) {
|
||||
setIsAuthorized(true);
|
||||
}
|
||||
else {
|
||||
setIsAuthorized(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (session.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
Authorize();
|
||||
setIsLoading(false);
|
||||
}, [session, org, pathname])
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
{props.authorizationMode === 'component' && isAuthorized === true && props.children}
|
||||
{props.authorizationMode === 'page' && isAuthorized === true && !isLoading && props.children}
|
||||
{props.authorizationMode === 'page' && isAuthorized === false && !isLoading &&
|
||||
<div className='flex justify-center items-center h-screen'>
|
||||
<h1 className='text-2xl'>You are not authorized to access this page</h1>
|
||||
</div>
|
||||
}
|
||||
|
||||
</>
|
||||
function checkPathname(pattern: string, pathname: string) {
|
||||
// Escape special characters in the pattern and replace '*' with a regex pattern
|
||||
const regexPattern = new RegExp(
|
||||
`^${pattern.replace(/\//g, '\\/').replace(/\*/g, '.*')}$`
|
||||
)
|
||||
|
||||
// Test if the pathname matches the regex pattern
|
||||
const isMatch = regexPattern.test(pathname)
|
||||
|
||||
return isMatch
|
||||
}
|
||||
|
||||
const Authorize = () => {
|
||||
if (props.authorizationMode === 'page') {
|
||||
// Check if user is in an admin path
|
||||
if (ADMIN_PATHS.some((path) => checkPathname(path, pathname))) {
|
||||
console.log('Admin path')
|
||||
if (isUserAuthenticated()) {
|
||||
// Check if the user is an Admin
|
||||
if (isUserAdmin()) {
|
||||
setIsAuthorized(true)
|
||||
} else {
|
||||
setIsAuthorized(false)
|
||||
router.push('/dash')
|
||||
}
|
||||
} else {
|
||||
router.push('/login')
|
||||
}
|
||||
} else {
|
||||
if (isUserAuthenticated()) {
|
||||
setIsAuthorized(true)
|
||||
} else {
|
||||
setIsAuthorized(false)
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (props.authorizationMode === 'component') {
|
||||
// Component mode
|
||||
if (isUserAuthenticated() && isUserAdmin()) {
|
||||
setIsAuthorized(true)
|
||||
} else {
|
||||
setIsAuthorized(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (session.isLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
Authorize()
|
||||
setIsLoading(false)
|
||||
}, [session, org, pathname])
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.authorizationMode === 'component' &&
|
||||
isAuthorized === true &&
|
||||
props.children}
|
||||
{props.authorizationMode === 'page' &&
|
||||
isAuthorized === true &&
|
||||
!isLoading &&
|
||||
props.children}
|
||||
{props.authorizationMode === 'page' &&
|
||||
isAuthorized === false &&
|
||||
!isLoading && (
|
||||
<div className="flex justify-center items-center h-screen">
|
||||
<h1 className="text-2xl">
|
||||
You are not authorized to access this page
|
||||
</h1>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AdminAuthorization
|
||||
export default AdminAuthorization
|
||||
|
|
|
|||
|
|
@ -1,77 +1,82 @@
|
|||
'use client';
|
||||
import React from "react";
|
||||
import { useSession } from "@components/Contexts/SessionContext";
|
||||
import { useOrg } from "@components/Contexts/OrgContext";
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useSession } from '@components/Contexts/SessionContext'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
|
||||
interface AuthenticatedClientElementProps {
|
||||
children: React.ReactNode;
|
||||
checkMethod: 'authentication' | 'roles';
|
||||
orgId?: string;
|
||||
ressourceType?: 'collections' | 'courses' | 'activities' | 'users' | 'organizations';
|
||||
action?: 'create' | 'update' | 'delete' | 'read';
|
||||
children: React.ReactNode
|
||||
checkMethod: 'authentication' | 'roles'
|
||||
orgId?: string
|
||||
ressourceType?:
|
||||
| 'collections'
|
||||
| 'courses'
|
||||
| 'activities'
|
||||
| 'users'
|
||||
| 'organizations'
|
||||
action?: 'create' | 'update' | 'delete' | 'read'
|
||||
}
|
||||
|
||||
export const AuthenticatedClientElement = (
|
||||
props: AuthenticatedClientElementProps
|
||||
) => {
|
||||
const [isAllowed, setIsAllowed] = React.useState(false)
|
||||
const session = useSession() as any
|
||||
const org = useOrg() as any
|
||||
|
||||
|
||||
export const AuthenticatedClientElement = (props: AuthenticatedClientElementProps) => {
|
||||
const [isAllowed, setIsAllowed] = React.useState(false);
|
||||
const session = useSession() as any;
|
||||
const org = useOrg() as any;
|
||||
|
||||
|
||||
function isUserAllowed(roles: any[], action: string, resourceType: string, org_uuid: string): boolean {
|
||||
// Iterate over the user's roles
|
||||
for (const role of roles) {
|
||||
|
||||
// Check if the role is for the right organization
|
||||
if (role.org.org_uuid === org_uuid) {
|
||||
// Check if the user has the role for the resource type
|
||||
if (role.role.rights && role.role.rights[resourceType]) {
|
||||
|
||||
|
||||
// Check if the user is allowed to execute the action
|
||||
const actionKey = `action_${action}`;
|
||||
if (role.role.rights[resourceType][actionKey] === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
function isUserAllowed(
|
||||
roles: any[],
|
||||
action: string,
|
||||
resourceType: string,
|
||||
org_uuid: string
|
||||
): boolean {
|
||||
// Iterate over the user's roles
|
||||
for (const role of roles) {
|
||||
// Check if the role is for the right organization
|
||||
if (role.org.org_uuid === org_uuid) {
|
||||
// Check if the user has the role for the resource type
|
||||
if (role.role.rights && role.role.rights[resourceType]) {
|
||||
// Check if the user is allowed to execute the action
|
||||
const actionKey = `action_${action}`
|
||||
if (role.role.rights[resourceType][actionKey] === true) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// If no role matches the organization, resource type, and action, return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function check() {
|
||||
if (session.isAuthenticated === false) {
|
||||
setIsAllowed(false);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (props.checkMethod === 'authentication') {
|
||||
setIsAllowed(session.isAuthenticated);
|
||||
} else if (props.checkMethod === 'roles') {
|
||||
return setIsAllowed(isUserAllowed(session.roles, props.action!, props.ressourceType!, org.org_uuid));
|
||||
}
|
||||
}
|
||||
// If no role matches the organization, resource type, and action, return false
|
||||
return false
|
||||
}
|
||||
|
||||
function check() {
|
||||
if (session.isAuthenticated === false) {
|
||||
setIsAllowed(false)
|
||||
return
|
||||
} else {
|
||||
if (props.checkMethod === 'authentication') {
|
||||
setIsAllowed(session.isAuthenticated)
|
||||
} else if (props.checkMethod === 'roles') {
|
||||
return setIsAllowed(
|
||||
isUserAllowed(
|
||||
session.roles,
|
||||
props.action!,
|
||||
props.ressourceType!,
|
||||
org.org_uuid
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (session.isLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (session.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
check();
|
||||
}, [session, org])
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAllowed && props.children}
|
||||
</>
|
||||
)
|
||||
|
||||
check()
|
||||
}, [session, org])
|
||||
|
||||
return <>{isAllowed && props.children}</>
|
||||
}
|
||||
|
||||
export default AuthenticatedClientElement
|
||||
export default AuthenticatedClientElement
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
'use client';
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import Link from "next/link";
|
||||
import { Settings } from "lucide-react";
|
||||
import { useSession } from "@components/Contexts/SessionContext";
|
||||
import UserAvatar from "@components/Objects/UserAvatar";
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Link from 'next/link'
|
||||
import { Settings } from 'lucide-react'
|
||||
import { useSession } from '@components/Contexts/SessionContext'
|
||||
import UserAvatar from '@components/Objects/UserAvatar'
|
||||
|
||||
export const HeaderProfileBox = () => {
|
||||
const session = useSession() as any;
|
||||
const session = useSession() as any
|
||||
|
||||
return (
|
||||
<ProfileArea>
|
||||
|
|
@ -15,14 +15,10 @@ export const HeaderProfileBox = () => {
|
|||
<UnidentifiedArea className="flex text-sm text-gray-700 font-bold p-1.5 px-2 rounded-lg">
|
||||
<ul className="flex space-x-3 items-center">
|
||||
<li>
|
||||
<Link href="/login">
|
||||
Login
|
||||
</Link>
|
||||
<Link href="/login">Login</Link>
|
||||
</li>
|
||||
<li className="bg-black rounded-lg shadow-md p-2 px-3 text-white">
|
||||
<Link href="/signup">
|
||||
Sign up
|
||||
</Link>
|
||||
<Link href="/signup">Sign up</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</UnidentifiedArea>
|
||||
|
|
@ -32,35 +28,35 @@ export const HeaderProfileBox = () => {
|
|||
<div className="flex items-center space-x-2">
|
||||
<div className="text-xs">{session.user.username} </div>
|
||||
<div className="py-4">
|
||||
<UserAvatar border="border-4" rounded='rounded-lg' width={30} />
|
||||
<UserAvatar border="border-4" rounded="rounded-lg" width={30} />
|
||||
</div>
|
||||
<Link className="text-gray-600" href={"/dash"}><Settings size={14} /></Link>
|
||||
<Link className="text-gray-600" href={'/dash'}>
|
||||
<Settings size={14} />
|
||||
</Link>
|
||||
</div>
|
||||
</AccountArea>
|
||||
)}
|
||||
</ProfileArea>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const AccountArea = styled.div`
|
||||
display: flex;
|
||||
place-items: center;
|
||||
|
||||
|
||||
img {
|
||||
width: 29px;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
const ProfileArea = styled.div`
|
||||
display: flex;
|
||||
place-items: stretch;
|
||||
place-items: center;
|
||||
`;
|
||||
`
|
||||
|
||||
const UnidentifiedArea = styled.div`
|
||||
display: flex;
|
||||
place-items: stretch;
|
||||
flex-grow: 1;
|
||||
|
||||
|
||||
`;
|
||||
`
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
"use client";
|
||||
'use client'
|
||||
|
||||
function NewCollectionButton() {
|
||||
return (
|
||||
<button className="rounded-lg bg-black hover:scale-105 transition-all duration-100 ease-linear antialiased ring-offset-purple-800 p-2 px-5 my-auto font text-xs font-bold text-white drop-shadow-lg flex space-x-2 items-center">
|
||||
<div>New Collection </div>
|
||||
<div className='text-md bg-neutral-800 px-1 rounded-full'>+</div>
|
||||
<div className="text-md bg-neutral-800 px-1 rounded-full">+</div>
|
||||
</button>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default NewCollectionButton
|
||||
export default NewCollectionButton
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
"use client";
|
||||
'use client'
|
||||
|
||||
function NewCourseButton() {
|
||||
return (
|
||||
<button className="rounded-lg bg-black hover:scale-105 transition-all duration-100 ease-linear antialiased ring-offset-purple-800 p-2 px-5 my-auto font text-xs font-bold text-white drop-shadow-lg flex space-x-2 items-center">
|
||||
<div>New Course </div>
|
||||
<div className='text-md bg-neutral-800 px-1 rounded-full'>+</div>
|
||||
<div className="text-md bg-neutral-800 px-1 rounded-full">+</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewCourseButton
|
||||
export default NewCourseButton
|
||||
|
|
|
|||
|
|
@ -1,128 +1,141 @@
|
|||
'use client';
|
||||
import React from 'react';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { styled, keyframes } from '@stitches/react';
|
||||
import { blackA, } from '@radix-ui/colors';
|
||||
import { AlertTriangle, Info } from 'lucide-react';
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
import { styled, keyframes } from '@stitches/react'
|
||||
import { blackA } from '@radix-ui/colors'
|
||||
import { AlertTriangle, Info } from 'lucide-react'
|
||||
|
||||
type ModalParams = {
|
||||
confirmationMessage: string;
|
||||
confirmationButtonText: string;
|
||||
dialogTitle: string;
|
||||
functionToExecute: any;
|
||||
dialogTrigger?: React.ReactNode;
|
||||
status?: "warning" | "info";
|
||||
};
|
||||
confirmationMessage: string
|
||||
confirmationButtonText: string
|
||||
dialogTitle: string
|
||||
functionToExecute: any
|
||||
dialogTrigger?: React.ReactNode
|
||||
status?: 'warning' | 'info'
|
||||
}
|
||||
|
||||
const ConfirmationModal = (params: ModalParams) => {
|
||||
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
|
||||
const warningColors = 'bg-red-100 text-red-600'
|
||||
const infoColors = 'bg-blue-100 text-blue-600'
|
||||
const warningButtonColors = 'text-white bg-red-500 hover:bg-red-600'
|
||||
const infoButtonColors = 'text-white bg-blue-500 hover:bg-blue-600'
|
||||
const [isDialogOpen, setIsDialogOpen] = React.useState(false)
|
||||
const warningColors = 'bg-red-100 text-red-600'
|
||||
const infoColors = 'bg-blue-100 text-blue-600'
|
||||
const warningButtonColors = 'text-white bg-red-500 hover:bg-red-600'
|
||||
const infoButtonColors = 'text-white bg-blue-500 hover:bg-blue-600'
|
||||
|
||||
const onOpenChange = React.useCallback(
|
||||
(open: any) => {
|
||||
setIsDialogOpen(open)
|
||||
},
|
||||
[setIsDialogOpen]
|
||||
)
|
||||
|
||||
const onOpenChange = React.useCallback(
|
||||
(open: any) => {
|
||||
setIsDialogOpen(open);
|
||||
},
|
||||
[setIsDialogOpen]
|
||||
);
|
||||
return (
|
||||
<Dialog.Root open={isDialogOpen} onOpenChange={onOpenChange}>
|
||||
{params.dialogTrigger ? (
|
||||
<Dialog.Trigger asChild>{params.dialogTrigger}</Dialog.Trigger>
|
||||
) : null}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={isDialogOpen} onOpenChange={onOpenChange}>
|
||||
{params.dialogTrigger ? (
|
||||
<Dialog.Trigger asChild >
|
||||
{params.dialogTrigger}
|
||||
</Dialog.Trigger>
|
||||
) : null}
|
||||
|
||||
<Dialog.Portal>
|
||||
<DialogOverlay />
|
||||
<DialogContent >
|
||||
<div className='h-26 flex space-x-4 tracking-tight'>
|
||||
<div className={`icon p-6 rounded-xl flex items-center align-content-center ${params.status === 'warning' ? warningColors : infoColors}`}>
|
||||
{params.status === 'warning' ? <AlertTriangle size={35} /> : <Info size={35} />}
|
||||
</div>
|
||||
<div className="text pt-1 space-x-0 w-auto flex-grow">
|
||||
<div className="text-xl font-bold text-black ">
|
||||
{params.dialogTitle}
|
||||
</div>
|
||||
<div className="text-md text-gray-500 w-60 leading-tight">
|
||||
{params.confirmationMessage}
|
||||
</div>
|
||||
<div className="flex flex-row-reverse pt-2">
|
||||
<div className={`rounded-md text-sm px-3 py-2 font-bold flex justify-center items-center hover:cursor-pointer ${params.status === 'warning' ? warningButtonColors : infoButtonColors}
|
||||
<Dialog.Portal>
|
||||
<DialogOverlay />
|
||||
<DialogContent>
|
||||
<div className="h-26 flex space-x-4 tracking-tight">
|
||||
<div
|
||||
className={`icon p-6 rounded-xl flex items-center align-content-center ${
|
||||
params.status === 'warning' ? warningColors : infoColors
|
||||
}`}
|
||||
>
|
||||
{params.status === 'warning' ? (
|
||||
<AlertTriangle size={35} />
|
||||
) : (
|
||||
<Info size={35} />
|
||||
)}
|
||||
</div>
|
||||
<div className="text pt-1 space-x-0 w-auto flex-grow">
|
||||
<div className="text-xl font-bold text-black ">
|
||||
{params.dialogTitle}
|
||||
</div>
|
||||
<div className="text-md text-gray-500 w-60 leading-tight">
|
||||
{params.confirmationMessage}
|
||||
</div>
|
||||
<div className="flex flex-row-reverse pt-2">
|
||||
<div
|
||||
className={`rounded-md text-sm px-3 py-2 font-bold flex justify-center items-center hover:cursor-pointer ${
|
||||
params.status === 'warning'
|
||||
? warningButtonColors
|
||||
: infoButtonColors
|
||||
}
|
||||
hover:shadow-lg transition duration-300 ease-in-out
|
||||
`}
|
||||
onClick={() => { params.functionToExecute(); setIsDialogOpen(false) }}>
|
||||
{params.confirmationButtonText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
};
|
||||
onClick={() => {
|
||||
params.functionToExecute()
|
||||
setIsDialogOpen(false)
|
||||
}}
|
||||
>
|
||||
{params.confirmationButtonText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
|
||||
const overlayShow = keyframes({
|
||||
'0%': { opacity: 0 },
|
||||
'100%': { opacity: 1 },
|
||||
});
|
||||
'0%': { opacity: 0 },
|
||||
'100%': { opacity: 1 },
|
||||
})
|
||||
|
||||
const overlayClose = keyframes({
|
||||
'0%': { opacity: 1 },
|
||||
'100%': { opacity: 0 },
|
||||
});
|
||||
'0%': { opacity: 1 },
|
||||
'100%': { opacity: 0 },
|
||||
})
|
||||
|
||||
const contentShow = keyframes({
|
||||
'0%': { opacity: 0, transform: 'translate(-50%, -50%) scale(.96)' },
|
||||
'100%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
|
||||
});
|
||||
'0%': { opacity: 0, transform: 'translate(-50%, -50%) scale(.96)' },
|
||||
'100%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
|
||||
})
|
||||
|
||||
const contentClose = keyframes({
|
||||
'0%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
|
||||
'100%': { opacity: 0, transform: 'translate(-50%, -52%) scale(.96)' },
|
||||
});
|
||||
'0%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
|
||||
'100%': { opacity: 0, transform: 'translate(-50%, -52%) scale(.96)' },
|
||||
})
|
||||
|
||||
const DialogOverlay = styled(Dialog.Overlay, {
|
||||
backgroundColor: blackA.blackA9,
|
||||
position: 'fixed',
|
||||
zIndex: 500,
|
||||
inset: 0,
|
||||
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
'&[data-state="closed"]': {
|
||||
animation: `${overlayClose} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
},
|
||||
});
|
||||
backgroundColor: blackA.blackA9,
|
||||
position: 'fixed',
|
||||
zIndex: 500,
|
||||
inset: 0,
|
||||
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
'&[data-state="closed"]': {
|
||||
animation: `${overlayClose} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
},
|
||||
})
|
||||
|
||||
const DialogContent = styled(Dialog.Content, {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 18,
|
||||
zIndex: 501,
|
||||
boxShadow:
|
||||
'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 'auto',
|
||||
minWidth: '500px',
|
||||
overflow: 'hidden',
|
||||
height: 'auto',
|
||||
maxHeight: '85vh',
|
||||
maxWidth: '600px',
|
||||
padding: 11,
|
||||
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
'&:focus': { outline: 'none' },
|
||||
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 18,
|
||||
zIndex: 501,
|
||||
boxShadow: 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 'auto',
|
||||
minWidth: '500px',
|
||||
overflow: 'hidden',
|
||||
height: 'auto',
|
||||
maxHeight: '85vh',
|
||||
maxWidth: '600px',
|
||||
padding: 11,
|
||||
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
'&:focus': { outline: 'none' },
|
||||
'&[data-state="closed"]': {
|
||||
animation: `${contentClose} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
},
|
||||
transition: 'max-height 0.3s ease-out',
|
||||
})
|
||||
|
||||
'&[data-state="closed"]': {
|
||||
animation: `${contentClose} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
},
|
||||
transition: "max-height 0.3s ease-out",
|
||||
});
|
||||
|
||||
|
||||
export default ConfirmationModal;
|
||||
export default ConfirmationModal
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import { XCircle } from 'lucide-react'
|
||||
import React from 'react'
|
||||
function ErrorUI() {
|
||||
return (
|
||||
<div className='flex items-center justify-center h-screen'>
|
||||
<div className='mx-auto bg-red-100 w-[800px] p-3 rounded-xl m-5 '>
|
||||
<div className='flex flex-row'>
|
||||
<div className='p-3 pr-4 items-center' >
|
||||
<XCircle size={40} className='text-red-600' />
|
||||
</div>
|
||||
<div className='p-3 '>
|
||||
<h1 className='text-2xl font-bold text-red-600'>Error</h1>
|
||||
<p className='pt-0 text-md text-red-600'>Something went wrong</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="mx-auto bg-red-100 w-[800px] p-3 rounded-xl m-5 ">
|
||||
<div className="flex flex-row">
|
||||
<div className="p-3 pr-4 items-center">
|
||||
<XCircle size={40} className="text-red-600" />
|
||||
</div>
|
||||
<div className="p-3 ">
|
||||
<h1 className="text-2xl font-bold text-red-600">Error</h1>
|
||||
<p className="pt-0 text-md text-red-600">Something went wrong</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorUI
|
||||
export default ErrorUI
|
||||
|
|
|
|||
|
|
@ -1,104 +1,110 @@
|
|||
import React from 'react';
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
import { styled } from '@stitches/react';
|
||||
import { blackA } from '@radix-ui/colors';
|
||||
import { Info } from 'lucide-react';
|
||||
import React from 'react'
|
||||
import * as Form from '@radix-ui/react-form'
|
||||
import { styled } from '@stitches/react'
|
||||
import { blackA } from '@radix-ui/colors'
|
||||
import { Info } from 'lucide-react'
|
||||
|
||||
const FormLayout = (props: any, onSubmit: any) => (
|
||||
<FormRoot className='h-fit' onSubmit={props.onSubmit}>
|
||||
{props.children}
|
||||
</FormRoot>
|
||||
);
|
||||
<FormRoot className="h-fit" onSubmit={props.onSubmit}>
|
||||
{props.children}
|
||||
</FormRoot>
|
||||
)
|
||||
|
||||
export const FormLabelAndMessage = (props: { label: string, message?: string }) => (
|
||||
<div className='flex items-center space-x-3'>
|
||||
<FormLabel className='grow'>{props.label}</FormLabel>
|
||||
{props.message && <div className='text-red-700 text-sm items-center rounded-md flex space-x-1'><Info size={10} /><div>{props.message}</div></div> || <></>}
|
||||
</div>
|
||||
);
|
||||
export const FormLabelAndMessage = (props: {
|
||||
label: string
|
||||
message?: string
|
||||
}) => (
|
||||
<div className="flex items-center space-x-3">
|
||||
<FormLabel className="grow">{props.label}</FormLabel>
|
||||
{(props.message && (
|
||||
<div className="text-red-700 text-sm items-center rounded-md flex space-x-1">
|
||||
<Info size={10} />
|
||||
<div>{props.message}</div>
|
||||
</div>
|
||||
)) || <></>}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const FormRoot = styled(Form.Root, {
|
||||
margin: 7
|
||||
});
|
||||
margin: 7,
|
||||
})
|
||||
|
||||
export const FormField = styled(Form.Field, {
|
||||
display: 'grid',
|
||||
marginBottom: 10,
|
||||
});
|
||||
display: 'grid',
|
||||
marginBottom: 10,
|
||||
})
|
||||
|
||||
export const FormLabel = styled(Form.Label, {
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
lineHeight: '35px',
|
||||
color: 'black',
|
||||
});
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
lineHeight: '35px',
|
||||
color: 'black',
|
||||
})
|
||||
|
||||
export const FormMessage = styled(Form.Message, {
|
||||
fontSize: 13,
|
||||
color: 'white',
|
||||
opacity: 0.8,
|
||||
});
|
||||
fontSize: 13,
|
||||
color: 'white',
|
||||
opacity: 0.8,
|
||||
})
|
||||
|
||||
export const Flex = styled('div', { display: 'flex' });
|
||||
export const Flex = styled('div', { display: 'flex' })
|
||||
|
||||
export const inputStyles = {
|
||||
all: 'unset',
|
||||
boxSizing: 'border-box',
|
||||
width: '100%',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 4,
|
||||
fontSize: 15,
|
||||
color: '#7c7c7c',
|
||||
background: "#F9FAFB",
|
||||
boxShadow: `0 0 0 1px #edeeef`,
|
||||
'&:hover': { boxShadow: `0 0 0 1px #edeeef` },
|
||||
'&:focus': { boxShadow: `0 0 0 2px #edeeef` },
|
||||
'&::selection': { backgroundColor: blackA.blackA9, color: 'white' },
|
||||
|
||||
};
|
||||
all: 'unset',
|
||||
boxSizing: 'border-box',
|
||||
width: '100%',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 4,
|
||||
fontSize: 15,
|
||||
color: '#7c7c7c',
|
||||
background: '#F9FAFB',
|
||||
boxShadow: `0 0 0 1px #edeeef`,
|
||||
'&:hover': { boxShadow: `0 0 0 1px #edeeef` },
|
||||
'&:focus': { boxShadow: `0 0 0 2px #edeeef` },
|
||||
'&::selection': { backgroundColor: blackA.blackA9, color: 'white' },
|
||||
}
|
||||
|
||||
export const Input = styled('input', {
|
||||
...inputStyles,
|
||||
height: 35,
|
||||
lineHeight: 1,
|
||||
padding: '0 10px',
|
||||
border: 'none',
|
||||
});
|
||||
...inputStyles,
|
||||
height: 35,
|
||||
lineHeight: 1,
|
||||
padding: '0 10px',
|
||||
border: 'none',
|
||||
})
|
||||
|
||||
export const Textarea = styled('textarea', {
|
||||
...inputStyles,
|
||||
resize: 'none',
|
||||
padding: 10,
|
||||
});
|
||||
...inputStyles,
|
||||
resize: 'none',
|
||||
padding: 10,
|
||||
})
|
||||
|
||||
export const ButtonBlack = styled('button', {
|
||||
variants: {
|
||||
state: {
|
||||
"loading": {
|
||||
pointerEvents: 'none',
|
||||
backgroundColor: "#808080",
|
||||
},
|
||||
"none": {
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
state: {
|
||||
loading: {
|
||||
pointerEvents: 'none',
|
||||
backgroundColor: '#808080',
|
||||
},
|
||||
none: {},
|
||||
},
|
||||
all: 'unset',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 8,
|
||||
padding: '0 15px',
|
||||
fontSize: 15,
|
||||
lineHeight: 1,
|
||||
fontWeight: 500,
|
||||
height: 35,
|
||||
},
|
||||
all: 'unset',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 8,
|
||||
padding: '0 15px',
|
||||
fontSize: 15,
|
||||
lineHeight: 1,
|
||||
fontWeight: 500,
|
||||
height: 35,
|
||||
|
||||
background: "#000000",
|
||||
color: "#FFFFFF",
|
||||
'&:hover': { backgroundColor: "#181818", cursor: "pointer" },
|
||||
'&:focus': { boxShadow: `0 0 0 2px black` },
|
||||
});
|
||||
background: '#000000',
|
||||
color: '#FFFFFF',
|
||||
'&:hover': { backgroundColor: '#181818', cursor: 'pointer' },
|
||||
'&:focus': { boxShadow: `0 0 0 2px black` },
|
||||
})
|
||||
|
||||
export default FormLayout;
|
||||
export default FormLayout
|
||||
|
|
|
|||
|
|
@ -1,158 +1,155 @@
|
|||
'use client';
|
||||
import React from 'react';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { styled, keyframes } from '@stitches/react';
|
||||
import { blackA, mauve } from '@radix-ui/colors';
|
||||
import { ButtonBlack } from '../Form/Form';
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
import { styled, keyframes } from '@stitches/react'
|
||||
import { blackA, mauve } from '@radix-ui/colors'
|
||||
import { ButtonBlack } from '../Form/Form'
|
||||
|
||||
type ModalParams = {
|
||||
dialogTitle: string;
|
||||
dialogDescription: string;
|
||||
dialogContent: React.ReactNode;
|
||||
dialogClose?: React.ReactNode | null;
|
||||
dialogTrigger?: React.ReactNode;
|
||||
addDefCloseButton?: boolean;
|
||||
onOpenChange: any;
|
||||
isDialogOpen?: boolean;
|
||||
minHeight?: "sm" | "md" | "lg" | "xl" | "no-min";
|
||||
};
|
||||
dialogTitle: string
|
||||
dialogDescription: string
|
||||
dialogContent: React.ReactNode
|
||||
dialogClose?: React.ReactNode | null
|
||||
dialogTrigger?: React.ReactNode
|
||||
addDefCloseButton?: boolean
|
||||
onOpenChange: any
|
||||
isDialogOpen?: boolean
|
||||
minHeight?: 'sm' | 'md' | 'lg' | 'xl' | 'no-min'
|
||||
}
|
||||
|
||||
const Modal = (params: ModalParams) => (
|
||||
<Dialog.Root open={params.isDialogOpen} onOpenChange={params.onOpenChange}>
|
||||
{params.dialogTrigger ? (
|
||||
<Dialog.Trigger asChild>
|
||||
{params.dialogTrigger}
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Root open={params.isDialogOpen} onOpenChange={params.onOpenChange}>
|
||||
{params.dialogTrigger ? (
|
||||
<Dialog.Trigger asChild>{params.dialogTrigger}</Dialog.Trigger>
|
||||
) : null}
|
||||
|
||||
<Dialog.Portal>
|
||||
<DialogOverlay />
|
||||
<DialogContent
|
||||
className="overflow-auto scrollbar-w-2 scrollbar-h-2 scrollbar scrollbar-thumb-black/20 scrollbar-thumb-rounded-full scrollbar-track-rounded-full"
|
||||
minHeight={params.minHeight}
|
||||
>
|
||||
<DialogTopBar className="-space-y-1">
|
||||
<DialogTitle>{params.dialogTitle}</DialogTitle>
|
||||
<DialogDescription>{params.dialogDescription}</DialogDescription>
|
||||
</DialogTopBar>
|
||||
{params.dialogContent}
|
||||
{params.dialogClose ? (
|
||||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Dialog.Close asChild>{params.dialogClose}</Dialog.Close>
|
||||
</Flex>
|
||||
) : null}
|
||||
|
||||
<Dialog.Portal>
|
||||
<DialogOverlay />
|
||||
<DialogContent className='overflow-auto scrollbar-w-2 scrollbar-h-2 scrollbar scrollbar-thumb-black/20 scrollbar-thumb-rounded-full scrollbar-track-rounded-full' minHeight={params.minHeight}>
|
||||
<DialogTopBar className='-space-y-1'>
|
||||
<DialogTitle>{params.dialogTitle}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{params.dialogDescription}
|
||||
</DialogDescription>
|
||||
</DialogTopBar>
|
||||
{params.dialogContent}
|
||||
{params.dialogClose ? (
|
||||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Dialog.Close asChild>
|
||||
{params.dialogClose}
|
||||
</Dialog.Close>
|
||||
</Flex>
|
||||
) : null}
|
||||
{params.addDefCloseButton ? (
|
||||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Dialog.Close asChild>
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>Close</ButtonBlack>
|
||||
</Dialog.Close>
|
||||
</Flex>
|
||||
) : null}
|
||||
|
||||
</DialogContent>
|
||||
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
{params.addDefCloseButton ? (
|
||||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Dialog.Close asChild>
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
||||
Close
|
||||
</ButtonBlack>
|
||||
</Dialog.Close>
|
||||
</Flex>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
|
||||
const overlayShow = keyframes({
|
||||
'0%': { opacity: 0 },
|
||||
'100%': { opacity: 1 },
|
||||
});
|
||||
'0%': { opacity: 0 },
|
||||
'100%': { opacity: 1 },
|
||||
})
|
||||
|
||||
const overlayClose = keyframes({
|
||||
'0%': { opacity: 1 },
|
||||
'100%': { opacity: 0 },
|
||||
});
|
||||
'0%': { opacity: 1 },
|
||||
'100%': { opacity: 0 },
|
||||
})
|
||||
|
||||
const contentShow = keyframes({
|
||||
'0%': { opacity: 0, transform: 'translate(-50%, -50%) scale(.96)' },
|
||||
'100%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
|
||||
});
|
||||
'0%': { opacity: 0, transform: 'translate(-50%, -50%) scale(.96)' },
|
||||
'100%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
|
||||
})
|
||||
|
||||
const contentClose = keyframes({
|
||||
'0%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
|
||||
'100%': { opacity: 0, transform: 'translate(-50%, -52%) scale(.96)' },
|
||||
});
|
||||
'0%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
|
||||
'100%': { opacity: 0, transform: 'translate(-50%, -52%) scale(.96)' },
|
||||
})
|
||||
|
||||
const DialogOverlay = styled(Dialog.Overlay, {
|
||||
backgroundColor: blackA.blackA9,
|
||||
position: 'fixed',
|
||||
zIndex: 500,
|
||||
inset: 0,
|
||||
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
'&[data-state="closed"]': {
|
||||
animation: `${overlayClose} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
},
|
||||
});
|
||||
backgroundColor: blackA.blackA9,
|
||||
position: 'fixed',
|
||||
zIndex: 500,
|
||||
inset: 0,
|
||||
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
'&[data-state="closed"]': {
|
||||
animation: `${overlayClose} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
},
|
||||
})
|
||||
|
||||
const DialogContent = styled(Dialog.Content, {
|
||||
|
||||
variants: {
|
||||
minHeight: {
|
||||
'no-min': {
|
||||
minHeight: '0px',
|
||||
},
|
||||
'sm': {
|
||||
minHeight: '300px',
|
||||
},
|
||||
'md': {
|
||||
minHeight: '500px',
|
||||
},
|
||||
'lg': {
|
||||
minHeight: '700px',
|
||||
},
|
||||
'xl': {
|
||||
minHeight: '900px',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
minHeight: {
|
||||
'no-min': {
|
||||
minHeight: '0px',
|
||||
},
|
||||
sm: {
|
||||
minHeight: '300px',
|
||||
},
|
||||
md: {
|
||||
minHeight: '500px',
|
||||
},
|
||||
lg: {
|
||||
minHeight: '700px',
|
||||
},
|
||||
xl: {
|
||||
minHeight: '900px',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 18,
|
||||
zIndex: 501,
|
||||
boxShadow: 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '90vw',
|
||||
maxHeight: '85vh',
|
||||
minHeight: '300px',
|
||||
maxWidth: '600px',
|
||||
padding: 11,
|
||||
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
'&:focus': { outline: 'none' },
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 18,
|
||||
zIndex: 501,
|
||||
boxShadow:
|
||||
'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '90vw',
|
||||
maxHeight: '85vh',
|
||||
minHeight: '300px',
|
||||
maxWidth: '600px',
|
||||
padding: 11,
|
||||
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
'&:focus': { outline: 'none' },
|
||||
|
||||
'&[data-state="closed"]': {
|
||||
animation: `${contentClose} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
},
|
||||
transition: "max-height 0.3s ease-out",
|
||||
});
|
||||
'&[data-state="closed"]': {
|
||||
animation: `${contentClose} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
},
|
||||
transition: 'max-height 0.3s ease-out',
|
||||
})
|
||||
|
||||
const DialogTopBar = styled('div', {
|
||||
background: "#F7F7F7",
|
||||
padding: "8px 14px ",
|
||||
borderRadius: 14,
|
||||
});
|
||||
background: '#F7F7F7',
|
||||
padding: '8px 14px ',
|
||||
borderRadius: 14,
|
||||
})
|
||||
const DialogTitle = styled(Dialog.Title, {
|
||||
margin: 0,
|
||||
fontWeight: 700,
|
||||
letterSpacing: "-0.05em",
|
||||
padding: 0,
|
||||
color: mauve.mauve12,
|
||||
fontSize: 21,
|
||||
});
|
||||
margin: 0,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '-0.05em',
|
||||
padding: 0,
|
||||
color: mauve.mauve12,
|
||||
fontSize: 21,
|
||||
})
|
||||
|
||||
const DialogDescription = styled(Dialog.Description, {
|
||||
color: mauve.mauve11,
|
||||
letterSpacing: "-0.03em",
|
||||
fontSize: 15,
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
});
|
||||
color: mauve.mauve11,
|
||||
letterSpacing: '-0.03em',
|
||||
fontSize: 15,
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
})
|
||||
|
||||
const Flex = styled('div', { display: 'flex' });
|
||||
const Flex = styled('div', { display: 'flex' })
|
||||
|
||||
export default Modal;
|
||||
export default Modal
|
||||
|
|
|
|||
|
|
@ -1,32 +1,27 @@
|
|||
import Image from 'next/image'
|
||||
import CoursesLogo from "public/svg/courses.svg";
|
||||
import CollectionsLogo from "public/svg/collections.svg";
|
||||
import TrailLogo from "public/svg/trail.svg";
|
||||
import CoursesLogo from 'public/svg/courses.svg'
|
||||
import CollectionsLogo from 'public/svg/collections.svg'
|
||||
import TrailLogo from 'public/svg/trail.svg'
|
||||
|
||||
function TypeOfContentTitle(props: { title: string, type: string }) {
|
||||
|
||||
function getLogo() {
|
||||
if (props.type == "col") {
|
||||
return CollectionsLogo;
|
||||
}
|
||||
|
||||
else if (props.type == "cou") {
|
||||
return CoursesLogo;
|
||||
}
|
||||
|
||||
else if (props.type == "tra") {
|
||||
return TrailLogo;
|
||||
}
|
||||
function TypeOfContentTitle(props: { title: string; type: string }) {
|
||||
function getLogo() {
|
||||
if (props.type == 'col') {
|
||||
return CollectionsLogo
|
||||
} else if (props.type == 'cou') {
|
||||
return CoursesLogo
|
||||
} else if (props.type == 'tra') {
|
||||
return TrailLogo
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="home_category_title flex my-5 items-center">
|
||||
<div className="ml-2 rounded-full ring-1 ring-slate-900/5 shadow-inner p-2 my-auto mr-4">
|
||||
<Image className="" src={getLogo()} alt="Courses logo" />
|
||||
</div>
|
||||
<h1 className="font-bold text-2xl">{props.title}</h1>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="home_category_title flex my-5 items-center">
|
||||
<div className="ml-2 rounded-full ring-1 ring-slate-900/5 shadow-inner p-2 my-auto mr-4">
|
||||
<Image className="" src={getLogo()} alt="Courses logo" />
|
||||
</div>
|
||||
<h1 className="font-bold text-2xl">{props.title}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TypeOfContentTitle
|
||||
export default TypeOfContentTitle
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
'use client';
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
|
||||
function Toast() {
|
||||
return (
|
||||
<><Toaster /></>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<Toaster />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Toast
|
||||
export default Toast
|
||||
|
|
|
|||
|
|
@ -1,66 +1,65 @@
|
|||
'use client';
|
||||
import React from 'react';
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
import { styled, keyframes } from '@stitches/react';
|
||||
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { styled, keyframes } from '@stitches/react'
|
||||
|
||||
type TooltipProps = {
|
||||
sideOffset?: number;
|
||||
content: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
side?: 'top' | 'right' | 'bottom' | 'left'; // default is bottom
|
||||
slateBlack?: boolean;
|
||||
};
|
||||
sideOffset?: number
|
||||
content: React.ReactNode
|
||||
children: React.ReactNode
|
||||
side?: 'top' | 'right' | 'bottom' | 'left' // default is bottom
|
||||
slateBlack?: boolean
|
||||
}
|
||||
|
||||
const ToolTip = (props: TooltipProps) => {
|
||||
|
||||
return (
|
||||
<Tooltip.Provider delayDuration={200}>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
{props.children}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Trigger asChild>{props.children}</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<TooltipContent slateBlack={props.slateBlack} side={props.side ? props.side : 'bottom'} sideOffset={props.sideOffset}>
|
||||
<TooltipContent
|
||||
slateBlack={props.slateBlack}
|
||||
side={props.side ? props.side : 'bottom'}
|
||||
sideOffset={props.sideOffset}
|
||||
>
|
||||
{props.content}
|
||||
</TooltipContent>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const slideUpAndFade = keyframes({
|
||||
'0%': { opacity: 0, transform: 'translateY(2px)' },
|
||||
'100%': { opacity: 1, transform: 'translateY(0)' },
|
||||
});
|
||||
})
|
||||
|
||||
const slideRightAndFade = keyframes({
|
||||
'0%': { opacity: 0, transform: 'translateX(-2px)' },
|
||||
'100%': { opacity: 1, transform: 'translateX(0)' },
|
||||
});
|
||||
})
|
||||
|
||||
const slideDownAndFade = keyframes({
|
||||
'0%': { opacity: 0, transform: 'translateY(-2px)' },
|
||||
'100%': { opacity: 1, transform: 'translateY(0)' },
|
||||
});
|
||||
})
|
||||
|
||||
const slideLeftAndFade = keyframes({
|
||||
'0%': { opacity: 0, transform: 'translateX(2px)' },
|
||||
'100%': { opacity: 1, transform: 'translateX(0)' },
|
||||
});
|
||||
})
|
||||
|
||||
const closeAndFade = keyframes({
|
||||
'0%': { opacity: 1 },
|
||||
'100%': { opacity: 0 },
|
||||
});
|
||||
})
|
||||
|
||||
const TooltipContent = styled(Tooltip.Content, {
|
||||
|
||||
variants: {
|
||||
slateBlack: {
|
||||
true: {
|
||||
backgroundColor: " #0d0d0d",
|
||||
backgroundColor: ' #0d0d0d',
|
||||
color: 'white',
|
||||
},
|
||||
},
|
||||
|
|
@ -70,10 +69,11 @@ const TooltipContent = styled(Tooltip.Content, {
|
|||
padding: '5px 10px',
|
||||
fontSize: 12,
|
||||
lineHeight: 1,
|
||||
color: "black",
|
||||
color: 'black',
|
||||
backgroundColor: 'rgba(217, 217, 217, 0.50)',
|
||||
zIndex: 500,
|
||||
boxShadow: 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
|
||||
boxShadow:
|
||||
'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
|
||||
userSelect: 'none',
|
||||
animationDuration: '400ms',
|
||||
animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
|
|
@ -92,8 +92,6 @@ const TooltipContent = styled(Tooltip.Content, {
|
|||
'&[data-side="bottom"]': { animationName: closeAndFade },
|
||||
'&[data-side="left"]': { animationName: closeAndFade },
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
|
||||
export default ToolTip;
|
||||
export default ToolTip
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
|
||||
function GeneralWrapperStyled({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
className='max-w-screen-2xl mx-auto px-16 py-5 tracking-tight z-50'
|
||||
|
||||
>{children}</div>
|
||||
)
|
||||
return (
|
||||
<div className="max-w-screen-2xl mx-auto px-16 py-5 tracking-tight z-50">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GeneralWrapperStyled
|
||||
export default GeneralWrapperStyled
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
"use client";
|
||||
'use client'
|
||||
|
||||
function ClientComponentSkeleton({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div>{children}</div>
|
||||
)
|
||||
function ClientComponentSkeleton({ children }: { children: React.ReactNode }) {
|
||||
return <div>{children}</div>
|
||||
}
|
||||
|
||||
export default ClientComponentSkeleton
|
||||
export default ClientComponentSkeleton
|
||||
|
|
|
|||
|
|
@ -1,30 +1,29 @@
|
|||
'use client'
|
||||
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useServerInsertedHTML } from 'next/navigation';
|
||||
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
|
||||
import React, { useState } from 'react'
|
||||
import { useServerInsertedHTML } from 'next/navigation'
|
||||
import { ServerStyleSheet, StyleSheetManager } from 'styled-components'
|
||||
|
||||
export default function StyledComponentsRegistry({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
// Only create stylesheet once with lazy initial state
|
||||
// x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
|
||||
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
|
||||
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet())
|
||||
|
||||
useServerInsertedHTML(() => {
|
||||
const styles = styledComponentsStyleSheet.getStyleElement();
|
||||
styledComponentsStyleSheet.instance.clearTag();
|
||||
return <>{styles}</>;
|
||||
});
|
||||
const styles = styledComponentsStyleSheet.getStyleElement()
|
||||
styledComponentsStyleSheet.instance.clearTag()
|
||||
return <>{styles}</>
|
||||
})
|
||||
|
||||
if (typeof window !== 'undefined') return <>{children}</>;
|
||||
if (typeof window !== 'undefined') return <>{children}</>
|
||||
|
||||
return (
|
||||
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
|
||||
{children as React.ReactChild}
|
||||
</StyleSheetManager>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue