+
+
+
+
+
+
+
-
-
-
+
+ setIsCreateModalOpen(false)}
+ onUpdate={(newScene) => handleCreateScene(newScene)}
+ />
+
+
+
{activeScene ? : null}
- {[1, 2, 3, 4, 5].map((step) => (
- s.step === step).length}
- >
- {state.scenes
- .filter((scene) => scene.step === step)
- .map((scene) => (
-
- ))}
-
- ))}
+ {[1, 2, 3, 4, 5].map((step) => {
+ const stepScenes = scenesByStep[step] || [];
+ return (
+
+ {stepScenes
+ .sort((a, b) => a.order - b.order)
+ .map((scene) => (
+
+ ))}
+
+ );
+ })}
diff --git a/src/reducers/scenes.ts b/src/reducers/scenes.ts
index d0a2cc7..b41fd38 100644
--- a/src/reducers/scenes.ts
+++ b/src/reducers/scenes.ts
@@ -1,63 +1,81 @@
-type Scene = {
- id: string;
- title: string;
- description: string;
- step: number;
- columnId: string;
- episode: string;
- recordDate: string;
- recordLocation: string;
-};
+import { arrayMove } from '@dnd-kit/sortable';
-type State = {
- scenes: Scene[];
- loading: boolean;
- error: string | null;
-};
+import { type Scene, type SceneAction, type SceneState } from '../types/scene';
-const initialSceneState: State = {
+const initialSceneState: SceneState = {
scenes: [],
loading: false,
error: null,
};
-type Action =
- | { type: 'SET_SCENES'; payload: Scene[] }
- | { type: 'MOVE_SCENE'; payload: { id: string; toStep: number } }
- | { type: 'SET_LOADING'; payload: boolean }
- | { type: 'SET_ERROR'; payload: string | null }
- | { type: 'UPDATE_SCENE'; payload: Scene };
-
-const sceneReducer = (state: State, action: Action): State => {
+const sceneReducer = (state: SceneState, action: SceneAction): SceneState => {
switch (action.type) {
- case 'SET_SCENES':
- return { ...state, scenes: action.payload, error: null };
-
- case 'MOVE_SCENE':
- return {
- ...state,
- scenes: state.scenes.map((scene) =>
- scene.id === action.payload.id ? { ...scene, step: action.payload.toStep } : scene,
- ),
- };
-
- case 'UPDATE_SCENE':
- return {
- ...state,
- scenes: state.scenes.map((scene) =>
- scene.id === action.payload.id ? { ...scene, ...action.payload } : scene,
- ),
- };
-
- case 'SET_LOADING':
- return { ...state, loading: action.payload };
-
- case 'SET_ERROR':
- return { ...state, error: action.payload };
-
- default:
+ case 'SET_SCENES':
+ return { ...state, scenes: action.payload, error: null };
+
+ case 'MOVE_SCENE': {
+ const { id, toStep } = action.payload;
+ return {
+ ...state,
+ scenes: state.scenes.map((scene) => (scene.id === id ? { ...scene, step: toStep } : scene)),
+ };
+ }
+
+ case 'UPDATE_SCENE': {
+ const { id } = action.payload;
+ return {
+ ...state,
+ scenes: state.scenes.map((scene) =>
+ scene.id === id ? { ...scene, ...action.payload } : scene,
+ ),
+ };
+ }
+
+ case 'CREATE_SCENE': {
+ return {
+ ...state,
+ scenes: [...state.scenes, action.payload],
+ };
+ }
+
+ case 'REORDER_SCENES': {
+ const { step, activeId, overId } = action.payload;
+
+ const scenesInStep = state.scenes
+ .filter((s) => s.step === step)
+ .sort((a, b) => a.order - b.order);
+
+ const activeIndex = scenesInStep.findIndex((s) => s.id === activeId);
+ const overIndex = scenesInStep.findIndex((s) => s.id === overId);
+
+ if (activeIndex === -1 || overIndex === -1 || activeIndex === overIndex) {
return state;
+ }
+
+ const reorderedScenesInStep = arrayMove(scenesInStep, activeIndex, overIndex);
+ const updatedScenesInStep = reorderedScenesInStep.map((scene, index) => ({
+ ...scene,
+ order: index,
+ }));
+
+ const newScenes = state.scenes.map((scene) => {
+ if (scene.step !== step) return scene;
+ const updatedScene = updatedScenesInStep.find((s) => s.id === scene.id);
+ return updatedScene || scene;
+ });
+
+ return { ...state, scenes: newScenes };
+ }
+
+ case 'SET_LOADING':
+ return { ...state, loading: action.payload };
+
+ case 'SET_ERROR':
+ return { ...state, error: action.payload };
+
+ default:
+ return state;
}
};
-export { initialSceneState, sceneReducer, type Scene };
+export { type Scene, sceneReducer, initialSceneState };
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 8ce80b7..d1783a8 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -1,22 +1,27 @@
-import {RouterProvider, createBrowserRouter} from "react-router-dom"
+import { RouterProvider, createBrowserRouter } from 'react-router-dom';
-import {Layout} from "../components/layout"
-import Studio from "../pages/Studio"
+import { Layout } from '../components/templates';
+import Studio from '../pages/Studio';
+import { Actors } from '../pages/Actors';
const router = createBrowserRouter([
{
element:
,
children: [
{
- path: "/",
- element:
- }
- ]
- }
-])
+ path: '/',
+ element:
,
+ },
+ {
+ path: '/actors',
+ element:
,
+ },
+ ],
+ },
+]);
const Routes = () => {
- return
-}
+ return
;
+};
-export default Routes
+export default Routes;
diff --git a/src/services/api.ts b/src/services/api.ts
new file mode 100644
index 0000000..4a445be
--- /dev/null
+++ b/src/services/api.ts
@@ -0,0 +1,133 @@
+import { type Scene } from '../types/scene';
+import { AppError, errorMessages, handleError } from './errorHandler';
+
+const API_URL = import.meta.env.VITE_API_URL;
+
+async function handleResponse
(response: Response): Promise {
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({}));
+ throw new AppError(
+ error.message || errorMessages.SERVER_ERROR,
+ 'API_ERROR',
+ response.status,
+ error,
+ );
+ }
+ return response.json();
+}
+
+export const api = {
+ scenes: {
+ getAll: async (): Promise => {
+ try {
+ const response = await fetch(`${API_URL}/scenes`);
+ return handleResponse(response);
+ } catch (error) {
+ throw handleError(error);
+ }
+ },
+
+ getById: async (id: string): Promise => {
+ try {
+ const response = await fetch(`${API_URL}/scenes/${id}`);
+ return handleResponse(response);
+ } catch (error) {
+ throw handleError(error);
+ }
+ },
+
+ create: async (scene: Scene): Promise => {
+ try {
+ const response = await fetch(`${API_URL}/scenes`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(scene),
+ });
+ return handleResponse(response);
+ } catch (error) {
+ throw handleError(error);
+ }
+ },
+
+ update: async (scene: Scene): Promise => {
+ try {
+ const response = await fetch(`${API_URL}/scenes/${scene.id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(scene),
+ });
+ return handleResponse(response);
+ } catch (error) {
+ throw handleError(error);
+ }
+ },
+
+ delete: async (id: string): Promise => {
+ try {
+ const response = await fetch(`${API_URL}/scenes/${id}`, {
+ method: 'DELETE',
+ });
+ await handleResponse(response);
+ } catch (error) {
+ throw handleError(error);
+ }
+ },
+ },
+
+ actors: {
+ getAll: async () => {
+ try {
+ const response = await fetch(`${API_URL}/actors`);
+ return handleResponse(response);
+ } catch (error) {
+ throw handleError(error);
+ }
+ },
+
+ getById: async (id: string) => {
+ try {
+ const response = await fetch(`${API_URL}/actors/${id}`);
+ return handleResponse(response);
+ } catch (error) {
+ throw handleError(error);
+ }
+ },
+
+ create: async (actor: { name: string; bio: string }) => {
+ try {
+ const response = await fetch(`${API_URL}/actors`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(actor),
+ });
+ return handleResponse(response);
+ } catch (error) {
+ throw handleError(error);
+ }
+ },
+
+ update: async (id: string, actor: { name: string; bio: string }) => {
+ try {
+ const response = await fetch(`${API_URL}/actors/${id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(actor),
+ });
+ return handleResponse(response);
+ } catch (error) {
+ throw handleError(error);
+ }
+ },
+
+ delete: async (id: string) => {
+ try {
+ const response = await fetch(`${API_URL}/actors/${id}`, {
+ method: 'DELETE',
+ });
+ await handleResponse(response);
+ } catch (error) {
+ throw handleError(error);
+ }
+ },
+ },
+};
diff --git a/src/services/errorHandler.ts b/src/services/errorHandler.ts
new file mode 100644
index 0000000..dd655a9
--- /dev/null
+++ b/src/services/errorHandler.ts
@@ -0,0 +1,57 @@
+import { toast } from 'react-hot-toast';
+
+export class AppError extends Error {
+ constructor(
+ message: string,
+ public code: string,
+ public status?: number,
+ public details?: unknown,
+ ) {
+ super(message);
+ this.name = 'AppError';
+ }
+}
+
+export const errorMessages = {
+ NETWORK_ERROR: 'Erro de conexão. Verifique sua internet.',
+ SERVER_ERROR: 'Erro no servidor. Tente novamente mais tarde.',
+ NOT_FOUND: 'Recurso não encontrado.',
+ UNAUTHORIZED: 'Você não tem permissão para realizar esta ação.',
+ VALIDATION_ERROR: 'Dados inválidos. Verifique os campos.',
+ UNKNOWN_ERROR: 'Ocorreu um erro inesperado.',
+} as const;
+
+export function handleError(error: unknown): AppError {
+ if (error instanceof AppError) {
+ return error;
+ }
+
+ if (error instanceof Error) {
+ if (error.message.includes('Network Error')) {
+ return new AppError(errorMessages.NETWORK_ERROR, 'NETWORK_ERROR');
+ }
+
+ if (error.message.includes('404')) {
+ return new AppError(errorMessages.NOT_FOUND, 'NOT_FOUND', 404);
+ }
+
+ if (error.message.includes('401') || error.message.includes('403')) {
+ return new AppError(errorMessages.UNAUTHORIZED, 'UNAUTHORIZED', 401);
+ }
+
+ if (error.message.includes('validation')) {
+ return new AppError(errorMessages.VALIDATION_ERROR, 'VALIDATION_ERROR', 400);
+ }
+ }
+
+ return new AppError(errorMessages.UNKNOWN_ERROR, 'UNKNOWN_ERROR');
+}
+
+export function showErrorToast(error: unknown): void {
+ const appError = handleError(error);
+ toast.error(appError.message);
+}
+
+export function isAppError(error: unknown): error is AppError {
+ return error instanceof AppError;
+}
diff --git a/src/styles/global.css b/src/styles/global.css
index fed2832..288fe3c 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -10,4 +10,18 @@
body {
@apply bg-background text-foreground;
}
+
+ *::-webkit-scrollbar {
+ width: 5px !important;
+ height: 5px !important;
+ }
+
+ *::-webkit-scrollbar-track {
+ background: transparent !important;
+ }
+
+ *::-webkit-scrollbar-thumb {
+ border-radius: 10px !important;
+ background: #c7c6d0 !important;
+ }
}
diff --git a/src/types/actor.ts b/src/types/actor.ts
new file mode 100644
index 0000000..7ca7ad2
--- /dev/null
+++ b/src/types/actor.ts
@@ -0,0 +1,10 @@
+interface Actor {
+ id: string;
+ name: string;
+ bio: string;
+ scenes: string[];
+}
+
+type ActorFormData = Omit;
+
+export type { Actor, ActorFormData };
diff --git a/src/types/scene.ts b/src/types/scene.ts
new file mode 100644
index 0000000..a08eeab
--- /dev/null
+++ b/src/types/scene.ts
@@ -0,0 +1,35 @@
+import { z } from 'zod';
+
+const sceneSchema = z.object({
+ id: z.string(),
+ title: z.string().min(1, 'Title is required'),
+ description: z.string().min(1, 'Description is required'),
+ step: z.number().min(1),
+ episode: z.string().min(1, 'Episode is required'),
+ recordDate: z.string().refine((val) => !isNaN(new Date(val).getTime())),
+ recordLocation: z.string().min(1, 'Location is required'),
+ columnId: z.string(),
+ order: z.number(),
+ actors: z.array(z.string()),
+});
+
+type Scene = z.infer;
+
+interface SceneState {
+ scenes: Scene[];
+ loading: boolean;
+ error: string | null;
+}
+
+type SceneAction =
+ | { type: 'SET_SCENES'; payload: Scene[] }
+ | { type: 'MOVE_SCENE'; payload: { id: string; toStep: number } }
+ | { type: 'SET_LOADING'; payload: boolean }
+ | { type: 'SET_ERROR'; payload: string | null }
+ | { type: 'UPDATE_SCENE'; payload: Scene }
+ | { type: 'REORDER_SCENES'; payload: { step: number; activeId: string; overId: string } }
+ | { type: 'CREATE_SCENE'; payload: Scene };
+
+export { sceneSchema };
+
+export type { Scene, SceneState, SceneAction };
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
new file mode 100644
index 0000000..b16ecf6
--- /dev/null
+++ b/src/utils/utils.ts
@@ -0,0 +1,7 @@
+export const STEPS: Record = {
+ 1: 'Roteirizado',
+ 2: 'Em pré-produção',
+ 3: 'Em gravação',
+ 4: 'Em pós-produção',
+ 5: 'Finalizado',
+};
diff --git a/vite.config.ts b/vite.config.ts
index ea298da..46ac457 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,6 +1,6 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
-import tailwindcss from '@tailwindcss/vite'
+import tailwindcss from '@tailwindcss/vite';
+import react from '@vitejs/plugin-react';
+import { defineConfig } from 'vite';
// https://vite.dev/config/
export default defineConfig({
@@ -17,10 +17,10 @@ export default defineConfig({
rollupOptions: {
output: {
manualChunks: {
- 'vendor': ['react', 'react-dom'],
- 'tailwind': ['tailwindcss', 'daisyui'],
- }
- }
- }
- }
-})
+ vendor: ['react', 'react-dom'],
+ tailwind: ['tailwindcss', 'daisyui'],
+ },
+ },
+ },
+ },
+});