diff --git a/.env.local b/.env.local new file mode 100644 index 00000000..4c041e22 --- /dev/null +++ b/.env.local @@ -0,0 +1,4 @@ + +#*** @Stripe *# +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51LBfSOLjx992qWAYRekYCPRpgZ3xa6POoteqC7Tp9555GXDGEcm67xHgOQt20N9vTrXaerlfKQdTcxZreijcvwI300GL0SnTOR +STRIPE_SECRET=sk_test_51LBfSOLjx992qWAYx8s062kh2dqbNsjbH6Po0xL93bZjeOKZyFCtxNzQ0gyFNCSOWYLSi45miRAZS6olA3epsayv00C9LKNqPB \ No newline at end of file diff --git a/env.test b/.env.test similarity index 100% rename from env.test rename to .env.test diff --git a/.eslintrc.json b/.eslintrc.json index 575485d3..5d6552df 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,9 +3,7 @@ "overrides": [ { "files": ["**/*.test.ts", "**/*.test.tsx"], - "env": { - "jest": true - } + "env": {} } ], "rules": { diff --git a/.gitignore b/.gitignore index 9aa96fea..4892b21d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules/ # Next.js .*.env +.*.env.local .next/ .swc/ .vercel diff --git a/next.config.js b/next.config.js index 53c2f889..305c8977 100644 --- a/next.config.js +++ b/next.config.js @@ -1,37 +1,36 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - experimental: { serverActions: true }, async headers() { return [ { - source: "/(.*)", // Apply these headers to all routes + source: '/(.*)', // Apply these headers to all routes headers: [ /* { key: "Content-Security-Policy", value: "default-src 'self'; script-src 'self'; style-src 'self'", }, */ { - key: "X-Content-Type-Options", - value: "nosniff", + key: 'X-Content-Type-Options', + value: 'nosniff', }, { - key: "X-Frame-Options", - value: "DENY", + key: 'X-Frame-Options', + value: 'DENY', }, { - key: "X-XSS-Protection", - value: "1; mode=block", + key: 'X-XSS-Protection', + value: '1; mode=block', }, { - key: "Strict-Transport-Security", - value: "max-age=31536000; includeSubDomains; preload", + key: 'Strict-Transport-Security', + value: 'max-age=31536000; includeSubDomains; preload', }, { - key: "Referrer-Policy", - value: "no-referrer", + key: 'Referrer-Policy', + value: 'no-referrer', }, { - key: "Feature-Policy", + key: 'Feature-Policy', value: "geolocation 'none'; camera 'none'; microphone 'none'", }, /* { @@ -39,8 +38,8 @@ const nextConfig = { value: "https://trusted-domain.com", }, */ { - key: "X-Content-Type-Options", - value: "nosniff", + key: 'X-Content-Type-Options', + value: 'nosniff', }, /* { key: "Expect-CT", diff --git a/package-lock.json b/package-lock.json index 2dd9958f..be00bb23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@nextui-org/react": "^2.1.13", "@react-hook/window-size": "^3.1.1", "@spotify/basic-pitch": "^1.0.1", + "@stripe/stripe-js": "^2.1.10", "@tensorflow/tfjs": "^4.12.0", "@types/node": "^20.8.9", "@types/react": "18.2.33", @@ -35,6 +36,8 @@ "react-dom": "^18.2.0", "react-useportal": "^1.0.18", "reactflow": "^11.9.4", + "server-only": "^0.0.1", + "stripe": "^14.2.0", "swr": "^2.2.4", "tailwindcss": "3.3.5", "tonal": "^5.0.0", @@ -2860,6 +2863,11 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/@stripe/stripe-js": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-2.1.10.tgz", + "integrity": "sha512-h79zhwvxAJVAvtVjtMoz++DtwI7GdcEItmTC0P2gciZoFUeAO6XX9DL+UXm9uADiEaUvTKqrExYwtBTlMYAaPA==" + }, "node_modules/@swc/core": { "version": "1.3.93", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.93.tgz", @@ -8790,6 +8798,11 @@ "node": ">=10" } }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==" + }, "node_modules/set-function-name": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", @@ -9040,6 +9053,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-14.2.0.tgz", + "integrity": "sha512-lMjDOyJbt+NVSDvkTathSP7uEV35l7oU8UrhBJrYD8lUi43BWujq8E9QHd3o9D2KPBR1Cze5DCw5s1btnLfdMA==", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/stripe/node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/styled-jsx": { "version": "5.1.1", "license": "MIT", diff --git a/package.json b/package.json index 03ebe7b6..1295592b 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@nextui-org/react": "^2.1.13", "@react-hook/window-size": "^3.1.1", "@spotify/basic-pitch": "^1.0.1", + "@stripe/stripe-js": "^2.1.10", "@tensorflow/tfjs": "^4.12.0", "@types/node": "^20.8.9", "@types/react": "18.2.33", @@ -49,6 +50,8 @@ "react-dom": "^18.2.0", "react-useportal": "^1.0.18", "reactflow": "^11.9.4", + "server-only": "^0.0.1", + "stripe": "^14.2.0", "swr": "^2.2.4", "tailwindcss": "3.3.5", "tonal": "^5.0.0", diff --git a/src/app/(routes)/layout.tsx b/src/app/(routes)/layout.tsx deleted file mode 100644 index e7b6aec3..00000000 --- a/src/app/(routes)/layout.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { PrimeReactProvider } from "primereact/api"; - -import { type Metadata } from "next"; -import { type ReactNode } from "react"; - -import "./globals.css"; -import { Inter } from "next/font/google"; -const inter = Inter({ subsets: ["latin"] }); - -export const metadata: Metadata = { - description: "Digital Audio Workstation", - title: "DAW", -}; - -interface IRootLayoutProps { - children: ReactNode; -} - -export default function RootLayout({ children }: IRootLayoutProps) { - return ( - - - {children} - - - ); -} diff --git a/src/app/(routes)/page.tsx b/src/app/(routes)/page.tsx deleted file mode 100644 index f467a7eb..00000000 --- a/src/app/(routes)/page.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client'; -import * as Tone from 'tone'; -import { useState } from 'react'; -import { Loader } from 'lucide-react'; -import { App } from 'app/_components'; -import { Button, Dialog } from '@/pfui'; -import t from '@/core/i18n'; - -import { EButtonType, ESize, EVariant } from '@/pfui/constants'; - -export default function Home() { - const [toneReady, setToneReady] = useState(false); - const [isLoading, setIsLoading] = useState(false); - - function Start() { - const onClick = () => { - setIsLoading(true); - Tone.start().then(() => setToneReady(true)); - }; - - return ( - -

{t('allowSound')}

- {isLoading ? ( - - ) : ( -
- ); - } - - return toneReady ? : ; -} diff --git a/src/app/_common/constants.tsx b/src/app/_common/constants.tsx index 47dbe902..2d033124 100644 --- a/src/app/_common/constants.tsx +++ b/src/app/_common/constants.tsx @@ -7,6 +7,7 @@ import { ETrackType, type ITrack } from 'app/_common/types/track.types'; import type { IChannel } from './types/channel.types'; import { EInstrument } from './types/instrument.types'; import { Note } from 'tone/build/esm/core/type/NoteUnits'; +import { Subdivision } from 'tone/build/esm/core/type/Units'; // ---------- General /*** @constants */ @@ -43,30 +44,21 @@ export const PROGRESSION = [ // Montgomery Ward bridge 'I IV ii V', ]; -export const ROMAN_NUMS = ['i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii']; - -// ---------- Utils -/*** @utils */ export const getMaxNotes = (notes: Note[][]) => Math.max(notes.reduce((a, b) => (a > b.length ? a : b.length), 0)); -export const getMeasureTime = (n: number) => Tone.Time(`${n}m`); -export const getMeasureSeconds = (n: number) => getMeasureTime(n).toSeconds(); -export const getNotationTime = (n: number) => Tone.Time(`${n}n`); -export const getNotationSeconds = (n: number) => getNotationTime(n).toSeconds(); -export const isRomanNum = (s: string) => ROMAN_NUMS.includes(_.toLower(s)); // ---------- Project -const DEFAULT_ACTIVE_TRACK_ID = 'track-bd'; +const DEFAULT_ACTIVE_TRACK_ID = 'audio-halloween'; const DEFAULT_BPM = 120; const DEFAULT_CLEF = 'D'; const DEFAULT_SCALE = 'minor'; const DEFAULT_MEASURE_COUNT = 2; const DEFAULT_NAME = t('untitled'); -const DEFAULT_OFFSET_LEFT = 184; +const DEFAULT_OFFSET_LEFT = 179; const DEFAULT_POSITION = '0:0:0'; const DEFAULT_QUANTIZATION = 16; const DEFAULT_SWING = 0; -const DEFAULT_SWING_SUBDIVISION = '8n'; +const DEFAULT_SWING_SUBDIVISION: Subdivision = '8n'; const DEFAULT_STATES = { tabTopActive: 0, tabBtmActive: 0, diff --git a/src/app/_common/styles.ts b/src/app/_common/styles.ts index 34d1fb89..d75c6c6d 100644 --- a/src/app/_common/styles.ts +++ b/src/app/_common/styles.ts @@ -1,7 +1,7 @@ const styles = { button: { navbar: - 'mr-1 w-11 h-11 p-2 bg-[#333] rounded-xs cursor-pointer hover:bg-[#222]', + 'mr-1 w-10 h-10 p-2 bg-[#333] rounded-xs cursor-pointer hover:bg-[#222]', }, dialog: 'flex flex-col shadow-2xl p-4 w-22 absolute top-0 left-0 right-0 bottom-0', @@ -10,43 +10,52 @@ const styles = { h2: 'text-2xl font-bold', h3: 'text-xl font-semibold', }, - icon: 'w-[16px] h-[16px] mr-1', - main: 'relative h-full flex flex-1 justify-between', + main: 'flex flex-col flex-1', // h-full flex flex-1 justify-between', /*** @components */ - arranger: { main: 'relative' }, + arranger: { + main: 'flex flex-1 relative bg-zinc-900/80', + inner: 'flex-1', + }, avatar: { - main: 'flex items-center justify-center bg-[#222] text-white cursor-pointer w-12 h-12 hover:rotate-180', - bordered: 'border border-gray-600', + main: 'flex items-center justify-center bg-zinc-900 text-white cursor-pointer w-12 h-12 hover:rotate-180', + bordered: 'border border-zinc-600', rounded: 'rounded-full', - icon: 'w-8 h-8', }, browser: { - a: 'p-2 border border-gray-600', + a: 'p-2 border border-zinc-600', main: 'w-4 h-4 mr-2', fileStyle: 'fill-green-200', folderStyle: 'fill-blue-200', liStyle: 'flex', }, + icon: { + sm: 'w-[16px] h-[16px]', + md: 'w-[24px] h-[24px]', + lg: 'w-[32px] h-[32px]', + }, locator: 'bg-black w-[1px] absolute top-0 bottom-0', + menu: { + main: 'absolute bg-black border border-zinc-700', + nav: 'flex flex-col items-center', + a: 'flex text-white py-2 px-4 gap-2 border-b border-zinc-600', + }, mixer: { - main: 'bg-[#3e4140] flex w-full text-white pl-[184px]', + main: 'bg-zinc-950 flex w-full text-zinc-200 pl-[179px]', inner: 'flex w-full items-center', - meter: 'w-full bg-[#333]', + meter: 'w-full bg-zinc-900', + meterActive: 'bg-zinc-900', track: { - main: 'pt-6 bg-[#3e4140] border border-r-[#555] border-r-[2px] justify-center text-xs items-center content-end relative', - channel: 'justify-end bg-green-700', + main: 'max-w-[110px] pt-6 mr-2 justify-center text-xs items-center content-end relative', inner: 'flex px-4 items-center gap-1 py-2 border-b border-b-[#555]', - master: - 'bg-[#3e4140] border border-r-[#555] border-r-[2px] h-full flex flex-col justify-end', - active: 'font-bold', + master: 'h-full', + active: 'bg-zinc-800 text-zinc-300', inactive: '', }, }, navbar: { - ui: 'p-4 flex flex-row w-full bg-black text-white items-center justify-between', + ui: 'p-4 flex flex-row w-full bg-zinc-900 text-white items-center justify-between', uiInner: 'flex px-4 py-2', - icon: 'w-8 h-8 mr-2', }, notes: { main: 'flex flex-1 justify-center items-center mr-1 text-black text-center cursor-pointer text-[0.5rem]', @@ -54,22 +63,24 @@ const styles = { bgActive: 'bg-blue-900 text-white', }, time: { - main: 'flex flex-1 items-center px-1 py-2 border-l border-r-gray-200', + main: 'flex w-full text-zinc-500 text-xs', + col1: 'w-[179px|', + inner: + 'bg-zinc-950 text-zinc-300 flex flex-1 items-center px-1 py-2 border-r border-r-zinc-600', }, track: { - audio: { main: 'w-full flex flex-col justify-center' }, - time: 'flex w-full bg-white text-gray-500 text-xs', - active: 'font-bold text-black', + audio: 'w-full flex flex-col justify-center', + active: 'font-bold bg-blue-200 text-black', col1: { - active: 'bg-[blue]', - main: 'flex justify-between items-center px-4 pl-8 bg-white', + active: 'bg-blue-400', + main: 'flex justify-between items-center px-4 pl-8 bg-blue-100 text-zinc-600', name: 'whitespace-nowrap w-28 overflow-x-hidden text-ellipsis', }, col2: { - main: 'relative flex w-full', + main: 'bg-blue-100/50 relative flex w-full', }, - icon: 'fill-white w-6 h-6', // TODO not white - row: 'relative flex w-full h-10 text-black mb-[1px] first:text-gray-400 first:mb-0 text-xs', + icon: 'fill-white w-[16px] h-[16px]', // TODO not white + li: 'relative flex w-full h-10 text-black mb-1 first:text-zinc-400 text-xs', }, transport: { main: 'flex px-4', @@ -81,7 +92,7 @@ const styles = { input: 'ml-1 bg-transparent', item: 'flex items-center', label: 'text-cyan-300', - position: 'text-[#fff] text-lg', + position: 'text-white text-lg', }, }, }; diff --git a/src/app/_common/types/arranger.types.ts b/src/app/_common/types/arranger.types.ts index efaef4d8..5058e983 100644 --- a/src/app/_common/types/arranger.types.ts +++ b/src/app/_common/types/arranger.types.ts @@ -1,5 +1,12 @@ +import type { ReactNode } from 'react'; +import { ITrack } from './track.types'; +import { IProject } from './project.types'; + interface IArranger { - className?: string; + readonly project: IProject; + readonly tracks: ITrack[]; + readonly children: ReactNode; + readonly className?: string; } export type { IArranger }; diff --git a/src/app/_common/types/instrument.types.ts b/src/app/_common/types/instrument.types.ts index d3637e08..67167f3b 100644 --- a/src/app/_common/types/instrument.types.ts +++ b/src/app/_common/types/instrument.types.ts @@ -13,6 +13,14 @@ import { } from 'tone'; import { RecursivePartial as TRecursivePartial } from 'tone/build/esm/core/util/Interface'; +export interface IInstrumentPortal { + InstrumentPortal: FC<{}>; + openInstrument: () => void; + toggleInstrument: () => void; + closeInstrument: () => void; + isOpen: boolean; +} + export enum EInstrument { AmSynth = 'AmSynth', FmSynth = 'FmSynth', diff --git a/src/app/_common/types/mixer.types.ts b/src/app/_common/types/mixer.types.ts index ca41426d..8c9389a8 100644 --- a/src/app/_common/types/mixer.types.ts +++ b/src/app/_common/types/mixer.types.ts @@ -1,4 +1,12 @@ +import { IChannel } from './channel.types'; +import { ITrack } from './track.types'; +import { UniqueIdentifier } from './utility.types'; + interface IMixer { + activeTrackId?: UniqueIdentifier; + channels: IChannel[]; + tracks: ITrack[]; + className?: string; openInstrument: () => void; } diff --git a/src/app/_common/types/project.types.ts b/src/app/_common/types/project.types.ts index d93b6700..f3616724 100644 --- a/src/app/_common/types/project.types.ts +++ b/src/app/_common/types/project.types.ts @@ -2,7 +2,7 @@ import type { Subdivision } from 'tone/build/esm/core/type/Units'; import type { UniqueIdentifier } from '@dnd-kit/core'; import type { ITrack } from 'app/_common/types/track.types'; import type { IChannel } from './channel.types'; -import { Dispatch, SetStateAction } from 'react'; +import { Dispatch, ReactNode, SetStateAction } from 'react'; export enum ETransportState { Paused = 'paused', @@ -12,25 +12,25 @@ export enum ETransportState { export enum EPortal { Instruments = 'portal-instruments', } - +export interface ISong { + children: ReactNode; + className?: string; + grow?: boolean; +} export interface IStartDialog { toneReady: boolean; setToneReady: Dispatch>; } -export interface IProjectContext { +export interface IProject { activeTrackId: UniqueIdentifier; bpm: number; clef: string; scale: string; // TODO enum - // - channels: IChannel[]; - tracks: ITrack[]; - // measureCount: number; name: string; + offsetLeft: number; position: string; quantization: number; - states: Record; swing: number; swingSubdivision: Subdivision; } diff --git a/src/app/_common/types/track.types.ts b/src/app/_common/types/track.types.ts index 7d9ed0f6..fbe2e762 100644 --- a/src/app/_common/types/track.types.ts +++ b/src/app/_common/types/track.types.ts @@ -31,4 +31,5 @@ export interface ITrack { routing: ITrackRouting; type: ETrackType; className?: string; + isActive?: boolean; } diff --git a/src/app/_components/App.tsx b/src/app/_components/App.tsx index 2fbf1006..57a21725 100644 --- a/src/app/_components/App.tsx +++ b/src/app/_components/App.tsx @@ -1,56 +1,41 @@ 'use client'; import _ from 'lodash/fp'; -import { useEffect, type MouseEvent } from 'react'; -import { CogIcon, GridIcon, HopIcon, InfinityIcon } from 'lucide-react'; -import * as Tone from 'tone'; -import { - DndContext, - useSensor, - type DragEndEvent, - MouseSensor, - useSensors, -} from '@dnd-kit/core'; -import styles from 'app/_common/styles'; -import { - DEFAULT_MEASURE_COUNT, - DEFAULT_OFFSET_LEFT, - DEFAULT_QUANTIZATION, -} from 'app/_common/constants'; -// Import { PanSongParsed } from "./test/unit/PanSong.parsed"; -// import useConverter from "@/core/hooks/useConverter"; +import t from '@/core/i18n'; import { Arranger, - Browser, - Droppable, + Footer, + Instrument, + Header, Mixer, - Navbar, - PianoRoll, - Progression, - Settings, - Sheet, -} from 'app/_components'; -import { A, Adsr, GoldKnob, Grid, Menu, Nav, Tabs, TabsPanel } from '@/pfui'; -import useProjectContext from 'app/_core/hooks/api/useProjectContext'; -import useAudioInstrument from 'app/_core/hooks/audio/useAudioInstrument'; + Song, + Track, +} from '@/components'; +import { Button, ButtonGroup, Dialog, Flex, Menu } from '@/pfui'; +import { useSelector } from '@/core/hooks/useSelector'; +import { useToneJs } from '@/core/hooks/useToneJs'; -import type { Note as TNote } from 'tone/build/esm/core/type/NoteUnits'; -import t from '@/core/i18n'; -import data from '../../../cypress/fixtures/sheet'; +import { EButtonType, ESize, EVariant } from '@/pfui/constants'; +import type { IChannel } from '@/common/types/channel.types'; +import type { ITrack } from '@/common/types/track.types'; +import type { IProject } from '@/common/types/project.types'; -export function App() { - const { isOpen, InstrumentPortal, openInstrument, closeInstrument } = - useAudioInstrument(); +import $ from '@/common/styles'; +import usePortal from 'react-useportal'; - useEffect(() => { - const audioContext = new Tone.Context().resume(); - // Your audio processing code goes here - }, []); +interface IData { + channels: IChannel[]; + project: IProject; + tracks: ITrack[]; +} - // const { audioToAbc, audioToMidi } = useConverter(); - const mouseSensor = useSensor(MouseSensor); - const sensors = useSensors(mouseSensor); +export function App({ channels: _channels, project, tracks: _tracks }: IData) { + const { Portal, isOpen, open, close } = usePortal(); + const { init, toneReady } = useToneJs(); + const { selectChannels, selectTracks } = useSelector(); + // Following comments will be reused in the future + // const { audioToAbc, audioToMidi } = useConverter(); /* const convertAudioToMidi = async () => { try { @@ -61,7 +46,6 @@ export function App() { } }; */ - /* TODO useEffect(() => { (async () => { @@ -73,24 +57,14 @@ export function App() { })(); }, [audioToAbc]); */ - - const { projectContext, patchProjectContext } = useProjectContext(); - const gridCols = - (projectContext?.quantization || DEFAULT_QUANTIZATION) * - (projectContext?.measureCount || DEFAULT_MEASURE_COUNT); - const activeTrackId = projectContext?.activeTrackId; - const clef = projectContext?.clef; - const tabBtmActive = projectContext?.states.tabBtmActive; - const tabTopActive = projectContext?.states.tabTopActive; - const tracks = projectContext?.tracks || []; - + /* const tabsBottomItems = [ { children:
Mixer
, href: '#', id: 'tabs-mixer', order: 1, - panel: , + panel:
Mixer
, // , title: 'Mixer', }, { @@ -110,6 +84,8 @@ export function App() { title: 'Browser', }, ]; + */ + /* const tabsTopItems = [ { children: ( @@ -123,20 +99,29 @@ export function App() { order: 1, panel: ( <> - {/* TODO Extract as 'Backdrop' component */}
- + + {tracks.map((track, trackIndex) => { + const trk = { + className: + activeTrackId === track.id ? styles.track.active : '', + measureCount, + quantization, + ...track, + }; + return ; + })} +
-