1
This commit is contained in:
parent
e56befb06a
commit
14794ed687
@ -62,6 +62,35 @@ router.post('/signin/local', wrapAsync(async (req, res) => {
|
||||
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
|
||||
* /api/auth/me:
|
||||
@ -204,4 +233,4 @@ function socialRedirect(res, state, token, config) {
|
||||
res.redirect(config.uiUrl + "/login?token=" + token);
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
@ -1,4 +1,5 @@
|
||||
const UsersDBApi = require('../db/api/users');
|
||||
const Access_codesDBApi = require('../db/api/access_codes');
|
||||
const ValidationError = require('./notifications/errors/validation');
|
||||
const ForbiddenError = require('./notifications/errors/forbidden');
|
||||
const bcrypt = require('bcrypt');
|
||||
@ -8,6 +9,7 @@ const PasswordResetEmail = require('./email/list/passwordReset');
|
||||
const EmailSender = require('./email');
|
||||
const config = require('../config');
|
||||
const helpers = require('../helpers');
|
||||
const db = require('../db/models');
|
||||
|
||||
class Auth {
|
||||
static async signup(email, password, options = {}, host) {
|
||||
@ -18,6 +20,8 @@ class Auth {
|
||||
config.bcrypt.saltRounds,
|
||||
);
|
||||
|
||||
let currentUser;
|
||||
|
||||
if (user) {
|
||||
if (user.authenticationUid) {
|
||||
throw new ValidationError(
|
||||
@ -37,44 +41,40 @@ class Auth {
|
||||
options,
|
||||
);
|
||||
|
||||
if (EmailSender.isConfigured) {
|
||||
await this.sendEmailAddressVerificationEmail(
|
||||
user.email,
|
||||
host,
|
||||
);
|
||||
}
|
||||
|
||||
const data = {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email
|
||||
}
|
||||
};
|
||||
|
||||
return helpers.jwtSign(data);
|
||||
currentUser = user;
|
||||
} else {
|
||||
currentUser = await UsersDBApi.createFromAuth(
|
||||
{
|
||||
firstName: email.split('@')[0],
|
||||
password: hashedPassword,
|
||||
email: email,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
const newUser = await UsersDBApi.createFromAuth(
|
||||
{
|
||||
firstName: email.split('@')[0],
|
||||
password: hashedPassword,
|
||||
email: email,
|
||||
|
||||
},
|
||||
options,
|
||||
);
|
||||
// Generate Access Code
|
||||
const code = Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||
await Access_codesDBApi.create({
|
||||
code,
|
||||
status: 'active',
|
||||
user: currentUser.id,
|
||||
max_uses: 1000,
|
||||
uses_count: 0
|
||||
}, options);
|
||||
|
||||
if (EmailSender.isConfigured) {
|
||||
await this.sendEmailAddressVerificationEmail(
|
||||
newUser.email,
|
||||
currentUser.email,
|
||||
host,
|
||||
);
|
||||
}
|
||||
|
||||
const data = {
|
||||
user: {
|
||||
id: newUser.id,
|
||||
email: newUser.email
|
||||
id: currentUser.id,
|
||||
email: currentUser.email,
|
||||
accessCode: code // Return the code so the user knows it
|
||||
}
|
||||
};
|
||||
|
||||
@ -133,6 +133,35 @@ class Auth {
|
||||
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(
|
||||
email,
|
||||
host,
|
||||
@ -309,4 +338,4 @@ class Auth {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Auth;
|
||||
module.exports = Auth;
|
||||
@ -1,6 +1,5 @@
|
||||
import React, {useEffect, useRef} from 'react'
|
||||
import React, {useEffect, useRef, useState} from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||
import BaseDivider from './BaseDivider'
|
||||
import BaseIcon from './BaseIcon'
|
||||
@ -129,4 +128,4 @@ export default function NavBarItem({ item }: Props) {
|
||||
}
|
||||
|
||||
return <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div>
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
import React, { ReactNode, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import React, { ReactNode, useEffect, useState } from 'react'
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||
import menuAside from '../menuAside'
|
||||
@ -126,4 +125,4 @@ export default function LayoutAuthenticated({
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,11 @@ const menuAside: MenuAsideItem[] = [
|
||||
icon: icon.mdiViewDashboardOutline,
|
||||
label: 'Dashboard',
|
||||
},
|
||||
{
|
||||
href: '/studio',
|
||||
icon: icon.mdiMusic,
|
||||
label: 'Musical Studio',
|
||||
},
|
||||
|
||||
{
|
||||
href: '/users/users-list',
|
||||
@ -184,4 +189,4 @@ const menuAside: MenuAsideItem[] = [
|
||||
},
|
||||
]
|
||||
|
||||
export default menuAside
|
||||
export default menuAside
|
||||
@ -1,166 +1,112 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { ReactElement } from 'react';
|
||||
import Head from 'next/head';
|
||||
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 BaseDivider from '../components/BaseDivider';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import SectionFullScreen from '../components/SectionFullScreen';
|
||||
import { mdiMusic, mdiMicrophone, mdiPiano, mdiChartTimelineVariant } from '@mdi/js';
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
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 (
|
||||
<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>
|
||||
<title>{getPageTitle('Starter Page')}</title>
|
||||
<title>{getPageTitle('Home')}</title>
|
||||
</Head>
|
||||
|
||||
<SectionFullScreen bg='violet'>
|
||||
<div
|
||||
className={`flex ${
|
||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
||||
} min-h-screen w-full`}
|
||||
>
|
||||
{contentType === 'image' && contentPosition !== 'background'
|
||||
? imageBlock(illustrationImage)
|
||||
: 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'>
|
||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
||||
<CardBoxComponentTitle title="Welcome to your Studio Musical Web app!"/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<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>
|
||||
<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>
|
||||
<div className="bg-[#121212] min-h-screen text-white font-sans overflow-x-hidden">
|
||||
{/* Navigation */}
|
||||
<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">
|
||||
<div className="flex items-center space-x-2">
|
||||
<BaseIcon path={mdiMusic} size={32} className="text-[#00E5FF]" />
|
||||
<span className="text-2xl font-bold tracking-tight">{title}</span>
|
||||
</div>
|
||||
<div className="hidden md:flex space-x-8 items-center font-medium">
|
||||
<a href="#features" className="hover:text-[#00E5FF] transition-colors">Features</a>
|
||||
<a href="#instruments" className="hover:text-[#00E5FF] transition-colors">Instruments</a>
|
||||
<Link href="/login" className="px-5 py-2 rounded-full border border-[#00E5FF] text-[#00E5FF] hover:bg-[#00E5FF] hover:text-black transition-all">
|
||||
Login
|
||||
</Link>
|
||||
<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)]">
|
||||
Start Creating
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* 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>
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
href='/login'
|
||||
label='Login'
|
||||
color='info'
|
||||
className='w-full'
|
||||
/>
|
||||
<h1 className="text-5xl md:text-7xl lg:text-8xl font-black mb-8 leading-tight tracking-tighter">
|
||||
CREATE YOUR <span className="text-transparent bg-clip-text bg-gradient-to-r from-[#00E5FF] to-[#BB86FC]">SYMPHONY</span> IN SECONDS
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl text-gray-400 mb-12 max-w-2xl mx-auto leading-relaxed">
|
||||
From Sertanejo to Hip-Hop. Professional instruments, intelligent beats, and AI lyrics generator — all with just your access code.
|
||||
</p>
|
||||
<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>
|
||||
</CardBox>
|
||||
{/* Features Section */}
|
||||
<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 — 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>
|
||||
</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'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>;
|
||||
};
|
||||
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import Head from 'next/head';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import CardBox from '../components/CardBox';
|
||||
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 LayoutGuest from '../layouts/Guest';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
@ -16,7 +14,7 @@ import BaseDivider from '../components/BaseDivider';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import { useRouter } from 'next/router';
|
||||
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 Link from 'next/link';
|
||||
import {toast, ToastContainer} from "react-toastify";
|
||||
@ -28,13 +26,14 @@ export default function Login() {
|
||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
||||
const notify = (type, msg) => toast(msg, { type });
|
||||
const [loginMethod, setLoginMethod] = useState<'email' | 'code'>('code');
|
||||
const [ illustrationImage, setIllustrationImage ] = useState({
|
||||
src: undefined,
|
||||
photographer: undefined,
|
||||
photographer_url: undefined,
|
||||
})
|
||||
const [ illustrationVideo, setIllustrationVideo ] = useState({video_files: []})
|
||||
const [contentType, setContentType] = useState('image');
|
||||
const [contentType, setContentType] = useState('video');
|
||||
const [contentPosition, setContentPosition] = useState('right');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector(
|
||||
@ -44,7 +43,7 @@ export default function Login() {
|
||||
password: '8e470127',
|
||||
remember: true })
|
||||
|
||||
const title = 'Studio Musical Web'
|
||||
const title = 'AI Music Studio'
|
||||
|
||||
// Fetch Pexels image/video
|
||||
useEffect( () => {
|
||||
@ -88,8 +87,12 @@ export default function Login() {
|
||||
};
|
||||
|
||||
const handleSubmit = async (value) => {
|
||||
const {remember, ...rest} = value
|
||||
await dispatch(loginUser(rest));
|
||||
if (loginMethod === 'email') {
|
||||
const {remember, ...rest} = value
|
||||
await dispatch(loginUser(rest));
|
||||
} else {
|
||||
await dispatch(loginWithCode({ code: value.code }));
|
||||
}
|
||||
};
|
||||
|
||||
const setLogin = (target: HTMLElement) => {
|
||||
@ -143,7 +146,7 @@ export default function Login() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={contentPosition === 'background' ? {
|
||||
<div className="bg-[#121212]" style={contentPosition === 'background' ? {
|
||||
backgroundImage: `${
|
||||
illustrationImage
|
||||
? `url(${illustrationImage.src?.original})`
|
||||
@ -157,101 +160,113 @@ export default function Login() {
|
||||
<title>{getPageTitle('Login')}</title>
|
||||
</Head>
|
||||
|
||||
<SectionFullScreen bg='violet'>
|
||||
<SectionFullScreen bg='dark'>
|
||||
<div className={`flex ${contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'} min-h-screen w-full`}>
|
||||
{contentType === 'image' && contentPosition !== 'background' ? imageBlock(illustrationImage) : 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'>
|
||||
|
||||
<h2 className="text-4xl font-semibold my-4">{title}</h2>
|
||||
|
||||
<div className='flex flex-row justify-between'>
|
||||
<div>
|
||||
|
||||
<p className='mb-2'>Use{' '}
|
||||
<code className={`cursor-pointer ${textColor} `}
|
||||
data-password="8e470127"
|
||||
onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '}
|
||||
<code className={`${textColor}`}>8e470127</code>{' / '}
|
||||
to login as Admin</p>
|
||||
<p>Use <code
|
||||
className={`cursor-pointer ${textColor} `}
|
||||
data-password="f04a8902244c"
|
||||
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '}
|
||||
<code className={`${textColor}`}>f04a8902244c</code>{' / '}
|
||||
to login as User</p>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w='w-16'
|
||||
h='h-16'
|
||||
size={48}
|
||||
path={mdiInformation}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="text-5xl font-black text-white mb-2 tracking-tighter uppercase italic">{title}</h2>
|
||||
<p className="text-[#00E5FF] font-bold">THE FUTURE OF SOUND</p>
|
||||
</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">
|
||||
<button
|
||||
onClick={() => setLoginMethod('code')}
|
||||
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'}`}
|
||||
>
|
||||
<BaseIcon path={mdiKey} size={20} />
|
||||
<span>ACCESS CODE</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLoginMethod('email')}
|
||||
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'}`}
|
||||
>
|
||||
<BaseIcon path={mdiEmail} size={20} />
|
||||
<span>EMAIL LOGIN</span>
|
||||
</button>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
||||
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
initialValues={{
|
||||
...initialValues,
|
||||
code: ''
|
||||
}}
|
||||
enableReinitialize
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
<FormField
|
||||
label='Login'
|
||||
help='Please enter your login'>
|
||||
<Field name='email' />
|
||||
</FormField>
|
||||
{loginMethod === 'email' ? (
|
||||
<>
|
||||
<FormField
|
||||
label='Login'
|
||||
help='Please enter your email'>
|
||||
<Field name='email' className="bg-gray-800 border-gray-700 text-white focus:ring-[#00E5FF]" />
|
||||
</FormField>
|
||||
|
||||
<div className='relative'>
|
||||
<FormField
|
||||
label='Password'
|
||||
help='Please enter your password'>
|
||||
<Field name='password' type={showPassword ? 'text' : 'password'} />
|
||||
</FormField>
|
||||
<div
|
||||
className='absolute bottom-8 right-0 pr-3 flex items-center cursor-pointer'
|
||||
onClick={togglePasswordVisibility}
|
||||
>
|
||||
<BaseIcon
|
||||
className='text-gray-500 hover:text-gray-700'
|
||||
size={20}
|
||||
path={showPassword ? mdiEyeOff : mdiEye}
|
||||
/>
|
||||
<div className='relative'>
|
||||
<FormField
|
||||
label='Password'
|
||||
help='Please enter your password'>
|
||||
<Field name='password' type={showPassword ? 'text' : 'password'} className="bg-gray-800 border-gray-700 text-white focus:ring-[#00E5FF]" />
|
||||
</FormField>
|
||||
<div
|
||||
className='absolute bottom-8 right-0 pr-3 flex items-center cursor-pointer'
|
||||
onClick={togglePasswordVisibility}
|
||||
>
|
||||
<BaseIcon
|
||||
className='text-gray-500 hover:text-[#00E5FF]'
|
||||
size={20}
|
||||
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 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>
|
||||
<BaseButton
|
||||
className={'w-full'}
|
||||
className={'w-full py-4 text-xl font-black rounded-xl'}
|
||||
type='submit'
|
||||
label={isFetching ? 'Loading...' : 'Login'}
|
||||
label={isFetching ? 'ENTERING...' : 'ENTER STUDIO'}
|
||||
color='info'
|
||||
disabled={isFetching}
|
||||
/>
|
||||
</BaseButtons>
|
||||
<br />
|
||||
<p className={'text-center'}>
|
||||
Don’t have an account yet?{' '}
|
||||
<Link className={`${textColor}`} href={'/register'}>
|
||||
New Account
|
||||
|
||||
<BaseDivider className="border-gray-800" />
|
||||
|
||||
<p className={'text-center text-gray-400 font-medium'}>
|
||||
New to the studio?{' '}
|
||||
<Link className={`text-[#00E5FF] font-black hover:underline`} href={'/register'}>
|
||||
Generate New Code
|
||||
</Link>
|
||||
</p>
|
||||
</Form>
|
||||
@ -260,17 +275,17 @@ export default function Login() {
|
||||
</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/'>
|
||||
<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 className="text-white font-bold">{title}</span>. Built for Creators.</p>
|
||||
<Link className='py-6 ml-4 text-sm hover:text-white' href='/privacy-policy/'>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</div>
|
||||
<ToastContainer />
|
||||
<ToastContainer theme="dark" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Login.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import Head from 'next/head';
|
||||
@ -12,78 +12,138 @@ import BaseDivider from '../components/BaseDivider';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import { useRouter } from 'next/router';
|
||||
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 jwt from 'jsonwebtoken';
|
||||
|
||||
export default function Register() {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [accessCode, setAccessCode] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
const notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
|
||||
|
||||
|
||||
const handleSubmit = async (value) => {
|
||||
setLoading(true)
|
||||
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);
|
||||
await router.push('/login')
|
||||
if (decoded && decoded.user && decoded.user.accessCode) {
|
||||
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)
|
||||
notify('success', 'Please check your email for verification link')
|
||||
} catch (error) {
|
||||
setLoading(false)
|
||||
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 (
|
||||
<>
|
||||
<div className="bg-[#121212] min-h-screen">
|
||||
<Head>
|
||||
<title>{getPageTitle('Login')}</title>
|
||||
<title>{getPageTitle('Register')}</title>
|
||||
</Head>
|
||||
|
||||
<SectionFullScreen bg='violet'>
|
||||
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: '',
|
||||
password: '',
|
||||
confirm: ''
|
||||
}}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<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>
|
||||
<SectionFullScreen bg='dark'>
|
||||
<div className="flex flex-col items-center justify-center w-full px-4">
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex items-center justify-center space-x-2 mb-2">
|
||||
<BaseIcon path={mdiMusic} size={40} className="text-[#00E5FF]" />
|
||||
<h2 className="text-4xl font-black text-white tracking-tighter uppercase italic">AI Music Studio</h2>
|
||||
</div>
|
||||
<p className="text-gray-400 font-bold">JOIN THE CREATIVE REVOLUTION</p>
|
||||
</div>
|
||||
|
||||
<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'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
|
||||
type='submit'
|
||||
label={loading ? 'Loading...' : 'Register' }
|
||||
color='info'
|
||||
/>
|
||||
<BaseButton
|
||||
href={'/login'}
|
||||
label={'Login'}
|
||||
color='info'
|
||||
href="/login"
|
||||
label="ENTER STUDIO NOW"
|
||||
color="info"
|
||||
className="w-full py-4 font-black rounded-xl text-xl shadow-[0_0_20px_rgba(0,229,255,0.4)]"
|
||||
/>
|
||||
<Link href="/register" onClick={() => setAccessCode(null)} className="text-gray-500 hover:text-white font-bold transition-colors">
|
||||
Create another account
|
||||
</Link>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
</Formik>
|
||||
</CardBox>
|
||||
</CardBox>
|
||||
)}
|
||||
</div>
|
||||
</SectionFullScreen>
|
||||
<ToastContainer />
|
||||
</>
|
||||
<ToastContainer theme="dark" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
272
frontend/src/pages/studio.tsx
Normal file
272
frontend/src/pages/studio.tsx
Normal 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;
|
||||
@ -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(
|
||||
'auth/passwordReset',
|
||||
async (value: Record<string, string>, { rejectWithValue }) => {
|
||||
@ -79,10 +109,7 @@ export const authSlice = createSlice({
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(loginUser.pending, (state) => {
|
||||
state.isFetching = true;
|
||||
});
|
||||
builder.addCase(loginUser.fulfilled, (state, action) => {
|
||||
const handleAuthFulfilled = (state, action) => {
|
||||
const token = action.payload;
|
||||
const user = jwt.decode(token);
|
||||
|
||||
@ -91,12 +118,36 @@ export const authSlice = createSlice({
|
||||
localStorage.setItem('token', token);
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
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) => {
|
||||
state.errorMessage = String(action.payload) || 'Something went wrong. Try again';
|
||||
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, () => {
|
||||
console.log('Pending findMe');
|
||||
});
|
||||
@ -121,4 +172,4 @@ export const authSlice = createSlice({
|
||||
// Action creators are generated for each case reducer function
|
||||
export const { logoutUser } = authSlice.actions;
|
||||
|
||||
export default authSlice.reducer;
|
||||
export default authSlice.reducer;
|
||||
Loading…
x
Reference in New Issue
Block a user