criando projetos
This commit is contained in:
parent
fd3f93afa3
commit
23b2fa4e1a
11 changed files with 453 additions and 1 deletions
|
|
@ -0,0 +1,23 @@
|
||||||
|
package net.beetechgroup.beetask.frameworks.config;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.enterprise.inject.Produces;
|
||||||
|
import net.beetechgroup.beetask.usecase.project.create.CreateProjectUseCase;
|
||||||
|
import net.beetechgroup.beetask.usecase.project.list.ListProjectsUseCase;
|
||||||
|
import net.beetechgroup.beetask.usecase.repository.OrganizationRepository;
|
||||||
|
import net.beetechgroup.beetask.usecase.repository.ProjectRepository;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ProjectUseCaseConfig {
|
||||||
|
|
||||||
|
@Produces
|
||||||
|
public CreateProjectUseCase createProjectUseCase(ProjectRepository projectRepository,
|
||||||
|
OrganizationRepository organizationRepository) {
|
||||||
|
return new CreateProjectUseCase(projectRepository, organizationRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Produces
|
||||||
|
public ListProjectsUseCase listProjectsUseCase(ProjectRepository projectRepository) {
|
||||||
|
return new ListProjectsUseCase(projectRepository);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ public class ProjectEntityMapper {
|
||||||
ProjectEntity entity = new ProjectEntity();
|
ProjectEntity entity = new ProjectEntity();
|
||||||
entity.setId(project.getId());
|
entity.setId(project.getId());
|
||||||
entity.setName(project.getName());
|
entity.setName(project.getName());
|
||||||
|
entity.setOrganization(OrganizationEntityMapper.toEntity(project.getOrganization()));
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,6 +20,7 @@ public class ProjectEntityMapper {
|
||||||
Project project = new Project();
|
Project project = new Project();
|
||||||
project.setId(entity.getId());
|
project.setId(entity.getId());
|
||||||
project.setName(entity.getName());
|
project.setName(entity.getName());
|
||||||
|
project.setOrganization(OrganizationEntityMapper.toDomain(entity.getOrganization()));
|
||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package net.beetechgroup.beetask.frameworks.persistence.repository;
|
||||||
|
|
||||||
|
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import net.beetechgroup.beetask.entities.Project;
|
||||||
|
import net.beetechgroup.beetask.frameworks.persistence.entities.ProjectEntity;
|
||||||
|
import net.beetechgroup.beetask.frameworks.persistence.mapper.ProjectEntityMapper;
|
||||||
|
import net.beetechgroup.beetask.usecase.repository.ProjectRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ProjectRepositoryImpl implements ProjectRepository, PanacheRepository<ProjectEntity> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Project saveProject(Project project) {
|
||||||
|
ProjectEntity entity = ProjectEntityMapper.toEntity(project);
|
||||||
|
if (entity.getId() == null) {
|
||||||
|
persist(entity);
|
||||||
|
} else {
|
||||||
|
entity = getEntityManager().merge(entity);
|
||||||
|
}
|
||||||
|
return ProjectEntityMapper.toDomain(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Project> findProjectById(Long id) {
|
||||||
|
return findByIdOptional(id)
|
||||||
|
.map(ProjectEntityMapper::toDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Project> findByOrganizationId(Long organizationId) {
|
||||||
|
return find("organization.id", organizationId).stream()
|
||||||
|
.map(ProjectEntityMapper::toDomain)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package net.beetechgroup.beetask.interfaceadapters.controllers.project;
|
||||||
|
|
||||||
|
import io.quarkus.security.Authenticated;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import net.beetechgroup.beetask.entities.Project;
|
||||||
|
import net.beetechgroup.beetask.usecase.project.create.CreateProjectUseCase;
|
||||||
|
import net.beetechgroup.beetask.usecase.project.list.ListProjectsUseCase;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Path("/organizations/{orgId}/projects")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
public class ProjectController {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
CreateProjectUseCase createProjectUseCase;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ListProjectsUseCase listProjectsUseCase;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Authenticated
|
||||||
|
public List<ProjectResponse> listProjects(@PathParam("orgId") Long orgId) {
|
||||||
|
return listProjectsUseCase.execute(orgId).stream()
|
||||||
|
.map(p -> new ProjectResponse(p.getId(), p.getName()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Authenticated
|
||||||
|
public ProjectResponse createProject(@PathParam("orgId") Long orgId, CreateProjectRequest request) {
|
||||||
|
Project project = createProjectUseCase.execute(orgId, request.name());
|
||||||
|
return new ProjectResponse(project.getId(), project.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO must be a independent file
|
||||||
|
public record CreateProjectRequest(String name) {}
|
||||||
|
|
||||||
|
// TODO must be a independent file
|
||||||
|
public record ProjectResponse(Long id, String name) {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package net.beetechgroup.beetask.usecase.project.create;
|
||||||
|
|
||||||
|
import net.beetechgroup.beetask.entities.Project;
|
||||||
|
import net.beetechgroup.beetask.entities.organization.Organization;
|
||||||
|
import net.beetechgroup.beetask.usecase.repository.OrganizationRepository;
|
||||||
|
import net.beetechgroup.beetask.usecase.repository.ProjectRepository;
|
||||||
|
|
||||||
|
public class CreateProjectUseCase {
|
||||||
|
private final ProjectRepository projectRepository;
|
||||||
|
private final OrganizationRepository organizationRepository;
|
||||||
|
|
||||||
|
public CreateProjectUseCase(ProjectRepository projectRepository, OrganizationRepository organizationRepository) {
|
||||||
|
this.projectRepository = projectRepository;
|
||||||
|
this.organizationRepository = organizationRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Project execute(Long organizationId, String projectName) {
|
||||||
|
Organization organization = organizationRepository.findOrganizationById(organizationId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Organization not found"));
|
||||||
|
|
||||||
|
Project project = new Project();
|
||||||
|
project.setName(projectName);
|
||||||
|
project.setOrganization(organization);
|
||||||
|
|
||||||
|
return projectRepository.saveProject(project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package net.beetechgroup.beetask.usecase.project.list;
|
||||||
|
|
||||||
|
import net.beetechgroup.beetask.entities.Project;
|
||||||
|
import net.beetechgroup.beetask.usecase.repository.ProjectRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ListProjectsUseCase {
|
||||||
|
private final ProjectRepository projectRepository;
|
||||||
|
|
||||||
|
public ListProjectsUseCase(ProjectRepository projectRepository) {
|
||||||
|
this.projectRepository = projectRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Project> execute(Long organizationId) {
|
||||||
|
return projectRepository.findByOrganizationId(organizationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package net.beetechgroup.beetask.usecase.repository;
|
||||||
|
|
||||||
|
import net.beetechgroup.beetask.entities.Project;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface ProjectRepository {
|
||||||
|
Project saveProject(Project project);
|
||||||
|
Optional<Project> findProjectById(Long id);
|
||||||
|
List<Project> findByOrganizationId(Long organizationId);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Layout, ListTodo, PlusCircle, Columns, LogOut, Settings, Building2, Clock, ChevronDown, Shield } from 'lucide-react'
|
import { Layout, ListTodo, PlusCircle, Columns, LogOut, Settings, Building2, Clock, ChevronDown, Shield, FolderKanban } from 'lucide-react'
|
||||||
import { NavLink, Outlet, Navigate } from 'react-router-dom'
|
import { NavLink, Outlet, Navigate } from 'react-router-dom'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import { OnboardingModal } from './OnboardingModal'
|
import { OnboardingModal } from './OnboardingModal'
|
||||||
|
|
@ -147,6 +147,10 @@ export function AppShell() {
|
||||||
<Columns size={18} />
|
<Columns size={18} />
|
||||||
Quadro
|
Quadro
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink to="/projects" className={navLinkClassName}>
|
||||||
|
<FolderKanban size={18} />
|
||||||
|
Projetos
|
||||||
|
</NavLink>
|
||||||
<NavLink to="/requests" className={navLinkClassName}>
|
<NavLink to="/requests" className={navLinkClassName}>
|
||||||
<Clock size={18} />
|
<Clock size={18} />
|
||||||
Solicitações
|
Solicitações
|
||||||
|
|
@ -215,6 +219,10 @@ export function AppShell() {
|
||||||
<Columns size={18} />
|
<Columns size={18} />
|
||||||
Quadro
|
Quadro
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink to="/projects" className={navLinkClassName}>
|
||||||
|
<FolderKanban size={18} />
|
||||||
|
Projetos
|
||||||
|
</NavLink>
|
||||||
<NavLink to="/requests" className={navLinkClassName}>
|
<NavLink to="/requests" className={navLinkClassName}>
|
||||||
<Clock size={18} />
|
<Clock size={18} />
|
||||||
Pedidos
|
Pedidos
|
||||||
|
|
|
||||||
249
frontend/src/pages/ProjectsPage.tsx
Normal file
249
frontend/src/pages/ProjectsPage.tsx
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import {
|
||||||
|
FolderKanban,
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
Building2,
|
||||||
|
Layout,
|
||||||
|
ArrowRight,
|
||||||
|
Loader2,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
|
import { projectService, type Project } from "../services/projectService";
|
||||||
|
|
||||||
|
export const ProjectsPage: React.FC = () => {
|
||||||
|
const { activeOrg } = useAuth();
|
||||||
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
const [newProjectName, setNewProjectName] = useState("");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchProjects = async () => {
|
||||||
|
if (!activeOrg) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await projectService.listByOrganization(activeOrg.id);
|
||||||
|
setProjects(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch projects", err);
|
||||||
|
setError("Não foi possível carregar os projetos.");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProjects();
|
||||||
|
}, [activeOrg]);
|
||||||
|
|
||||||
|
const handleCreateProject = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!activeOrg || !newProjectName.trim()) return;
|
||||||
|
|
||||||
|
setIsCreating(true);
|
||||||
|
try {
|
||||||
|
const newProject = await projectService.create(
|
||||||
|
activeOrg.id,
|
||||||
|
newProjectName,
|
||||||
|
);
|
||||||
|
setProjects((prev) => [...prev, newProject]);
|
||||||
|
setNewProjectName("");
|
||||||
|
setIsCreating(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to create project", err);
|
||||||
|
setError("Falha ao criar o projeto.");
|
||||||
|
setIsCreating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredProjects = projects.filter((p) =>
|
||||||
|
p.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!activeOrg) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[60vh] text-center p-6">
|
||||||
|
<div className="w-20 h-20 bg-surface-muted rounded-[2rem] flex items-center justify-center mb-6 text-text-muted">
|
||||||
|
<Building2 size={40} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-black text-text-main mb-2">
|
||||||
|
Nenhuma Organização Ativa
|
||||||
|
</h2>
|
||||||
|
<p className="text-text-muted max-w-sm">
|
||||||
|
Selecione uma organização no menu superior para gerenciar seus
|
||||||
|
projetos.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8 pb-20">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-3 text-brand font-black uppercase tracking-widest text-[10px] mb-2">
|
||||||
|
<Layout size={14} />
|
||||||
|
Gerenciamento
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl font-black text-text-main tracking-tight">
|
||||||
|
Projetos de <span className="text-brand">{activeOrg.name}</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-text-muted font-medium mt-1">
|
||||||
|
Organize suas tarefas em projetos específicos para melhor
|
||||||
|
acompanhamento.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="relative group">
|
||||||
|
<Search
|
||||||
|
className="absolute left-4 top-1/2 -translate-y-1/2 text-text-muted group-focus-within:text-brand transition-colors"
|
||||||
|
size={18}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Filtrar projetos..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-12 pr-6 py-3.5 bg-surface border border-border-soft rounded-2xl outline-none focus:border-brand focus:ring-4 focus:ring-brand/5 transition-all w-full md:w-64 font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
|
{/* New Project Form */}
|
||||||
|
<div className="lg:col-span-1">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
className="bg-surface border border-border-soft p-8 rounded-[2.5rem] shadow-panel sticky top-24"
|
||||||
|
>
|
||||||
|
<div className="w-12 h-12 bg-brand/10 text-brand rounded-2xl flex items-center justify-center mb-6">
|
||||||
|
<Plus size={24} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-black text-text-main mb-2">
|
||||||
|
Novo Projeto
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-text-muted mb-8 font-medium">
|
||||||
|
Crie um novo contêiner para suas tarefas e objetivos.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form onSubmit={handleCreateProject} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-black uppercase tracking-widest text-text-muted ml-1">
|
||||||
|
Nome do Projeto
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="Ex: Redesign Mobile, Marketing Q3..."
|
||||||
|
value={newProjectName}
|
||||||
|
onChange={(e) => setNewProjectName(e.target.value)}
|
||||||
|
className="w-full px-5 py-4 bg-app-bg border border-border-soft rounded-2xl outline-none focus:border-brand focus:ring-4 focus:ring-brand/5 transition-all font-bold"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isCreating || !newProjectName.trim()}
|
||||||
|
className="w-full bg-brand hover:bg-brand-dark disabled:bg-surface-muted disabled:text-text-muted text-white font-black py-4 rounded-2xl shadow-lg shadow-brand/20 transition-all flex items-center justify-center gap-2 group active:scale-95"
|
||||||
|
>
|
||||||
|
{isCreating ? (
|
||||||
|
<Loader2 className="animate-spin" size={20} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>Criar Projeto</span>
|
||||||
|
<ArrowRight
|
||||||
|
size={18}
|
||||||
|
className="group-hover:translate-x-1 transition-transform"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="mt-6 p-4 bg-rose-500/10 border border-rose-500/20 rounded-2xl text-rose-500 text-xs font-bold flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
{error}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Projects List */}
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{[1, 2, 4, 5, 6].map((i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="h-32 bg-surface-muted animate-pulse rounded-[2rem]"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : filteredProjects.length === 0 ? (
|
||||||
|
<div className="bg-surface/50 border-2 border-dashed border-border-soft rounded-[3rem] p-20 text-center">
|
||||||
|
<div className="w-16 h-16 bg-surface-muted rounded-2xl flex items-center justify-center mx-auto mb-6 text-text-muted/30">
|
||||||
|
<FolderKanban size={32} />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-text-main mb-2">
|
||||||
|
Nenhum projeto encontrado
|
||||||
|
</h3>
|
||||||
|
<p className="text-text-muted max-w-xs mx-auto font-medium">
|
||||||
|
{searchQuery
|
||||||
|
? "Nenhum projeto corresponde à sua busca."
|
||||||
|
: "Sua organização ainda não possui projetos."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{filteredProjects.map((project, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={project.id}
|
||||||
|
layout
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9 }}
|
||||||
|
transition={{ delay: index * 0.05 }}
|
||||||
|
className="group bg-surface hover:bg-brand/[0.02] border border-border-soft p-6 rounded-[2rem] shadow-sm hover:shadow-xl hover:shadow-brand/5 transition-all flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<div className="w-14 h-14 bg-surface-muted rounded-2xl flex items-center justify-center text-brand font-black text-xl shadow-inner group-hover:bg-brand group-hover:text-white transition-all">
|
||||||
|
{project.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-black text-text-main group-hover:text-brand transition-colors text-lg leading-tight">
|
||||||
|
{project.name}
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<span className="text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||||
|
ID #{project.id}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-10 h-10 rounded-full flex items-center justify-center text-text-muted/20 group-hover:text-brand group-hover:bg-brand/10 transition-all">
|
||||||
|
<ArrowRight size={20} />
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -10,6 +10,7 @@ import { CreateOrganizationPage } from "./pages/CreateOrganizationPage";
|
||||||
import { JoinOrganizationPage } from "./pages/JoinOrganizationPage";
|
import { JoinOrganizationPage } from "./pages/JoinOrganizationPage";
|
||||||
import { OrganizationAdminPage } from "./pages/OrganizationAdminPage";
|
import { OrganizationAdminPage } from "./pages/OrganizationAdminPage";
|
||||||
import { MyRequestsPage } from "./pages/MyRequestsPage";
|
import { MyRequestsPage } from "./pages/MyRequestsPage";
|
||||||
|
import { ProjectsPage } from "./pages/ProjectsPage";
|
||||||
|
|
||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
|
|
@ -52,6 +53,10 @@ export const router = createBrowserRouter([
|
||||||
path: "requests",
|
path: "requests",
|
||||||
element: <MyRequestsPage />,
|
element: <MyRequestsPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "projects",
|
||||||
|
element: <ProjectsPage />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
19
frontend/src/services/projectService.ts
Normal file
19
frontend/src/services/projectService.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { apiFetch } from '../lib/api'
|
||||||
|
|
||||||
|
export interface Project {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const projectService = {
|
||||||
|
listByOrganization: async (orgId: number): Promise<Project[]> => {
|
||||||
|
return apiFetch<Project[]>(`/organizations/${orgId}/projects`)
|
||||||
|
},
|
||||||
|
|
||||||
|
create: async (orgId: number, name: string): Promise<Project> => {
|
||||||
|
return apiFetch<Project>(`/organizations/${orgId}/projects`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { name },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue