This commit is contained in:
Flatlogic Bot 2026-03-01 20:26:04 +00:00
parent e56befb06a
commit 14794ed687
10 changed files with 728 additions and 323 deletions

View File

@ -62,6 +62,35 @@ router.post('/signin/local', wrapAsync(async (req, res) => {
res.status(200).send(payload); res.status(200).send(payload);
})); }));
/**
* @swagger
* /api/auth/signin/code:
* post:
* tags: [Auth]
* summary: Logs user into the system using access code
* description: Logs user into the system using access code
* requestBody:
* description: Set valid access code
* content:
* application/json:
* schema:
* type: object
* required:
* - code
* properties:
* code:
* type: string
* responses:
* 200:
* description: Successful login
* 400:
* description: Invalid code supplied
*/
router.post('/signin/code', wrapAsync(async (req, res) => {
const payload = await AuthService.signinWithCode(req.body.code);
res.status(200).send(payload);
}));
/** /**
* @swagger * @swagger
* /api/auth/me: * /api/auth/me:
@ -204,4 +233,4 @@ function socialRedirect(res, state, token, config) {
res.redirect(config.uiUrl + "/login?token=" + token); res.redirect(config.uiUrl + "/login?token=" + token);
} }
module.exports = router; module.exports = router;

View File

@ -1,4 +1,5 @@
const UsersDBApi = require('../db/api/users'); const UsersDBApi = require('../db/api/users');
const Access_codesDBApi = require('../db/api/access_codes');
const ValidationError = require('./notifications/errors/validation'); const ValidationError = require('./notifications/errors/validation');
const ForbiddenError = require('./notifications/errors/forbidden'); const ForbiddenError = require('./notifications/errors/forbidden');
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
@ -8,6 +9,7 @@ const PasswordResetEmail = require('./email/list/passwordReset');
const EmailSender = require('./email'); const EmailSender = require('./email');
const config = require('../config'); const config = require('../config');
const helpers = require('../helpers'); const helpers = require('../helpers');
const db = require('../db/models');
class Auth { class Auth {
static async signup(email, password, options = {}, host) { static async signup(email, password, options = {}, host) {
@ -18,6 +20,8 @@ class Auth {
config.bcrypt.saltRounds, config.bcrypt.saltRounds,
); );
let currentUser;
if (user) { if (user) {
if (user.authenticationUid) { if (user.authenticationUid) {
throw new ValidationError( throw new ValidationError(
@ -37,44 +41,40 @@ class Auth {
options, options,
); );
if (EmailSender.isConfigured) { currentUser = user;
await this.sendEmailAddressVerificationEmail( } else {
user.email, currentUser = await UsersDBApi.createFromAuth(
host, {
); firstName: email.split('@')[0],
} password: hashedPassword,
email: email,
const data = { },
user: { options,
id: user.id, );
email: user.email
}
};
return helpers.jwtSign(data);
} }
const newUser = await UsersDBApi.createFromAuth( // Generate Access Code
{ const code = Math.random().toString(36).substring(2, 8).toUpperCase();
firstName: email.split('@')[0], await Access_codesDBApi.create({
password: hashedPassword, code,
email: email, status: 'active',
user: currentUser.id,
}, max_uses: 1000,
options, uses_count: 0
); }, options);
if (EmailSender.isConfigured) { if (EmailSender.isConfigured) {
await this.sendEmailAddressVerificationEmail( await this.sendEmailAddressVerificationEmail(
newUser.email, currentUser.email,
host, host,
); );
} }
const data = { const data = {
user: { user: {
id: newUser.id, id: currentUser.id,
email: newUser.email email: currentUser.email,
accessCode: code // Return the code so the user knows it
} }
}; };
@ -133,6 +133,35 @@ class Auth {
return helpers.jwtSign(data); return helpers.jwtSign(data);
} }
static async signinWithCode(code) {
const accessCode = await Access_codesDBApi.findBy({ code, status: 'active' });
if (!accessCode || !accessCode.user) {
throw new ValidationError('auth.invalidCode');
}
const user = await UsersDBApi.findBy({ id: accessCode.user.id });
if (!user || user.disabled) {
throw new ValidationError('auth.userDisabled');
}
// Update uses count
await Access_codesDBApi.update(accessCode.id, {
uses_count: (accessCode.uses_count || 0) + 1,
used_at: new Date()
}, { currentUser: user });
const data = {
user: {
id: user.id,
email: user.email
}
};
return helpers.jwtSign(data);
}
static async sendEmailAddressVerificationEmail( static async sendEmailAddressVerificationEmail(
email, email,
host, host,
@ -309,4 +338,4 @@ class Auth {
} }
} }
module.exports = Auth; module.exports = Auth;

View File

@ -1,6 +1,5 @@
import React, {useEffect, useRef} from 'react' import React, {useEffect, useRef, useState} from 'react'
import Link from 'next/link' import Link from 'next/link'
import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider' import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon' import BaseIcon from './BaseIcon'
@ -129,4 +128,4 @@ export default function NavBarItem({ item }: Props) {
} }
return <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div> return <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div>
} }

View File

@ -1,5 +1,4 @@
import React, { ReactNode, useEffect } from 'react' import React, { ReactNode, useEffect, useState } from 'react'
import { useState } from 'react'
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside' import menuAside from '../menuAside'
@ -126,4 +125,4 @@ export default function LayoutAuthenticated({
</div> </div>
</div> </div>
) )
} }

View File

@ -7,6 +7,11 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiViewDashboardOutline, icon: icon.mdiViewDashboardOutline,
label: 'Dashboard', label: 'Dashboard',
}, },
{
href: '/studio',
icon: icon.mdiMusic,
label: 'Musical Studio',
},
{ {
href: '/users/users-list', href: '/users/users-list',
@ -184,4 +189,4 @@ const menuAside: MenuAsideItem[] = [
}, },
] ]
export default menuAside export default menuAside

View File

@ -1,166 +1,112 @@
import { ReactElement } from 'react';
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import Link from 'next/link'; import Link from 'next/link';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest'; import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider'; import SectionFullScreen from '../components/SectionFullScreen';
import BaseButtons from '../components/BaseButtons'; import { mdiMusic, mdiMicrophone, mdiPiano, mdiChartTimelineVariant } from '@mdi/js';
import BaseIcon from '../components/BaseIcon';
import { getPageTitle } from '../config'; import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
export default function Starter() {
const [illustrationImage, setIllustrationImage] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('image');
const [contentPosition, setContentPosition] = useState('right');
const textColor = useAppSelector((state) => state.style.linkColor);
const title = 'Studio Musical Web'
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
const imageBlock = (image) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
export default function Home() {
const title = "AI Music Studio";
return ( return (
<div <>
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<Head> <Head>
<title>{getPageTitle('Starter Page')}</title> <title>{getPageTitle('Home')}</title>
</Head> </Head>
<SectionFullScreen bg='violet'> <div className="bg-[#121212] min-h-screen text-white font-sans overflow-x-hidden">
<div {/* Navigation */}
className={`flex ${ <nav className="flex items-center justify-between px-6 py-4 md:px-12 border-b border-gray-800 bg-[#121212]/80 backdrop-blur-md sticky top-0 z-50">
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row' <div className="flex items-center space-x-2">
} min-h-screen w-full`} <BaseIcon path={mdiMusic} size={32} className="text-[#00E5FF]" />
> <span className="text-2xl font-bold tracking-tight">{title}</span>
{contentType === 'image' && contentPosition !== 'background' </div>
? imageBlock(illustrationImage) <div className="hidden md:flex space-x-8 items-center font-medium">
: null} <a href="#features" className="hover:text-[#00E5FF] transition-colors">Features</a>
{contentType === 'video' && contentPosition !== 'background' <a href="#instruments" className="hover:text-[#00E5FF] transition-colors">Instruments</a>
? videoBlock(illustrationVideo) <Link href="/login" className="px-5 py-2 rounded-full border border-[#00E5FF] text-[#00E5FF] hover:bg-[#00E5FF] hover:text-black transition-all">
: null} Login
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'> </Link>
<CardBox className='w-full md:w-3/5 lg:w-2/3'> <Link href="/register" className="px-5 py-2 rounded-full bg-[#00E5FF] text-black hover:bg-white transition-all shadow-[0_0_15px_rgba(0,229,255,0.4)]">
<CardBoxComponentTitle title="Welcome to your Studio Musical Web app!"/> Start Creating
</Link>
<div className="space-y-3"> </div>
<p className='text-center '>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p> </nav>
<p className='text-center '>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p> {/* Hero Section */}
<SectionFullScreen bg="dark" className="relative flex flex-col items-center justify-center pt-20 pb-32">
<div className="absolute top-0 left-0 w-full h-full overflow-hidden z-0 pointer-events-none opacity-20">
<div className="absolute top-[10%] left-[5%] w-72 h-72 bg-[#BB86FC] rounded-full filter blur-[100px] animate-pulse"></div>
<div className="absolute bottom-[20%] right-[10%] w-96 h-96 bg-[#03DAC6] rounded-full filter blur-[120px] animate-pulse delay-700"></div>
</div>
<div className="relative z-10 text-center px-4 max-w-4xl mx-auto">
<div className="inline-block px-4 py-1 mb-6 rounded-full bg-gray-800 border border-gray-700 text-sm font-medium text-[#00E5FF]">
AI-Powered Music Composition
</div> </div>
<h1 className="text-5xl md:text-7xl lg:text-8xl font-black mb-8 leading-tight tracking-tighter">
<BaseButtons> CREATE YOUR <span className="text-transparent bg-clip-text bg-gradient-to-r from-[#00E5FF] to-[#BB86FC]">SYMPHONY</span> IN SECONDS
<BaseButton </h1>
href='/login' <p className="text-xl md:text-2xl text-gray-400 mb-12 max-w-2xl mx-auto leading-relaxed">
label='Login' From Sertanejo to Hip-Hop. Professional instruments, intelligent beats, and AI lyrics generator &mdash; all with just your access code.
color='info' </p>
className='w-full' <div className="flex flex-col sm:flex-row items-center justify-center space-y-4 sm:space-y-0 sm:space-x-6">
/> <Link href="/register" className="w-full sm:w-auto px-10 py-5 rounded-xl bg-[#00E5FF] text-black text-xl font-bold hover:scale-105 transition-transform shadow-[0_0_25px_rgba(0,229,255,0.5)]">
Launch Studio
</Link>
<Link href="/login" className="w-full sm:w-auto px-10 py-5 rounded-xl bg-transparent border-2 border-white text-white text-xl font-bold hover:bg-white hover:text-black transition-all">
Enter with Code
</Link>
</div>
</div>
</SectionFullScreen>
</BaseButtons> {/* Features Section */}
</CardBox> <div id="features" className="py-32 px-6 md:px-12 bg-[#0A0A0A]">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
<div className="p-8 rounded-2xl bg-gradient-to-br from-gray-900 to-black border border-gray-800 hover:border-[#00E5FF] transition-all group">
<BaseIcon path={mdiMicrophone} size={48} className="text-[#BB86FC] mb-6 group-hover:scale-110 transition-transform" />
<h3 className="text-2xl font-bold mb-4">AI Lyrics Engine</h3>
<p className="text-gray-400 leading-relaxed">Input an idea, choose a style, and watch as our AI crafts a complete song structure with verses, chorus, and bridge.</p>
</div>
<div className="p-8 rounded-2xl bg-gradient-to-br from-gray-900 to-black border border-gray-800 hover:border-[#00E5FF] transition-all group">
<BaseIcon path={mdiPiano} size={48} className="text-[#03DAC6] mb-6 group-hover:scale-110 transition-transform" />
<h3 className="text-2xl font-bold mb-4">World Instruments</h3>
<p className="text-gray-400 leading-relaxed">Piano, Guitar, Accordion, Flute &mdash; access every instrument in the world with high-fidelity studio samples.</p>
</div>
<div className="p-8 rounded-2xl bg-gradient-to-br from-gray-900 to-black border border-gray-800 hover:border-[#00E5FF] transition-all group">
<BaseIcon path={mdiChartTimelineVariant} size={48} className="text-[#00E5FF] mb-6 group-hover:scale-110 transition-transform" />
<h3 className="text-2xl font-bold mb-4">Beat Configurator</h3>
<p className="text-gray-400 leading-relaxed">Customize rhythms, BPM, and swing. Perfect for any style from Sertanejo Raiz to Modern Hip-Hop.</p>
</div>
</div>
</div>
</div> </div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
</div> {/* Call to Action */}
<div className="py-32 px-6 relative overflow-hidden">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full h-full bg-gradient-to-r from-blue-900/20 to-purple-900/20 z-0"></div>
<div className="max-w-4xl mx-auto text-center relative z-10">
<h2 className="text-4xl md:text-6xl font-black mb-8 italic uppercase tracking-tighter">Your music journey starts with one code.</h2>
<p className="text-xl text-gray-400 mb-12 italic">Join thousands of creators building the future of sound.</p>
<Link href="/register" className="px-12 py-6 rounded-full bg-white text-black text-2xl font-black hover:bg-[#00E5FF] transition-all uppercase tracking-widest shadow-2xl">
Get Started Now
</Link>
</div>
</div>
{/* Footer */}
<footer className="py-12 px-6 border-t border-gray-900 text-center text-gray-600">
<p>© 2026 {title}. The World&apos;s Most Advanced AI Music Studio.</p>
</footer>
</div>
</>
); );
} }
Starter.getLayout = function getLayout(page: ReactElement) { Home.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>; return <LayoutGuest>{page}</LayoutGuest>;
}; };

View File

@ -1,12 +1,10 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import BaseButton from '../components/BaseButton'; import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox'; import CardBox from '../components/CardBox';
import BaseIcon from "../components/BaseIcon"; import BaseIcon from "../components/BaseIcon";
import { mdiInformation, mdiEye, mdiEyeOff } from '@mdi/js'; import { mdiInformation, mdiEye, mdiEyeOff, mdiKey, mdiEmail } from '@mdi/js';
import SectionFullScreen from '../components/SectionFullScreen'; import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest'; import LayoutGuest from '../layouts/Guest';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
@ -16,7 +14,7 @@ import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons'; import BaseButtons from '../components/BaseButtons';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { getPageTitle } from '../config'; import { getPageTitle } from '../config';
import { findMe, loginUser, resetAction } from '../stores/authSlice'; import { findMe, loginUser, loginWithCode, resetAction } from '../stores/authSlice';
import { useAppDispatch, useAppSelector } from '../stores/hooks'; import { useAppDispatch, useAppSelector } from '../stores/hooks';
import Link from 'next/link'; import Link from 'next/link';
import {toast, ToastContainer} from "react-toastify"; import {toast, ToastContainer} from "react-toastify";
@ -28,13 +26,14 @@ export default function Login() {
const textColor = useAppSelector((state) => state.style.linkColor); const textColor = useAppSelector((state) => state.style.linkColor);
const iconsColor = useAppSelector((state) => state.style.iconsColor); const iconsColor = useAppSelector((state) => state.style.iconsColor);
const notify = (type, msg) => toast(msg, { type }); const notify = (type, msg) => toast(msg, { type });
const [loginMethod, setLoginMethod] = useState<'email' | 'code'>('code');
const [ illustrationImage, setIllustrationImage ] = useState({ const [ illustrationImage, setIllustrationImage ] = useState({
src: undefined, src: undefined,
photographer: undefined, photographer: undefined,
photographer_url: undefined, photographer_url: undefined,
}) })
const [ illustrationVideo, setIllustrationVideo ] = useState({video_files: []}) const [ illustrationVideo, setIllustrationVideo ] = useState({video_files: []})
const [contentType, setContentType] = useState('image'); const [contentType, setContentType] = useState('video');
const [contentPosition, setContentPosition] = useState('right'); const [contentPosition, setContentPosition] = useState('right');
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector( const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector(
@ -44,7 +43,7 @@ export default function Login() {
password: '8e470127', password: '8e470127',
remember: true }) remember: true })
const title = 'Studio Musical Web' const title = 'AI Music Studio'
// Fetch Pexels image/video // Fetch Pexels image/video
useEffect( () => { useEffect( () => {
@ -88,8 +87,12 @@ export default function Login() {
}; };
const handleSubmit = async (value) => { const handleSubmit = async (value) => {
const {remember, ...rest} = value if (loginMethod === 'email') {
await dispatch(loginUser(rest)); const {remember, ...rest} = value
await dispatch(loginUser(rest));
} else {
await dispatch(loginWithCode({ code: value.code }));
}
}; };
const setLogin = (target: HTMLElement) => { const setLogin = (target: HTMLElement) => {
@ -143,7 +146,7 @@ export default function Login() {
}; };
return ( return (
<div style={contentPosition === 'background' ? { <div className="bg-[#121212]" style={contentPosition === 'background' ? {
backgroundImage: `${ backgroundImage: `${
illustrationImage illustrationImage
? `url(${illustrationImage.src?.original})` ? `url(${illustrationImage.src?.original})`
@ -157,101 +160,113 @@ export default function Login() {
<title>{getPageTitle('Login')}</title> <title>{getPageTitle('Login')}</title>
</Head> </Head>
<SectionFullScreen bg='violet'> <SectionFullScreen bg='dark'>
<div className={`flex ${contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'} min-h-screen w-full`}> <div className={`flex ${contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'} min-h-screen w-full`}>
{contentType === 'image' && contentPosition !== 'background' ? imageBlock(illustrationImage) : null} {contentType === 'image' && contentPosition !== 'background' ? imageBlock(illustrationImage) : null}
{contentType === 'video' && contentPosition !== 'background' ? videoBlock(illustrationVideo) : null} {contentType === 'video' && contentPosition !== 'background' ? videoBlock(illustrationVideo) : null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'> <div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full px-4'>
<CardBox id="loginRoles" className='w-full md:w-3/5 lg:w-2/3'> <div className="text-center mb-8">
<h2 className="text-5xl font-black text-white mb-2 tracking-tighter uppercase italic">{title}</h2>
<h2 className="text-4xl font-semibold my-4">{title}</h2> <p className="text-[#00E5FF] font-bold">THE FUTURE OF SOUND</p>
</div>
<div className='flex flex-row justify-between'>
<div> <CardBox className='w-full md:w-3/5 lg:w-2/3 bg-gray-900 border-gray-800 shadow-2xl'>
<div className="flex mb-8 bg-gray-800 rounded-xl p-1">
<p className='mb-2'>Use{' '} <button
<code className={`cursor-pointer ${textColor} `} onClick={() => setLoginMethod('code')}
data-password="8e470127" className={`flex-1 flex items-center justify-center space-x-2 py-3 rounded-lg font-bold transition-all ${loginMethod === 'code' ? 'bg-[#00E5FF] text-black shadow-lg' : 'text-gray-400 hover:text-white'}`}
onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '} >
<code className={`${textColor}`}>8e470127</code>{' / '} <BaseIcon path={mdiKey} size={20} />
to login as Admin</p> <span>ACCESS CODE</span>
<p>Use <code </button>
className={`cursor-pointer ${textColor} `} <button
data-password="f04a8902244c" onClick={() => setLoginMethod('email')}
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '} className={`flex-1 flex items-center justify-center space-x-2 py-3 rounded-lg font-bold transition-all ${loginMethod === 'email' ? 'bg-[#00E5FF] text-black shadow-lg' : 'text-gray-400 hover:text-white'}`}
<code className={`${textColor}`}>f04a8902244c</code>{' / '} >
to login as User</p> <BaseIcon path={mdiEmail} size={20} />
</div> <span>EMAIL LOGIN</span>
<div> </button>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
path={mdiInformation}
/>
</div>
</div> </div>
</CardBox>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<Formik <Formik
initialValues={initialValues} initialValues={{
...initialValues,
code: ''
}}
enableReinitialize enableReinitialize
onSubmit={(values) => handleSubmit(values)} onSubmit={(values) => handleSubmit(values)}
> >
<Form> <Form>
<FormField {loginMethod === 'email' ? (
label='Login' <>
help='Please enter your login'> <FormField
<Field name='email' /> label='Login'
</FormField> help='Please enter your email'>
<Field name='email' className="bg-gray-800 border-gray-700 text-white focus:ring-[#00E5FF]" />
</FormField>
<div className='relative'> <div className='relative'>
<FormField <FormField
label='Password' label='Password'
help='Please enter your password'> help='Please enter your password'>
<Field name='password' type={showPassword ? 'text' : 'password'} /> <Field name='password' type={showPassword ? 'text' : 'password'} className="bg-gray-800 border-gray-700 text-white focus:ring-[#00E5FF]" />
</FormField> </FormField>
<div <div
className='absolute bottom-8 right-0 pr-3 flex items-center cursor-pointer' className='absolute bottom-8 right-0 pr-3 flex items-center cursor-pointer'
onClick={togglePasswordVisibility} onClick={togglePasswordVisibility}
> >
<BaseIcon <BaseIcon
className='text-gray-500 hover:text-gray-700' className='text-gray-500 hover:text-[#00E5FF]'
size={20} size={20}
path={showPassword ? mdiEyeOff : mdiEye} path={showPassword ? mdiEyeOff : mdiEye}
/> />
</div>
</div>
<div className={'flex justify-between mb-6'}>
<FormCheckRadio type='checkbox' label='Remember me'>
<Field type='checkbox' name='remember' />
</FormCheckRadio>
<Link className={`text-[#00E5FF] font-bold hover:underline`} href={'/forgot'}>
Forgot password?
</Link>
</div>
</>
) : (
<div className="mb-8">
<FormField
label='Access Code'
help='Enter the code generated for your account'>
<Field
name='code'
placeholder="XXXXXX"
className="bg-gray-800 border-gray-700 text-white text-3xl text-center font-black tracking-widest placeholder-gray-600 focus:ring-[#00E5FF] focus:border-[#00E5FF] rounded-xl py-6"
/>
</FormField>
<p className="text-sm text-gray-500 mt-4 italic text-center">
Each creator has a unique system-generated code.
</p>
</div> </div>
</div> )}
<div className={'flex justify-between'}>
<FormCheckRadio type='checkbox' label='Remember'>
<Field type='checkbox' name='remember' />
</FormCheckRadio>
<Link className={`${textColor} text-blue-600`} href={'/forgot'}>
Forgot password?
</Link>
</div>
<BaseDivider />
<BaseButtons> <BaseButtons>
<BaseButton <BaseButton
className={'w-full'} className={'w-full py-4 text-xl font-black rounded-xl'}
type='submit' type='submit'
label={isFetching ? 'Loading...' : 'Login'} label={isFetching ? 'ENTERING...' : 'ENTER STUDIO'}
color='info' color='info'
disabled={isFetching} disabled={isFetching}
/> />
</BaseButtons> </BaseButtons>
<br />
<p className={'text-center'}> <BaseDivider className="border-gray-800" />
Dont have an account yet?{' '}
<Link className={`${textColor}`} href={'/register'}> <p className={'text-center text-gray-400 font-medium'}>
New Account New to the studio?{' '}
<Link className={`text-[#00E5FF] font-black hover:underline`} href={'/register'}>
Generate New Code
</Link> </Link>
</p> </p>
</Form> </Form>
@ -260,17 +275,17 @@ export default function Login() {
</div> </div>
</div> </div>
</SectionFullScreen> </SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'> <div className='bg-[#121212] text-gray-500 flex flex-col text-center justify-center md:flex-row border-t border-gray-900'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. © All rights reserved</p> <p className='py-6 text-sm'>© 2026 <span className="text-white font-bold">{title}</span>. Built for Creators.</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'> <Link className='py-6 ml-4 text-sm hover:text-white' href='/privacy-policy/'>
Privacy Policy Privacy Policy
</Link> </Link>
</div> </div>
<ToastContainer /> <ToastContainer theme="dark" />
</div> </div>
); );
} }
Login.getLayout = function getLayout(page: ReactElement) { Login.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>; return <LayoutGuest>{page}</LayoutGuest>;
}; };

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useState } from 'react';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
import Head from 'next/head'; import Head from 'next/head';
@ -12,78 +12,138 @@ import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons'; import BaseButtons from '../components/BaseButtons';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { getPageTitle } from '../config'; import { getPageTitle } from '../config';
import BaseIcon from '../components/BaseIcon';
import { mdiMusic, mdiContentCopy, mdiCheckDecagram } from '@mdi/js';
import Link from 'next/link';
import axios from "axios"; import axios from "axios";
import jwt from 'jsonwebtoken';
export default function Register() { export default function Register() {
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const [accessCode, setAccessCode] = useState<string | null>(null);
const router = useRouter(); const router = useRouter();
const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); const notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const handleSubmit = async (value) => { const handleSubmit = async (value) => {
setLoading(true) setLoading(true)
try { try {
const { data: token } = await axios.post('/auth/signup', value);
const decoded: any = jwt.decode(token);
const { data: response } = await axios.post('/auth/signup',value); if (decoded && decoded.user && decoded.user.accessCode) {
await router.push('/login') setAccessCode(decoded.user.accessCode);
notify('success', 'Account created! Here is your access code.');
} else {
await router.push('/login')
notify('success', 'Account created! Please login.')
}
setLoading(false) setLoading(false)
notify('success', 'Please check your email for verification link')
} catch (error) { } catch (error) {
setLoading(false) setLoading(false)
console.log('error: ', error) console.log('error: ', error)
notify('error', 'Something was wrong. Try again') notify('error', 'Something went wrong. Try again')
} }
}; };
const copyToClipboard = () => {
if (accessCode) {
navigator.clipboard.writeText(accessCode);
notify('info', 'Code copied to clipboard!');
}
}
return ( return (
<> <div className="bg-[#121212] min-h-screen">
<Head> <Head>
<title>{getPageTitle('Login')}</title> <title>{getPageTitle('Register')}</title>
</Head> </Head>
<SectionFullScreen bg='violet'> <SectionFullScreen bg='dark'>
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'> <div className="flex flex-col items-center justify-center w-full px-4">
<Formik <div className="text-center mb-8">
initialValues={{ <div className="flex items-center justify-center space-x-2 mb-2">
email: '', <BaseIcon path={mdiMusic} size={40} className="text-[#00E5FF]" />
password: '', <h2 className="text-4xl font-black text-white tracking-tighter uppercase italic">AI Music Studio</h2>
confirm: '' </div>
}} <p className="text-gray-400 font-bold">JOIN THE CREATIVE REVOLUTION</p>
onSubmit={(values) => handleSubmit(values)} </div>
>
<Form>
<FormField label='Email' help='Please enter your email'>
<Field type='email' name='email' />
</FormField>
<FormField label='Password' help='Please enter your password'>
<Field type='password' name='password' />
</FormField>
<FormField label='Confirm Password' help='Please confirm your password'>
<Field type='password' name='confirm' />
</FormField>
<BaseDivider /> {!accessCode ? (
<CardBox className='w-full md:w-3/5 lg:w-1/3 xl:w-1/4 bg-gray-900 border-gray-800 shadow-2xl'>
<Formik
initialValues={{
email: '',
password: '',
confirm: ''
}}
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Email' help='Your creative identity'>
<Field type='email' name='email' className="bg-gray-800 border-gray-700 text-white focus:ring-[#00E5FF]" />
</FormField>
<FormField label='Password' help='Keep your studio secure'>
<Field type='password' name='password' className="bg-gray-800 border-gray-700 text-white focus:ring-[#00E5FF]" />
</FormField>
<FormField label='Confirm Password' help='Just to be sure'>
<Field type='password' name='confirm' className="bg-gray-800 border-gray-700 text-white focus:ring-[#00E5FF]" />
</FormField>
<BaseButtons> <BaseDivider className="border-gray-800" />
<BaseButtons>
<BaseButton
type='submit'
label={loading ? 'CREATING...' : 'GENERATE ACCESS CODE' }
color='info'
className="w-full py-4 font-black rounded-xl shadow-[0_0_15px_rgba(0,229,255,0.3)] hover:scale-105 transition-transform"
/>
</BaseButtons>
<div className="mt-8 text-center">
<p className="text-gray-400">
Already have a code?{' '}
<Link href="/login" className="text-[#00E5FF] font-black hover:underline">
Login Here
</Link>
</p>
</div>
</Form>
</Formik>
</CardBox>
) : (
<CardBox className='w-full md:w-3/5 lg:w-1/3 xl:w-1/4 bg-gray-900 border-gray-800 shadow-2xl text-center py-10'>
<BaseIcon path={mdiCheckDecagram} size={64} className="text-[#00E5FF] mx-auto mb-6" />
<h3 className="text-2xl font-black text-white mb-2 uppercase italic tracking-tighter">Your Access Code</h3>
<p className="text-gray-400 mb-8">Save this code. You&apos;ll need it to enter your studio.</p>
<div className="bg-black border border-gray-800 rounded-2xl p-8 mb-8 relative group cursor-pointer" onClick={copyToClipboard}>
<div className="text-5xl font-black text-white tracking-[0.2em] mb-4">{accessCode}</div>
<div className="text-[#00E5FF] flex items-center justify-center space-x-2 opacity-60 group-hover:opacity-100 transition-opacity">
<BaseIcon path={mdiContentCopy} size={20} />
<span className="text-sm font-bold uppercase">Click to Copy</span>
</div>
</div>
<BaseButtons className="flex-col space-y-4">
<BaseButton <BaseButton
type='submit' href="/login"
label={loading ? 'Loading...' : 'Register' } label="ENTER STUDIO NOW"
color='info' color="info"
/> className="w-full py-4 font-black rounded-xl text-xl shadow-[0_0_20px_rgba(0,229,255,0.4)]"
<BaseButton
href={'/login'}
label={'Login'}
color='info'
/> />
<Link href="/register" onClick={() => setAccessCode(null)} className="text-gray-500 hover:text-white font-bold transition-colors">
Create another account
</Link>
</BaseButtons> </BaseButtons>
</Form> </CardBox>
</Formik> )}
</CardBox> </div>
</SectionFullScreen> </SectionFullScreen>
<ToastContainer /> <ToastContainer theme="dark" />
</> </div>
); );
} }

View File

@ -0,0 +1,272 @@
import {
mdiMusic,
mdiRobotOutline,
mdiPlus,
mdiPlay,
mdiPiano,
mdiGuitarAcoustic,
mdiMicrophone,
mdiTuneVariant
} from '@mdi/js';
import Head from 'next/head';
import React, { ReactElement, useEffect, useState } from 'react';
import CardBox from '../components/CardBox';
import LayoutAuthenticated from '../layouts/Authenticated';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { getPageTitle } from '../config';
import BaseIcon from '../components/BaseIcon';
import BaseButton from '../components/BaseButton';
import FormField from '../components/FormField';
import BaseDivider from '../components/BaseDivider';
import axios from 'axios';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { aiResponse } from '../stores/openAiSlice';
import { toast, ToastContainer } from 'react-toastify';
const Studio = () => {
const dispatch = useAppDispatch();
const { currentUser } = useAppSelector((state) => state.auth);
const { isAskingResponse, aiResponse: gptResult } = useAppSelector((state) => state.openAi);
const [prompt, setPrompt] = useState('');
const [style, setStyle] = useState('Pop');
const [projects, setProjects] = useState([]);
const [instruments, setInstruments] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
fetchStudioData();
}, []);
const fetchStudioData = async () => {
try {
setLoading(true);
const [projRes, instRes] = await Promise.all([
axios.get('/projects?limit=5'),
axios.get('/instruments?limit=8')
]);
setProjects(projRes.data.rows || []);
setInstruments(instRes.data.rows || []);
} catch (error) {
console.error('Error fetching studio data:', error);
} finally {
setLoading(false);
}
};
const handleAiGenerate = async () => {
if (!prompt) {
toast.error('Please enter an idea for your song');
return;
}
const fullPrompt = `Create a song structure based on this idea: "${prompt}". Style: ${style}. Return a JSON with title, lyrics (verses, chorus), and suggested instruments.`;
dispatch(aiResponse({
input: [
{ role: 'system', content: 'You are a professional music producer and songwriter.' },
{ role: 'user', content: fullPrompt },
],
options: { poll_interval: 5, poll_timeout: 300 },
}));
};
useEffect(() => {
if (gptResult) {
toast.success('AI has generated your song structure!');
// In a real app, we would save this as a new project
}
}, [gptResult]);
return (
<>
<Head>
<title>{getPageTitle('Musical Studio')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiMusic} title='Musical Studio' main>
<BaseButton
label="New Project"
color="info"
icon={mdiPlus}
onClick={() => toast.info('Manual project creation coming soon!')}
/>
</SectionTitleLineWithButton>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* AI Generator Section */}
<div className="lg:col-span-2 space-y-6">
<CardBox className="bg-gradient-to-br from-gray-900 to-indigo-900 border-none shadow-2xl overflow-hidden relative">
<div className="absolute top-0 right-0 p-4 opacity-10">
<BaseIcon path={mdiRobotOutline} size={120} />
</div>
<div className="relative z-10">
<h2 className="text-2xl font-bold text-white mb-4 flex items-center">
<BaseIcon path={mdiRobotOutline} className="mr-2 text-[#00E5FF]" />
AI SONGWRITER
</h2>
<p className="text-indigo-200 mb-6">Describe your idea, choose a style, and let AI build the foundation of your next hit.</p>
<div className="space-y-4">
<FormField label="Your Song Idea" labelColor="text-white">
<textarea
className="w-full bg-black/40 border-gray-700 text-white rounded-xl p-4 focus:ring-[#00E5FF]"
placeholder="e.g. A romantic song about a summer night in Ibiza..."
rows={3}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
/>
</FormField>
<div className="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4">
<div className="flex-1">
<FormField label="Musical Style" labelColor="text-white">
<select
className="w-full bg-black/40 border-gray-700 text-white rounded-xl p-3 focus:ring-[#00E5FF]"
value={style}
onChange={(e) => setStyle(e.target.value)}
>
<option>Pop</option>
<option>Rock</option>
<option>Sertanejo</option>
<option>Hip-Hop</option>
<option>Jazz</option>
<option>Electronic</option>
<option>MPB</option>
</select>
</FormField>
</div>
<div className="flex items-end">
<BaseButton
label={isAskingResponse ? "COMPOSING..." : "GENERATE SONG"}
color="info"
className="w-full md:w-auto px-8 py-3 font-bold rounded-xl shadow-[0_0_15px_rgba(0,229,255,0.4)]"
disabled={isAskingResponse}
onClick={handleAiGenerate}
/>
</div>
</div>
</div>
</div>
</CardBox>
{gptResult && (
<CardBox className="border-[#00E5FF]/30 bg-black/40 backdrop-blur-md animate-fade-in">
<h3 className="text-xl font-bold text-[#00E5FF] mb-4 uppercase tracking-wider italic">Generated Structure</h3>
<div className="prose prose-invert max-w-none">
<pre className="whitespace-pre-wrap font-mono text-sm bg-gray-900/50 p-6 rounded-2xl border border-gray-800">
{typeof gptResult === 'string' ? gptResult : JSON.stringify(gptResult, null, 2)}
</pre>
</div>
<div className="mt-6 flex space-x-4">
<BaseButton label="Create Project from this" color="success" icon={mdiPlus} />
<BaseButton label="Refine" color="white" outline />
</div>
</CardBox>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<CardBox title="Recent Projects" icon={mdiMusic}>
{projects.length > 0 ? (
<div className="space-y-4">
{projects.map((project: any) => (
<div key={project.id} className="flex items-center justify-between p-3 bg-gray-800/50 rounded-xl hover:bg-gray-800 transition-colors cursor-pointer group">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-indigo-900 rounded-lg flex items-center justify-center">
<BaseIcon path={mdiMusic} className="text-indigo-400" />
</div>
<div>
<div className="font-bold text-sm">{project.name || 'Untitled Project'}</div>
<div className="text-xs text-gray-500">{project.genre?.name || 'No Genre'}</div>
</div>
</div>
<BaseIcon path={mdiPlay} className="text-gray-600 group-hover:text-[#00E5FF] transition-colors" />
</div>
))}
<BaseButton href="/projects/projects-list" label="View All Projects" color="info" className="w-full" outline />
</div>
) : (
<div className="text-center py-8 text-gray-500">
<p>No projects yet. Start by generating one!</p>
</div>
)}
</CardBox>
<CardBox title="Your Instruments" icon={mdiPiano}>
<div className="grid grid-cols-2 gap-3">
{instruments.map((inst: any) => (
<div key={inst.id} className="p-3 bg-gray-800/50 rounded-xl flex flex-col items-center text-center hover:bg-gray-800 transition-colors cursor-pointer">
<BaseIcon path={mdiPiano} className="text-[#03DAC6] mb-2" />
<span className="text-xs font-medium">{inst.name}</span>
</div>
))}
<div className="p-3 bg-gray-900 border border-dashed border-gray-700 rounded-xl flex flex-col items-center text-center justify-center text-gray-500 hover:text-white hover:border-white transition-all cursor-pointer">
<BaseIcon path={mdiPlus} size={20} />
<span className="text-[10px] uppercase font-bold mt-1">Add More</span>
</div>
</div>
</CardBox>
</div>
</div>
{/* Sidebar / Quick Settings */}
<div className="space-y-6">
<CardBox title="Studio Status" icon={mdiTuneVariant}>
<div className="space-y-6">
<div>
<div className="flex justify-between text-xs font-bold uppercase tracking-widest text-gray-500 mb-2">
<span>Master Volume</span>
<span>85%</span>
</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
<div className="h-full bg-gradient-to-r from-[#00E5FF] to-[#BB86FC] w-[85%]"></div>
</div>
</div>
<div className="grid grid-cols-2 gap-4 text-center">
<div className="bg-gray-800 p-4 rounded-2xl border border-gray-700">
<div className="text-2xl font-black text-white">128</div>
<div className="text-[10px] text-gray-500 uppercase font-bold">BPM</div>
</div>
<div className="bg-gray-800 p-4 rounded-2xl border border-gray-700">
<div className="text-2xl font-black text-white">4/4</div>
<div className="text-[10px] text-gray-500 uppercase font-bold">Time</div>
</div>
</div>
<BaseDivider />
<div className="space-y-3">
<h4 className="text-xs font-black text-gray-500 uppercase">Active Effects</h4>
<div className="flex flex-wrap gap-2">
{['Reverb', 'Compressor', 'Delay'].map(fx => (
<span key={fx} className="px-3 py-1 bg-indigo-900/30 text-indigo-400 border border-indigo-900 rounded-full text-[10px] font-bold">
{fx}
</span>
))}
</div>
</div>
</div>
</CardBox>
<CardBox className="bg-gradient-to-br from-[#00E5FF] to-[#BB86FC] p-0 border-none overflow-hidden">
<div className="p-6 text-black">
<h3 className="text-xl font-black mb-2 italic">GO PRO</h3>
<p className="text-sm font-medium mb-4">Unlock unlimited AI generations and 500+ premium instruments.</p>
<BaseButton label="Upgrade Now" color="white" className="w-full font-black rounded-xl" />
</div>
</CardBox>
</div>
</div>
</SectionMain>
<ToastContainer theme="dark" />
</>
);
};
Studio.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default Studio;

View File

@ -40,6 +40,36 @@ export const loginUser = createAsyncThunk(
} }
); );
export const loginWithCode = createAsyncThunk(
'auth/loginWithCode',
async (creds: { code: string }, { rejectWithValue }) => {
try {
const response = await axios.post('auth/signin/code', creds);
return response.data;
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
}
);
export const signupUser = createAsyncThunk(
'auth/signupUser',
async (creds: Record<string, string>, { rejectWithValue }) => {
try {
const response = await axios.post('auth/signup', creds);
return response.data;
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
}
);
export const passwordReset = createAsyncThunk( export const passwordReset = createAsyncThunk(
'auth/passwordReset', 'auth/passwordReset',
async (value: Record<string, string>, { rejectWithValue }) => { async (value: Record<string, string>, { rejectWithValue }) => {
@ -79,10 +109,7 @@ export const authSlice = createSlice({
}, },
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(loginUser.pending, (state) => { const handleAuthFulfilled = (state, action) => {
state.isFetching = true;
});
builder.addCase(loginUser.fulfilled, (state, action) => {
const token = action.payload; const token = action.payload;
const user = jwt.decode(token); const user = jwt.decode(token);
@ -91,12 +118,36 @@ export const authSlice = createSlice({
localStorage.setItem('token', token); localStorage.setItem('token', token);
localStorage.setItem('user', JSON.stringify(user)); localStorage.setItem('user', JSON.stringify(user));
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token; axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
}); state.isFetching = false;
};
builder.addCase(loginUser.pending, (state) => {
state.isFetching = true;
});
builder.addCase(loginUser.fulfilled, handleAuthFulfilled);
builder.addCase(loginUser.rejected, (state, action) => { builder.addCase(loginUser.rejected, (state, action) => {
state.errorMessage = String(action.payload) || 'Something went wrong. Try again'; state.errorMessage = String(action.payload) || 'Something went wrong. Try again';
state.isFetching = false; state.isFetching = false;
}); });
builder.addCase(loginWithCode.pending, (state) => {
state.isFetching = true;
});
builder.addCase(loginWithCode.fulfilled, handleAuthFulfilled);
builder.addCase(loginWithCode.rejected, (state, action) => {
state.errorMessage = String(action.payload) || 'Invalid access code';
state.isFetching = false;
});
builder.addCase(signupUser.pending, (state) => {
state.isFetching = true;
});
builder.addCase(signupUser.fulfilled, handleAuthFulfilled);
builder.addCase(signupUser.rejected, (state, action) => {
state.errorMessage = String(action.payload) || 'Something went wrong. Try again';
state.isFetching = false;
});
builder.addCase(findMe.pending, () => { builder.addCase(findMe.pending, () => {
console.log('Pending findMe'); console.log('Pending findMe');
}); });
@ -121,4 +172,4 @@ export const authSlice = createSlice({
// Action creators are generated for each case reducer function // Action creators are generated for each case reducer function
export const { logoutUser } = authSlice.actions; export const { logoutUser } = authSlice.actions;
export default authSlice.reducer; export default authSlice.reducer;