diff --git a/backend/src/coverage/clover.xml b/backend/src/coverage/clover.xml
deleted file mode 100644
index bcae853..0000000
--- a/backend/src/coverage/clover.xml
+++ /dev/null
@@ -1,481 +0,0 @@
-
-
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| src | -
-
- |
- 95.45% | -105/110 | -100% | -0/0 | -92.59% | -25/27 | -95.45% | -105/110 | -
| src/business | -
-
- |
- 81.85% | -194/237 | -64.8% | -81/125 | -77.27% | -34/44 | -85.11% | -183/215 | -
| src/controller | -
-
- |
- 80.18% | -89/111 | -75% | -3/4 | -87.5% | -21/24 | -80.9% | -89/110 | -
| src/utils | -
-
- |
- 100% | -4/4 | -100% | -0/0 | -100% | -1/1 | -100% | -4/4 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 | 5x -5x -5x -5x - - - - - -5x -5x -5x -5x - - - -5x -5x - - - -5x - - - -5x - - - -5x | import express from 'express';
-import cors from 'cors';
-import routes from './routes';
-import { errorHandler } from './middleware/errorHandler';
-
-class App {
- public server: express.Application;
-
- constructor() {
- this.server = express();
- this.middlewares();
- this.routes();
- this.handleErrors();
- }
-
- private middlewares(): void {
- this.server.use(express.json());
- this.server.use(cors());
- }
-
- private routes(): void {
- this.server.use(routes);
- }
-
- private handleErrors(): void {
- this.server.use(errorHandler);
- }
-}
-
-export default new App().server; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 | - -5x -5x -5x - - - - - - - -82x - -82x - - - - -82x -2x -1x - -1x -1x - - - -80x - -80x - -80x - - -80x - - - - - - - - -80x - - - -80x - -80x - - - - -77x -77x - - - -77x -1x - - -76x - - - - -76x -1x - - -75x - -75x - - - -2x - - - - - - - -5x |
-import { UserData, LoginData } from "../models/User";
-import { userValidate } from "../utils/validationUser";
-import knex from '../data/index';
-import bcrypt from 'bcryptjs';
-
-
-
-class BusinessLogicAuth{
-
- async newUser(data: UserData){
-
- userValidate(data);
-
- const existingUser = await knex('User')
- .where('username', data.username)
- .orWhere('email', data.email)
- .first();
-
- if (existingUser) {
- if (existingUser.username === data.username) {
- throw new Error('Este username já está em uso.');
- }
- Eif (existingUser.email === data.email) {
- throw new Error('Este e-mail já está em uso.');
- }
- }
-
- const salt = await bcrypt.genSalt(10);
-
- const senhaHash = await bcrypt.hash(data.senha, salt);
-
- const { nomeCompleto, username, email, telefone, dataNascimento } = data;
-
-
- await knex('User').insert({
- fullName: nomeCompleto,
- username,
- email,
- phone: telefone,
- birthDate: dataNascimento,
- passwordHash: senhaHash
- });
-
- const user = await knex('User')
- .where('username', data.username)
- .first();
-
- const { passwordHash, ...userData} = user;
-
- return userData;
-
- }
-
- async enterUser(data: LoginData){
- try{
- const user = await knex('User')
- .where('username', data.username)
- .first();
-
- if(!user){
- throw new Error('Usuário ou senha incorretos.');
- }
-
- const isPasswordValid = await bcrypt.compare(
- data.senha,
- user.passwordHash
- );
-
- if (!isPasswordValid) {
- throw new Error('Usuário ou senha incorretos.');
- }
-
- const { passwordHash, ...userData } = user;
-
- return userData;
-
- }catch(error){
-
- throw error;
-
- }
- }
-
-}
-
-
-export default new BusinessLogicAuth(); |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 | 5x - - - - - - - - -17x - - - -17x -2x - - -15x - -15x - - - - - -15x - - - -15x - - - -15x - - - - - - -15x - -2x - - - -2x -2x - - - - - -2x -2x - - - -15x - - - - - -4x - - - -4x - - - -4x - - - - -4x -1x - - -3x - - - - - - -3x - - - - -3x - - - -3x - - - -3x -1x - - -2x - - - -2x -1x - - -1x - - - -1x - - - - -1x - - - - - - - - -1x - -1x - - - - - - - - - - - - - - - - - -1x - - - - - - - - - - - - - - - - - - - -1x - - - -1x - -2x - -1x - -1x - - - - -2x - - - - - - -1x - - - - -1x - - - -1x - - - -1x - - - -1x -1x - -1x - - - - -1x - - - - -1x - - - - -1x - - - - - - - - - - - - - -3x - -3x - - - -3x -1x - - -2x -1x - - -1x -1x -1x - -1x -1x -1x - - - - -1x -1x - - - -1x -1x - - - -1x - - - - -1x -1x - - - - -1x - - - - -1x - - - -1x - - - -1x -1x - - - - - - - - - - -5x | import knex from '../data/index';
-import { CommunityData } from '../models/Community'
-
-
-
-class BusinessLogicCommunity{
-
- async newCommunity(data: CommunityData, creatorID: number){
-
- const existingCommunity = await knex('Communities')
- .where('name', data.name)
- .first();
-
- if (existingCommunity) {
- throw new Error("Já existe uma comunidade com este nome.");
- }
-
- return knex.transaction(async (trx) => {
-
- const communityToInsert = {
- name: data.name,
- description: data.description,
- creatorID: creatorID
- };
-
- const [createdCommunity] = await trx('Communities')
- .insert(communityToInsert)
- .returning('*');
-
- Iif (!createdCommunity) {
- throw new Error("Falha ao criar comunidade.");
- }
-
- await trx('CommunityMembers').insert({
- communityID: createdCommunity.communityID,
- userID: creatorID,
- role: 'admin',
- joinedAt: new Date()
- });
-
- if (data.technologies && data.technologies.length > 0) {
-
- const keywordIDs = await trx('Keywords')
- .whereIn('tag', data.technologies)
- .select('keywordID');
-
- const keywordsToInsert = keywordIDs.map(keyword => {
- return {
- communityID: createdCommunity.communityID,
- keywordID: keyword.keywordID
- };
- });
-
- Eif (keywordsToInsert.length > 0) {
- await trx('CommunitiesKeywords').insert(keywordsToInsert);
- }
- }
-
- return createdCommunity;
- });
- }
-
- async newMemberCommunity(userID: number, communityID: string){
-
- const community = await knex('Communities')
- .where('communityID', communityID)
- .first();
-
- Iif (!community) {
- throw new Error("Comunidade não encontrada.");
- }
-
- const existingMember = await knex('CommunityMembers')
- .where('communityID', communityID)
- .andWhere('userID', userID)
- .first();
-
- if (existingMember) {
- throw new Error("Usuário já é membro desta comunidade.");
- }
-
- await knex('CommunityMembers').insert({
- communityID: communityID,
- userID: userID,
- role: 'member',
- joinedAt: new Date()
- });
-
- return { message: "Membro adicionado com sucesso", communityID, userID };
- }
-
- async leaveMemberCommunity(userID: number, communityID: string){
-
- const community = await knex('Communities')
- .where('communityID', communityID)
- .first();
-
- Iif (!community) {
- throw new Error("Comunidade não encontrada.");
- }
-
- if (community.creatorID === userID) {
- throw new Error("O criador não pode sair da comunidade. Você deve deletá-la ou transferir a propriedade.");
- }
-
- const member = await knex('CommunityMembers')
- .where({ communityID, userID })
- .first();
-
- if (!member) {
- throw new Error("Você não é membro desta comunidade.");
- }
-
- await knex('CommunityMembers')
- .where({ communityID, userID })
- .del();
-
- return { message: "Você saiu da comunidade com sucesso." };
- }
-
- async getAllUserCommunities(userID: number) {
-
- const userCommunities = await knex('Communities')
- .join('CommunityMembers', 'Communities.communityID', '=', 'CommunityMembers.communityID')
- .where('CommunityMembers.userID', userID)
- .select(
- 'Communities.*',
- 'CommunityMembers.role',
- 'CommunityMembers.joinedAt'
- );
-
- const communityIds = userCommunities.map(c => c.communityID);
-
- Eif (communityIds.length === 0) return [];
-
- const keywords = await knex('CommunitiesKeywords')
- .join('Keywords', 'CommunitiesKeywords.keywordID', '=', 'Keywords.keywordID')
- .whereIn('CommunitiesKeywords.communityID', communityIds)
- .select('CommunitiesKeywords.communityID', 'Keywords.tag');
-
- const result = userCommunities.map(comm => ({
- ...comm,
- technologies: keywords
- .filter((k: any) => k.communityID === comm.communityID)
- .map((k: any) => k.tag)
- }));
-
- return result;
- }
-
- async getUserFeed(userID: number){
- const feed = await knex('Projects')
-
- .join('ProjectCommunities', 'Projects.projectID', '=', 'ProjectCommunities.projectID')
-
- .join('CommunityMembers', 'ProjectCommunities.communityID', '=', 'CommunityMembers.communityID')
-
- .join('User', 'Projects.creatorID', '=', 'User.id')
-
- .where('CommunityMembers.userID', userID)
-
- .distinct('Projects.projectID')
-
- .select(
- 'Projects.*',
- 'User.username as authorUsername',
- 'User.fullName as authorName'
- )
-
- .orderBy('Projects.createdAt', 'desc');
-
- return feed;
- }
-
- async getAllCommunities(){
- const communities = await knex('Communities').select('*');
-
- const communityIds = communities.map(c => c.communityID);
-
- Iif (communityIds.length === 0) return [];
-
- const keywords = await knex('CommunitiesKeywords')
- .join('Keywords', 'CommunitiesKeywords.keywordID', '=', 'Keywords.keywordID')
- .whereIn('CommunitiesKeywords.communityID', communityIds)
- .select('CommunitiesKeywords.communityID', 'Keywords.tag');
-
- const result = communities.map(comm => ({
- ...comm,
- technologies: keywords
- .filter((k: any) => k.communityID === comm.communityID)
- .map((k: any) => k.tag)
- }));
-
- return result;
- }
-
- async getCommunityData(communityID: string, userID: number) {
-
- const community = await knex('Communities')
- .where('communityID', communityID)
- .first();
-
- Iif (!community) {
- throw new Error("Comunidade não encontrada.");
- }
-
- const membership = await knex('CommunityMembers')
- .where({ communityID: communityID, userID: userID })
- .first();
-
- const isMember = !!membership;
- const isAdmin = (community.creatorID === userID) || (membership && membership.role === 'admin');
-
- const keywords = await knex('CommunitiesKeywords')
- .join('Keywords', 'CommunitiesKeywords.keywordID', '=', 'Keywords.keywordID')
- .where('CommunitiesKeywords.communityID', communityID)
- .select('Keywords.tag');
-
- const memberCount = await knex('CommunityMembers')
- .where('communityID', communityID)
- .count('userID as count')
- .first();
-
- const projects = await knex('Projects')
- .join('ProjectCommunities', 'Projects.projectID', '=', 'ProjectCommunities.projectID')
- .where('ProjectCommunities.communityID', communityID)
- .select('Projects.*');
-
- return {
- community: {
- ...community,
- technologies: keywords.map((k: any) => k.tag),
- memberCount: memberCount?.count || 0,
- isMember: isMember,
- isAdmin: isAdmin
- },
- posts: projects || []
- };
- }
-
- async updateCommunity(creatorID: number, communityID: string, data: CommunityData){
-
- return knex.transaction(async (trx) => {
-
- const community = await trx('Communities')
- .where('communityID', communityID)
- .first();
-
- if (!community) {
- throw new Error("Comunidade não encontrada.");
- }
-
- if (community.creatorID !== creatorID) {
- throw new Error("Você não tem permissão para editar esta comunidade.");
- }
-
- const fieldsToUpdate: any = {};
- Eif (data.name) fieldsToUpdate.name = data.name;
- Eif (data.description) fieldsToUpdate.description = data.description;
-
- Eif (Object.keys(fieldsToUpdate).length > 0) {
- fieldsToUpdate.updatedAt = new Date();
- await trx('Communities')
- .where('communityID', communityID)
- .update(fieldsToUpdate);
- }
-
- Eif (data.technologies !== undefined) {
- await trx('CommunitiesKeywords')
- .where('communityID', communityID)
- .del();
-
- Eif (Array.isArray(data.technologies) && data.technologies.length > 0) {
- const keywordIDs = await trx('Keywords')
- .whereIn('tag', data.technologies)
- .select('keywordID');
-
- const linksToInsert = keywordIDs.map((k: any) => ({
- communityID: communityID,
- keywordID: k.keywordID
- }));
-
- Eif (linksToInsert.length > 0) {
- await trx('CommunitiesKeywords').insert(linksToInsert);
- }
- }
- }
-
- return await trx('Communities').where('communityID', communityID).first();
- });
- }
-
- async removeCommunity(creatorID: number, communityID: string){
- const community = await knex('Communities')
- .where('communityID', communityID)
- .first();
-
- Iif (!community) {
- throw new Error("Comunidade não encontrada.");
- }
-
- Eif (community.creatorID !== creatorID) {
- throw new Error("Você não tem permissão para deletar esta comunidade.");
- }
-
- await knex('Communities')
- .where('communityID', communityID)
- .del();
- }
-
-}
-
-
-export default new BusinessLogicCommunity(); |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 | 5x - -5x - - - - - - - - -3x - -3x -3x -3x -3x -3x - -3x - - - - -3x -1x - - -2x - -2x - - - -2x - - - - -2x - - - -2x - -2x - - - -2x - - - -2x - - - - - -5x | import knex from '../data/index';
-import { UserData } from '../models/User';
-import bcrypt from 'bcryptjs';
-
-
-
-class businessLogicProfile{
-
-
- async updateProfile(data: UserData, userID: number){
-
- const fieldsToUpdate: any = {};
-
- if (data.nomeCompleto) fieldsToUpdate.fullName = data.nomeCompleto;
- Iif (data.email) fieldsToUpdate.email = data.email;
- Iif (data.username) fieldsToUpdate.username = data.username;
- if (data.telefone) fieldsToUpdate.phone = data.telefone;
- Iif (data.dataNascimento) fieldsToUpdate.birthDate = data.dataNascimento;
-
- Iif (data.senha) {
- const salt = await bcrypt.genSalt(10);
- fieldsToUpdate.passwordHash = await bcrypt.hash(data.senha, salt);
- }
-
- if (Object.keys(fieldsToUpdate).length === 0) {
- throw new Error("Nenhum dado para atualizar.");
- }
-
- fieldsToUpdate.updatedAt = new Date();
-
- await knex('User')
- .where('id', userID)
- .update(fieldsToUpdate);
-
- const updatedUser = await knex('User')
- .where('id', userID)
- .select('id', 'fullName', 'username', 'email', 'phone', 'birthDate', 'createdAt')
- .first();
-
- return updatedUser;
- }
-
- async removeProfile(userID: number){
- const user = await knex('User').where('id', userID).first();
-
- Iif (!user) {
- throw new Error("Usuário não encontrado.");
- }
-
- await knex('User')
- .where('id', userID)
- .del();
-
- return { message: "Perfil deletado com sucesso." };
- }
-
-}
-
-
-export default new businessLogicProfile(); |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 | -5x - - - - - - -17x - -17x - - - - - - - -17x - - - -17x - -3x - - - -3x -5x - - - - - -3x -3x - - - - - -3x -5x - - -3x -1x - - - - - -1x - - - - -17x - - - - - -2x - -2x - - - -2x - - - -2x -1x - - -1x -1x -1x -1x -1x - -1x -1x -1x - - - - -1x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -1x - - - - - -4x - - -4x - - - - -9x - -9x - -9x - - - - - - -9x - - - -2x - - - - - - - - - -2x - - - -3x - - - - - - - - - -3x - - - - -2x - - - -2x - - - -2x -1x - - -1x - - - - - - - -2x - - - -2x - - - -2x -1x - - -1x - - - - - - -3x - - - - - - - - - - -3x -2x - - - -1x - - - - - -1x - -2x - - - - - -5x |
-import knex from '../data/index';
-import { ProjectData } from '../models/Project';
-
-class BusinessLogicProject{
-
- async newProject(data: ProjectData, creatorID: number){
-
- return knex.transaction(async (trx) => {
-
- const projectToInsert = {
- title: data.title,
- description: data.description,
- status: data.status,
- startDate: data.startDate,
- creatorID: creatorID
- };
-
- const [newProject] = await trx('Projects')
- .insert(projectToInsert)
- .returning('*');
-
- if (data.technologies && data.technologies.length > 0) {
-
- const keywordIDs = await trx('Keywords')
- .whereIn('tag', data.technologies)
- .select('keywordID');
-
- const keywordsToInsert = keywordIDs.map(keyword => {
- return {
- projectID: newProject.projectID,
- keywordID: keyword.keywordID
- };
- });
-
- Eif (keywordsToInsert.length > 0) {
- await trx('ProjectsKeywords').insert(keywordsToInsert);
-
- // --- NOVA LÓGICA DE ASSOCIAÇÃO AUTOMÁTICA ---
-
- // 3. Busca Comunidades que usam essas mesmas Keywords
- // Usamos .distinct() para evitar duplicatas (caso uma comunidade tenha React E Node, por exemplo)
- const matchingCommunities = await trx('CommunitiesKeywords')
- .whereIn('keywordID', keywordIDs.map(k => k.keywordID))
- .distinct('communityID');
-
- if (matchingCommunities.length > 0) {
- const communitiesToLink = matchingCommunities.map(comm => ({
- projectID: newProject.projectID,
- communityID: comm.communityID,
- associatedAt: new Date()
- }));
-
- await trx('ProjectCommunities').insert(communitiesToLink);
- }
- }
- }
-
- return newProject;
- });
- }
-
- async updateProject(projectId: string, projectData: Partial<any>, userId: number) {
-
- return knex.transaction(async (trx) => {
-
- const existingProject = await trx('Projects')
- .where({ projectID: projectId })
- .first();
-
- Iif (!existingProject) {
- throw new Error("Projeto não encontrado.");
- }
-
- if (existingProject.creatorID !== userId) {
- throw new Error("Você não tem permissão para editar este projeto.");
- }
-
- const fieldsToUpdate: any = {};
- Eif (projectData.title !== undefined) fieldsToUpdate.title = projectData.title;
- Eif (projectData.description !== undefined) fieldsToUpdate.description = projectData.description;
- Iif (projectData.status !== undefined) fieldsToUpdate.status = projectData.status;
- Iif (projectData.startDate !== undefined) fieldsToUpdate.startDate = projectData.startDate;
-
- Eif (Object.keys(fieldsToUpdate).length > 0) {
- fieldsToUpdate.updatedAt = new Date();
- await trx('Projects')
- .where({ projectID: projectId })
- .update(fieldsToUpdate);
- }
-
- Iif (projectData.technologies !== undefined) {
-
- await trx('ProjectsKeywords')
- .where({ projectID: projectId })
- .del();
-
- await trx('ProjectCommunities')
- .where({ projectID: projectId })
- .del();
-
- if (Array.isArray(projectData.technologies) && projectData.technologies.length > 0) {
-
- const keywordIDs = await trx('Keywords')
- .whereIn('tag', projectData.technologies)
- .select('keywordID');
-
- const keywordsToInsert = keywordIDs.map((k: any) => ({
- projectID: projectId,
- keywordID: k.keywordID
- }));
-
- if (keywordsToInsert.length > 0) {
- await trx('ProjectsKeywords').insert(keywordsToInsert);
- }
-
- const matchingCommunities = await trx('CommunitiesKeywords')
- .whereIn('keywordID', keywordIDs.map((k:any) => k.keywordID))
- .distinct('communityID');
-
- if (matchingCommunities.length > 0) {
- const communitiesToLink = matchingCommunities.map((comm:any) => ({
- projectID: projectId,
- communityID: comm.communityID,
- associatedAt: new Date()
- }));
-
- await trx('ProjectCommunities').insert(communitiesToLink);
- }
- }
- }
-
- return await trx('Projects').where({ projectID: projectId }).first();
- });
- }
-
- async userProjects(creatorID: number){
-
- const projects = await knex('Projects')
- .where('creatorID', creatorID);
-
- return projects;
- }
-
- async newComment(userID: number, projectID: string, content: string){
-
- const project = await knex('Projects').where('projectID', projectID).first();
-
- Iif (!project) throw new Error("Projeto não encontrado.");
-
- const [newComment] = await knex('Comments').insert({
- authorID: userID,
- projectID: projectID,
- content: content,
- createdAt: new Date()
- }).returning('*');
-
- return newComment;
- }
-
- async getProjectComments(projectID: string){
- const comments = await knex('Comments')
- .join('User', 'Comments.authorID', '=', 'User.id')
- .where('Comments.projectID', projectID)
- .select(
- 'Comments.*',
- 'User.fullName',
- 'User.username'
- )
- .orderBy('Comments.createdAt', 'desc');
-
- return comments;
- }
-
- async getUserComments(userID: number){
- const comments = await knex('Comments')
- .join('Projects', 'Comments.projectID', '=', 'Projects.projectID')
- .where('Comments.authorID', userID)
- .select(
- 'Comments.*',
- 'Projects.title as projectTitle'
- )
- .orderBy('Comments.createdAt', 'desc')
- .orderBy('Comments.commentID', 'desc');
-
- return comments;
- }
-
- async removeComment(userID: number, commentID: string){
-
- const comment = await knex('Comments')
- .where('commentID', commentID)
- .first();
-
- Iif (!comment) {
- throw new Error("Comentário não encontrado.");
- }
-
- if (comment.authorID !== userID) {
- throw new Error("Você não tem permissão para deletar este comentário.");
- }
-
- await knex('Comments')
- .where('commentID', commentID)
- .del();
-
- }
-
- async removeProject(creatorID: number, projectID: string){
-
- const project = await knex('Projects')
- .where('projectID', projectID)
- .first();
-
- Iif (!project) {
- throw new Error("Projeto não encontrado.");
- }
-
- if (project.creatorID !== creatorID) {
- throw new Error("Você não tem permissão para deletar este projeto.");
- }
-
- await knex('Projects')
- .where('projectID', projectID)
- .del();
- }
-
- async getProjectById(projectId: string) {
- // Busca o projeto com dados do autor (join)
- const project = await knex('Projects')
- .join('User', 'Projects.creatorID', '=', 'User.id')
- .where('Projects.projectID', projectId)
- .select(
- 'Projects.*',
- 'User.username as authorUsername',
- 'User.fullName as authorName',
- 'User.id as authorID'
- )
- .first();
-
- if (!project) {
- throw new Error("Projeto não encontrado.");
- }
-
- // Busca as tecnologias (keywords) associadas
- const keywords = await knex('ProjectsKeywords')
- .join('Keywords', 'ProjectsKeywords.keywordID', '=', 'Keywords.keywordID')
- .where('ProjectsKeywords.projectID', projectId)
- .select('Keywords.tag');
-
- // Retorna o objeto formatado
- return {
- ...project,
- technologies: keywords.map((k: any) => k.tag)
- };
- }
-
-}
-
-export default new BusinessLogicProject(); |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| businessLogicAuth.ts | -
-
- |
- 100% | -28/28 | -90% | -9/10 | -100% | -2/2 | -100% | -28/28 | -
| businessLogicCommunity.ts | -
-
- |
- 83% | -83/100 | -65.45% | -36/55 | -68.18% | -15/22 | -84.61% | -77/91 | -
| businessLogicProfile.ts | -
-
- |
- 78.57% | -22/28 | -68.75% | -11/16 | -100% | -2/2 | -86.95% | -20/23 | -
| businessLogicProject.ts | -
-
- |
- 75.3% | -61/81 | -56.81% | -25/44 | -83.33% | -15/18 | -79.45% | -58/73 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| requestController.ts | -
-
- |
- 80.18% | -89/111 | -75% | -3/4 | -87.5% | -21/24 | -80.9% | -89/110 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 | 5x -5x - - -5x -5x -5x - -5x -5x -5x - -5x - - - - -82x - -82x - -80x - -80x - - - - - -80x - - -2x - - - - -77x - -77x - -75x - -75x - - - - - -75x - - - -2x - - - - -17x -17x -17x - -17x - - - - - - - - - - - - - - - - - - - - - - - - -4x - -4x - -4x - - - - - - - -2x - -2x - -1x - - -1x - - - - -2x - -2x - - -1x - - - - -3x -3x -1x - -2x - - - - -17x - -17x -15x - - -2x - - - - - - - - - - - - - - -1x -1x - -1x - -1x - - - - - - - - - -1x - -1x - -1x - - - - - - - - -1x - -1x - -1x - - - - - - - -4x - -4x -3x - - -1x - - - - -3x - -3x - -1x - - -2x - - - - - -3x - -3x - -1x - - -2x - - - - -1x - -1x - - -1x - - - - -9x - -9x - - - -9x - -9x - - - - - - - -2x - -2x - -2x - - - - - - - -3x - -3x - -2x - - -1x - - - - - -2x - -2x -2x - - - - - - - - -2x - -2x - - -1x - - - - -3x - -3x - -3x - - - - - - - - -5x | import businessLogicUser from '../business/businessLogicAuth'
-import BusinessLogicProject from '../business/businessLogicProject';
-import {UserData, LoginData } from '../models/User'
-import { ProjectData } from '../models/Project';
-import jwt from 'jsonwebtoken';
-import { authConfig } from '../config/auth'
-import knex from '../data';
-import { CommunityData } from '../models/Community';
-import BusinessLogicCommunity from '../business/businessLogicCommunity';
-import businessLogicProject from '../business/businessLogicProject';
-import businessLogicCommunity from '../business/businessLogicCommunity';
-import { error } from 'console';
-import businessLogicProfile from '../business/businessLogicProfile';
-
-class RequestController {
-
- async createUser(data: UserData) {
- try{
-
- const userData = await businessLogicUser.newUser(data);
-
- const payload = { id: userData.id, username: userData.username };
-
- const token = jwt.sign(
- payload,
- authConfig.secret,
- { expiresIn: authConfig.expiresIn }
- );
-
- return { user: userData, token: token };
-
- } catch(error){
- throw error;
- }
- }
-
- async enterUser(data: LoginData){
- try{
-
- const userData = await businessLogicUser.enterUser(data);
-
- const payload = { id: userData.id, username: userData.username };
-
- const token = jwt.sign(
- payload,
- authConfig.secret,
- { expiresIn: authConfig.expiresIn }
- );
-
- return { user: userData, token: token };
-
- }catch(error){
-
- throw error;
- }
- }
-
- async createProject(data: ProjectData, creatorID: number){
- try{
- console.log(data);
- const newProject = await BusinessLogicProject.newProject(data, creatorID);
-
- return newProject;
-
- }catch(error){
-
- throw new Error("Erro nos dados do projeto.");
-
- }
- }
-
- async getKeywords(){
- try{
-
- const keywords = await knex('Keywords').select('tag');
-
- const tags = keywords.map(kw => kw.tag);
-
- return tags;
-
- }catch(error){
- console.error("Erro ao buscar keywords:", error);
- throw new Error("Não foi possível buscar as keywords.");
- }
- }
-
- async getUserProjects(creatorID: number){
- try{
-
- const projects = await BusinessLogicProject.userProjects(creatorID);
-
- return projects;
-
- }catch(error){
- throw new Error("Erro ao buscar os projetos do usuário.");
- }
- }
-
- async updateProject(projectId: string, data: ProjectData, userId: number){
- try{
-
- const updatedProject = await BusinessLogicProject.updateProject(projectId, data, userId);
-
- return updatedProject;
-
- }catch(error){
- throw new Error("Erro ao atualizar o projeto.");
- }
- }
-
- async removeProject(userID: number, projectID: string){
- try{
-
- await BusinessLogicProject.removeProject(userID, projectID);
-
- }catch(error){
- throw error;
- }
- }
-
- async getProjectById(projectId: string) {
- try {
- const project = await businessLogicProject.getProjectById(projectId);
- return project;
- } catch (error) {
- throw error;
- }
- }
-
- async newCommunity(data: CommunityData, creatorID: number){
- try{
-
- const newCommunity = await BusinessLogicCommunity.newCommunity(data, creatorID);
- return newCommunity;
-
- }catch(error){
- throw error;
- }
- }
-
- async getAllUserCommunities(userID: number) {
- try {
- const communities = await BusinessLogicCommunity.getAllUserCommunities(userID);
- return communities;
- } catch (error) {
- console.error("Erro ao buscar comunidades:", error);
- throw new Error("Erro ao carregar as comunidades.");
- }
- }
-
- async getUserHome(userID: number){
- try{
- const communities = await businessLogicCommunity.getAllUserCommunities(userID);
-
- const feed = await businessLogicCommunity.getUserFeed(userID);
-
- return {
- communities: communities,
- feed: feed
- };
-
- }catch(error){
- throw error;
- }
- }
- async getAllCommunities(){
- try{
-
- const communities = await BusinessLogicCommunity.getAllCommunities();
-
- return communities;
-
- }catch(error){
- throw error;
- }
-
- }
-
- async getCommunityData(communityID: string, userID: number){
- try{
-
- const result = await BusinessLogicCommunity.getCommunityData(communityID,userID);
-
- return result;
-
- }catch(error){
- throw error;
- }
- }
-
- async newMemberCommunity(userID: number, communityID: string){
- try{
-
- const result = await BusinessLogicCommunity.newMemberCommunity(userID, communityID);
- return result;
-
- }catch(error){
- throw error;
- }
- }
-
- async leaveMemberCommunity(userID: number, communityID: string){
- try{
-
- const result = await businessLogicCommunity.leaveMemberCommunity(userID, communityID);
-
- return result;
-
- }catch(error){
- throw error;
- }
-
- }
-
- async updateCommunity(creatorID: number, communityID: string, data: CommunityData){
- try{
-
- const updatedCommunity = await BusinessLogicCommunity.updateCommunity(creatorID, communityID, data);
-
- return updatedCommunity;
-
- }catch(error){
- throw error;
- }
- }
-
- async removeCommunity(creatorID: number, communityID: string){
- try{
-
- await BusinessLogicCommunity.removeCommunity(creatorID, communityID);
-
- }catch(error){
- throw error;
- }
- }
-
- async newComment(userID: number, projectID: string, content: string){
- try{
-
- Iif (!content || content.trim() === "") {
- throw new Error("O comentário não pode ser vazio.");
- }
-
- const comment = await businessLogicProject.newComment(userID, projectID, content);
-
- return comment;
-
- }catch(error){
- throw error;
- }
- }
-
- async getProjectComments(projectID: string) {
- try{
-
- const projectComments = await businessLogicProject.getProjectComments(projectID);
-
- return projectComments;
-
- }catch(error){
- throw error;
- }
- }
-
- async updateProfile(data: UserData, userID: number){
- try{
-
- const result = await businessLogicProfile.updateProfile(data, userID);
-
- return result;
-
- }catch(error){
- throw error;
- }
-
- }
-
- async deleteProfile(userID: number){
- try{
-
- const message = await businessLogicProfile.removeProfile(userID);
- return message;
-
- }catch(error){
- throw error;
- }
-
- }
-
- async deleteComment(userID: number, commentID: string){
- try{
-
- await businessLogicProject.removeComment(userID, commentID);
-
- }catch(error){
- throw error;
- }
- }
-
- async getUserComments(userID: number){
- try{
-
- const allComments = await businessLogicProject.getUserComments(userID);
-
- return allComments;
-
- }catch(error){
- throw error;
- }
- }
-
-}
-
-export default new RequestController(); |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 | 5x -5x -5x - - - -5x - - -5x -82x -82x -80x - - - - - - - -5x -77x -75x - - - - - - - - -5x - - - - - - -5x -17x - -17x - -17x - -17x - - - - - - - -5x -4x -4x -4x - - - - - -5x -2x -2x -2x - -2x - -1x - - - - - -5x - -2x - -2x - -1x - - - -5x - -3x - -3x - -1x - - - -5x - -17x -15x - - - - - -5x - - - - - - -5x - -1x -1x - -1x - -1x - - -5x - -1x -1x - - - -5x - -4x -4x - -4x - -3x - - - -5x -3x -3x - -3x - -1x - - - - - - - -5x -1x - -1x - - - - - - -5x - -9x -9x - -9x - -9x - - - -5x - -3x - -3x - - -5x - -2x - -2x - -1x - - - -5x - -2x - -2x - -2x - - - - -5x - -3x - -2x - - -5x - -2x - -2x - - -5x - -1x - -1x - - -5x -3x -3x -3x -1x - -2x - - - - -5x | import express from 'express';
-import RequestController from './controller/requestController';
-import authMiddleware from './middleware/auth';
-import auth from './middleware/auth';
-import { request } from 'http';
-
-const routes = express.Router();
-
-// Endpoint para CADASTRAR um novo usuário
-routes.post('/api/register', async (request, response) => {
- console.log("Recebendo requisição de cadastro:", request.body);
- const {user, token} = await RequestController.createUser(request.body);
- return response.status(201).json({
- user,
- token,
- message: 'Usuário cadastrado com sucesso!'
- });
-});
-
-// Endpoint para verificar a existência de usuário
-routes.post('/api/login', async(request, response) => {
- const { user, token } = await RequestController.enterUser(request.body);
- return response.status(200).json({
- user,
- token,
- message: 'Usuário autenticado com sucesso.'
- });
-});
-
-
-// Endpoint para enviar ao fronend os keywords disponíveis
-routes.get('/api/keywords', async(request, response) => {
- const tags = await RequestController.getKeywords();
-
- return response.status(200).json(tags);
-});
-
-// Endpoint para criar projeto
-routes.post('/api/user/newproject', authMiddleware, async(request, response) =>{
- const projectData = request.body;
-
- const creatorID = request.user.id;
-
- const newProject = await RequestController.createProject(projectData, creatorID);
-
- return response.status(201).json({
- message: "Projeto criado com sucesso!",
- project: newProject
- });
-});
-
-
-// Endpoint para enviar ao frontend os projetos de determinado usuário
-routes.get('/api/user/projects', authMiddleware, async(request, response) => {
- const creatorID = request.user.id;
- const projects = await RequestController.getUserProjects(creatorID);
- return response.status(200).json({
- projects
- })
-});
-
-// Endpoint para atualizar um projeto existente
-routes.put('/api/user/updateproject/:projectId', authMiddleware, async(request, response) => {
- const { projectId } = request.params;
- const updatedData = request.body;
- const creatorID = request.user.id;
-
- const updatedProject = await RequestController.updateProject(projectId, updatedData, creatorID);
-
- return response.status(200).json({
- message: "Projeto atualizado com sucesso!",
- project: updatedProject
- });
-});
-
-routes.delete('/api/user/deleteproject/:projectId', authMiddleware, async(request, response) =>{
-
- const { projectId } = request.params;
-
- await RequestController.removeProject(request.user.id, projectId);
-
- return response.status(200).json({ message: "Projeto deletado com sucesso." });
-});
-
-
-routes.delete('/api/user/leavecommunity/:communityID', authMiddleware, async(request, response) =>{
-
- const { communityID } = request.params;
-
- const result = await RequestController.leaveMemberCommunity(request.user.id, communityID);
-
- return response.status(200).json(result);
-})
-
-// Endpoint para criar uma comunidade
-routes.post('/api/newcommunity', authMiddleware, async(request, response) => {
-
- const newCommunity = await RequestController.newCommunity(request.body, request.user.id);
- return response.status(201).json({
- community: newCommunity
- })
-
-});
-
-routes.get('/api/user/communities', authMiddleware, async (request, response) => {
-
- const communities = await RequestController.getAllUserCommunities(request.user.id);
- return response.status(200).json(communities);
-
-});
-
-routes.get('/api/communities/data/:communityId', authMiddleware, async (request, response) =>{
-
- const { communityId } = request.params;
- const userID = request.user.id;
-
- const data = await RequestController.getCommunityData(communityId, userID);
-
- return response.status(200).json(data);
-});
-
-routes.get('/api/communities', async(request, response) => {
-
- const communities = await RequestController.getAllCommunities();
- return response.status(200).json(communities);
-
-});
-
-routes.post('/api/communities/:communityId/join', authMiddleware, async(request, response) =>{
-
- const userID = request.user.id;
- const { communityId } = request.params;
-
- const result = await RequestController.newMemberCommunity(userID, communityId);
-
- return response.status(201).json(result);
-});
-
-
-routes.put('/api/communities/updatecommunity/:communityId', authMiddleware, async(request, response) => {
- const { communityId } = request.params;
- const data = request.body;
-
- const updatedCommunity = await RequestController.updateCommunity(request.user.id, communityId, data);
-
- return response.status(200).json({
- message: "Comunidade atualizada com sucesso!",
- community: updatedCommunity
- });
-
-});
-
-
-routes.delete('/api/communities/deletecommunity/:communityId', authMiddleware, async(request, response) => {
- const { communityId } = request.params;
-
- await RequestController.removeCommunity(request.user.id, communityId);
-
- return response.status(200).json({ message: "Comunidade deletada com sucesso." });
-
-})
-
-
-routes.post('/api/project/:projectID/comments', authMiddleware, async(request, response) =>{
-
- const { projectID } = request.params;
- const { content } = request.body;
-
- const comment = await RequestController.newComment(request.user.id, projectID, content);
-
- return response.status(201).json(comment);
-
-})
-
-routes.get('/api/user/comments', authMiddleware, async(request, response) => {
-
- const allComments = await RequestController.getUserComments(request.user.id);
-
- return response.status(200).json(allComments);
-})
-
-routes.delete('/api/project/:commentID/deletecomment', authMiddleware, async(request, response) =>{
-
- const { commentID } = request.params;
-
- await RequestController.deleteComment( request.user.id, commentID );
-
- return response.status(200).json({ message: "Comentário deletado com sucesso."});
-
-})
-
-routes.get('/api/project/:projectID/comments', async(request, response) => {
-
- const { projectID } = request.params;
-
- const projectComments = await RequestController.getProjectComments( projectID );
-
- return response.status(200).json(projectComments);
-
-})
-
-
-routes.put('/api/user/editprofile', authMiddleware, async(request, response) =>{
-
- const updatedProfile = await RequestController.updateProfile(request.body, request.user.id);
-
- return response.status(200).json(updatedProfile);
-})
-
-routes.delete('/api/user/deleteprofile', authMiddleware, async(request, response) =>{
-
- const message = await RequestController.deleteProfile(request.user.id);
-
- return response.status(200).json(message);
-})
-
-routes.get('/api/user/home', authMiddleware, async(request, response) => {
-
- const homeData = await RequestController.getUserHome(request.user.id);
-
- return response.status(200).json(homeData);
-})
-
-routes.get('/api/projects/:projectId', authMiddleware, async (request, response) => {
- const { projectId } = request.params;
- try {
- const project = await RequestController.getProjectById(projectId);
- return response.status(200).json(project);
- } catch (error) {
- return response.status(404).json({ message: "Projeto não encontrado" });
- }
-});
-
-
-export default routes; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| validationUser.ts | -
-
- |
- 100% | -4/4 | -100% | -0/0 | -100% | -1/1 | -100% | -4/4 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 | 5x - - -5x - - - - - - - - -5x -82x - | import { z } from 'zod';
-import { UserData } from '../models/User';
-
-const userSchema = z.object({
- nomeCompleto: z.string().min(3, "Nome muito curto"),
- username: z.string().min(4, "Username muito curto"),
- email: z.string().email("Formato de e-mail inválido"),
- telefone: z.string().optional(),
- dataNascimento: z.string()
-});
-
-
-export function userValidate(data: UserData){
- userSchema.parse(data);
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| src | -
-
- |
- 100% | -1/1 | -100% | -0/0 | -100% | -1/1 | -100% | -1/1 | -
| src/API | -
-
- |
- 5.3% | -6/113 | -4% | -2/50 | -4.54% | -1/22 | -5.3% | -6/113 | -
| src/components/common/Keyword | -
-
- |
- 100% | -3/3 | -100% | -0/0 | -100% | -1/1 | -100% | -3/3 | -
| src/components/common/Modal | -
-
- |
- 100% | -12/12 | -100% | -4/4 | -100% | -4/4 | -100% | -12/12 | -
| src/components/common/Toast | -
-
- |
- 100% | -7/7 | -100% | -0/0 | -100% | -4/4 | -100% | -7/7 | -
| src/components/domain/CreationForm | -
-
- |
- 87.32% | -62/71 | -85.71% | -12/14 | -84.48% | -49/58 | -86.95% | -60/69 | -
| src/components/domain/Postcard | -
-
- |
- 69.23% | -72/104 | -58.9% | -43/73 | -64.28% | -18/28 | -72.16% | -70/97 | -
| src/components/domain/Searchbar | -
-
- |
- 91.07% | -51/56 | -83.33% | -30/36 | -78.94% | -15/19 | -90.9% | -50/55 | -
| src/components/domain/TagInput | -
-
- |
- 94.28% | -33/35 | -76.19% | -16/21 | -92.85% | -13/14 | -93.93% | -31/33 | -
| src/components/layout/Header | -
-
- |
- 100% | -1/1 | -100% | -0/0 | -100% | -1/1 | -100% | -1/1 | -
| src/components/layout/HeaderHome | -
-
- |
- 100% | -1/1 | -100% | -0/0 | -100% | -1/1 | -100% | -1/1 | -
| src/components/layout/Sidebar | -
-
- |
- 93.75% | -15/16 | -75% | -3/4 | -100% | -5/5 | -93.75% | -15/16 | -
| src/pages/CommunityPage | -
-
- |
- 83.94% | -115/137 | -68.42% | -39/57 | -88.05% | -59/67 | -85.24% | -104/122 | -
| src/pages/CreateCommunity | -
-
- |
- 81.81% | -27/33 | -90.9% | -20/22 | -71.42% | -5/7 | -81.81% | -27/33 | -
| src/pages/CreateProject | -
-
- |
- 84.44% | -38/45 | -87.09% | -27/31 | -80% | -8/10 | -86.04% | -37/43 | -
| src/pages/EditProfile | -
-
- |
- 87.09% | -27/31 | -55.55% | -10/18 | -66.66% | -4/6 | -93.1% | -27/29 | -
| src/pages/Feed | -
-
- |
- 86.27% | -44/51 | -69.23% | -9/13 | -74.07% | -20/27 | -87.75% | -43/49 | -
| src/pages/Home | -
-
- |
- 100% | -1/1 | -100% | -0/0 | -100% | -1/1 | -100% | -1/1 | -
| src/pages/Login | -
-
- |
- 87.5% | -14/16 | -75% | -3/4 | -50% | -2/4 | -87.5% | -14/16 | -
| src/pages/Profile | -
-
- |
- 92.43% | -110/119 | -82.25% | -51/62 | -89.58% | -43/48 | -94.54% | -104/110 | -
| src/pages/ProjectPage | -
-
- |
- 90% | -18/20 | -75% | -6/8 | -100% | -4/4 | -94.11% | -16/17 | -
| src/pages/Register | -
-
- |
- 87.5% | -14/16 | -75% | -3/4 | -60% | -3/5 | -87.5% | -14/16 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| src | -
-
- |
- 100% | -1/1 | -100% | -0/0 | -100% | -1/1 | -100% | -1/1 | -
| src/API | -
-
- |
- 5.3% | -6/113 | -4% | -2/50 | -4.54% | -1/22 | -5.3% | -6/113 | -
| src/components/common/Keyword | -
-
- |
- 100% | -3/3 | -100% | -0/0 | -100% | -1/1 | -100% | -3/3 | -
| src/components/common/Modal | -
-
- |
- 100% | -12/12 | -100% | -4/4 | -100% | -4/4 | -100% | -12/12 | -
| src/components/common/Toast | -
-
- |
- 100% | -7/7 | -100% | -0/0 | -100% | -4/4 | -100% | -7/7 | -
| src/components/domain/CreationForm | -
-
- |
- 87.32% | -62/71 | -85.71% | -12/14 | -84.48% | -49/58 | -86.95% | -60/69 | -
| src/components/domain/Postcard | -
-
- |
- 69.23% | -72/104 | -58.9% | -43/73 | -64.28% | -18/28 | -72.16% | -70/97 | -
| src/components/domain/Searchbar | -
-
- |
- 91.07% | -51/56 | -83.33% | -30/36 | -78.94% | -15/19 | -90.9% | -50/55 | -
| src/components/domain/TagInput | -
-
- |
- 94.28% | -33/35 | -76.19% | -16/21 | -92.85% | -13/14 | -93.93% | -31/33 | -
| src/components/layout/Header | -
-
- |
- 100% | -1/1 | -100% | -0/0 | -100% | -1/1 | -100% | -1/1 | -
| src/components/layout/HeaderHome | -
-
- |
- 100% | -1/1 | -100% | -0/0 | -100% | -1/1 | -100% | -1/1 | -
| src/components/layout/Sidebar | -
-
- |
- 93.75% | -15/16 | -75% | -3/4 | -100% | -5/5 | -93.75% | -15/16 | -
| src/pages/CommunityPage | -
-
- |
- 83.94% | -115/137 | -68.42% | -39/57 | -88.05% | -59/67 | -85.24% | -104/122 | -
| src/pages/CreateCommunity | -
-
- |
- 81.81% | -27/33 | -90.9% | -20/22 | -71.42% | -5/7 | -81.81% | -27/33 | -
| src/pages/CreateProject | -
-
- |
- 84.44% | -38/45 | -87.09% | -27/31 | -80% | -8/10 | -86.04% | -37/43 | -
| src/pages/EditProfile | -
-
- |
- 87.09% | -27/31 | -55.55% | -10/18 | -66.66% | -4/6 | -93.1% | -27/29 | -
| src/pages/Feed | -
-
- |
- 86.27% | -44/51 | -69.23% | -9/13 | -74.07% | -20/27 | -87.75% | -43/49 | -
| src/pages/Home | -
-
- |
- 100% | -1/1 | -100% | -0/0 | -100% | -1/1 | -100% | -1/1 | -
| src/pages/Login | -
-
- |
- 87.5% | -14/16 | -75% | -3/4 | -50% | -2/4 | -87.5% | -14/16 | -
| src/pages/Profile | -
-
- |
- 92.43% | -110/119 | -82.25% | -51/62 | -89.58% | -43/48 | -94.54% | -104/110 | -
| src/pages/ProjectPage | -
-
- |
- 90% | -18/20 | -75% | -6/8 | -100% | -4/4 | -94.11% | -16/17 | -
| src/pages/Register | -
-
- |
- 87.5% | -14/16 | -75% | -3/4 | -60% | -3/5 | -87.5% | -14/16 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 | - - - - - - - - - - - - -1x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | import api from './api';
-import { isAxiosError } from 'axios';
-export interface CommentProps {
- projectTitle?: string;
- commentID?: string;
- content: string;
- createdAt?: string;
- authorID?: string;
- projectID?: string;
- username?: string;
- fullName?: string;
-}
-
-const getAuthHeader = () => {
- return {
- headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
- }
- };
-};
-
-export async function CreateComment(projectId: string, content: string) {
- try{
-
- const response = await api.post(`/api/project/${projectId}/comments`,
- {content},
- getAuthHeader()
- );
-
- return response.data;
-
- }catch(error){
-
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao enviar comentário.");
-
- }
-}
-
-export async function GetComments(projectId: string): Promise<CommentProps[]> {
- try{
-
- const response = await api.get(`/api/project/${projectId}/comments`, getAuthHeader());
-
- return await response.data;
- }catch(error){
- console.error("Erro ao buscar comentários:", error);
- throw new Error("Erro ao carregar comentários.");
- }
-
-
-}
-
-export async function GetUserComments(): Promise<CommentProps[]> {
- try{
- const response = await api.get('/api/user/comments', getAuthHeader());
-
- return response.data;
- }catch(error){
- console.error("Erro ao buscar comentários do usuário:", error);
- throw new Error("Erro ao carregar comentários do usuário.");
- }
-}
-
-export async function DeleteComment(commentId: string) {
- try{
-
- const response = await api.delete(`/api/project/${commentId}/deletecomment`, getAuthHeader());
-
- return response.data;
- }catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao excluir comentário.");
- }
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 | - - - - - - - - - - - - - - -2x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | import api from './api';
-import { isAxiosError } from 'axios';
-
-export interface CommunityProps {
- communityID: string;
- name: string;
- description: string;
- technologies: string[];
- createdAt: Date;
- updatedAt: Date;
- memberCount?: number;
- isMember?: boolean;
- isAdmin?: boolean;
-}
-
-const getAuthHeader = () => {
- return {
- headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
- }
- };
-};
-
-export async function NewCommunity(data: CommunityProps) {
-
- try{
- console.log("Enviando dados da comunidade:", data);
- const response = await api.post('/api/newcommunity', data, getAuthHeader());
- return response.data
- }catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao criar comunidade.");
- }
-}
-
-export async function GetUserCommunities(): Promise<CommunityProps[]> {
- try{
- const response = await api.get<CommunityProps[]>('/api/user/communities', getAuthHeader());
- return response.data
- }catch(error){
- console.error("Erro ao obter comunidades:", error);
- throw new Error('Erro ao obter comunidades');
- }
-}
-
-export async function GetCommunityById(communityId: string) {
- try{
- const response = await api.get(`/api/communities/data/${communityId}`, getAuthHeader());
-
- return await response.data;
- }catch(error){
- if (isAxiosError(error) && error.response?.status === 404) {
- throw new Error('Comunidade não encontrada');
- }
- throw new Error('Erro ao carregar a comunidade');
- }
-}
-
-export async function JoinCommunity(communityId: string) {
- try{
- const response = await api.post(`/api/communities/${communityId}/join`, {}, getAuthHeader());
-
- return await response.data;
- }catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error('Erro ao entrar na comunidade');
- }
-}
-
-export async function DeleteCommunity(communityId: string) {
- try{
- const response = await api.delete(`/api/communities/deletecommunity/${communityId}`, getAuthHeader());
- return response.data
- }catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao excluir comunidade.");
- }
-}
-
-export async function UpdateCommunity(communityId: string, data: CommunityProps) {
- try{
- const response = await api.put(`/api/communities/updatecommunity/${communityId}`,
- data,
- getAuthHeader()
- );
-
- return response.data;
- }catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao atualizar comunidade.");
- }
-}
-
-export async function LeaveCommunity(communityId: string) {
- try{
- const response = await api.delete(`/api/user/leavecommunity/${communityId}`, getAuthHeader());
-
- return await response.data;
- }catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao sair da comunidade.");
- }
-}
-
-export async function GetAllCommunities(): Promise<CommunityProps[]> {
- try{
- const response = await api.get<CommunityProps[]>('/api/communities', getAuthHeader());
- return response.data
- }catch(error){
- console.error("Erro ao obter todas as comunidades:", error);
- throw new Error('Erro ao obter todas as comunidades');
- }
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 | - - - - - - - - - - - - - - - - -2x - - -2x - - -4x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | import api from './api';
-import { isAxiosError } from 'axios';
-
-export interface ProjectProps {
- id?: string;
- title: string;
- description: string;
- technologies: string[];
- status: string;
- startDate: Date;
- authorUsername?: string;
- authorName?: string;
- creatorID?: number;
-}
-
-export function parseDate(dataString: string): Date {
- // Divide a string "20/11/2025" em partes
- const [dia, mes, ano] = dataString.split('/');
-
- // Cria a data: new Date(ano, mês - 1, dia)
- return new Date(Number(ano), Number(mes) - 1, Number(dia));
-}
-
-const getAuthHeader = () => {
- return {
- headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
- }
- };
-};
-
-export async function NewProject(data: ProjectProps) {
- try{
- const response = await api.post('/api/user/newproject', data, getAuthHeader());
-
- return response.data;
- }catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao criar projeto.");
- }
-}
-
-export async function UpdateProject(projectId: string, data: ProjectProps) {
- try{
- const response = await api.put(`/api/user/updateproject/${projectId}`,
- data,
- getAuthHeader()
- );
- return response.data;
- }catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao atualizar projeto.");
- }
-
-}
-
-export async function GetFeedProjects(): Promise<ProjectProps[]> {
-
- try{
- const response = await api.get('/api/user/home', getAuthHeader());
-
- console.log("Dados do feed de projetos:", response.data);
-
- return response.data.feed;
- }catch(error){
- console.error("Erro ao buscar feed:", error);
- throw new Error("Erro ao carregar o feed de projetos.");
- }
-}
-
-export async function DeleteProject(projectId: string) {
- try{
- await api.delete(`/api/user/deleteproject/${projectId}`, getAuthHeader());
- }
- catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao excluir projeto.");
- }
-}
-
-export async function GetUserProjects(): Promise<ProjectProps[]> {
- try{
- const response = await api.get('/api/user/projects', getAuthHeader());
-
- console.log("Dados dos projetos do usuário:", response.data.projects);
-
- return response.data.projects;
- }catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao buscar projetos do usuário.");
- }
-}
-
-export async function GetProjectById(projectId: string): Promise<ProjectProps> {
- try{
- const response = await api.get(`/api/projects/${projectId}`, getAuthHeader());
-
- return await response.data;
- }catch(error){
- console.error("Erro ao buscar projeto:", error);
- throw new Error('Erro ao carregar projeto');
- }
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 | - -5x - - - - | import axios from 'axios';
-
-const api = axios.create({
- baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000',
-});
-
-export default api; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| Comment.ts | -
-
- |
- 4.16% | -1/24 | -0% | -0/8 | -0% | -0/5 | -4.16% | -1/24 | -
| Community.ts | -
-
- |
- 2.04% | -1/49 | -0% | -0/24 | -0% | -0/9 | -2.04% | -1/49 | -
| Project.ts | -
-
- |
- 7.69% | -3/39 | -0% | -0/16 | -12.5% | -1/8 | -7.69% | -3/39 | -
| api.ts | -
-
- |
- 100% | -1/1 | -100% | -2/2 | -100% | -0/0 | -100% | -1/1 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 | - - - - - - - -2x - - - - - - - - - - - | import { ThemeProvider } from "styled-components"
-import { defaultTheme } from './styles/themes/default'
-import { GlobalStyle } from './styles/global'
-import { Router } from "./Router"
-import { BrowserRouter } from "react-router-dom"
-
-function App() {
-
- return (
- <ThemeProvider theme={defaultTheme}>
- <BrowserRouter>
- <GlobalStyle/>
- <Router />
- </BrowserRouter>
- </ThemeProvider>
- )
-}
-
-export default App
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 100% | -3/3 | -100% | -0/0 | -100% | -1/1 | -100% | -3/3 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 | - - - - - - - - -1x -2x - - - - - - - - - -1x | import React from 'react';
-import * as S from './styles';
-
-// Props que o componente Keyword aceita
-interface KeywordProps {
- children: React.ReactNode;
- onRemove: () => void;
-}
-
-export const Keyword: React.FC<KeywordProps> = ({ children, onRemove }) => {
- return (
- <S.KeywordTag>
- {children}
- <S.KeywordRemoveButton onClick={onRemove} aria-label={`Remover ${children}`}>
- ×
- </S.KeywordRemoveButton>
- </S.KeywordTag>
- );
-};
-
-export const KeywordContainer = S.KeywordContainer; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 100% | -12/12 | -100% | -4/4 | -100% | -4/4 | -100% | -12/12 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 | - - - - - - - - - - -1x - -5x -5x -4x - -1x - - -5x -5x - - - -5x -1x - - - -4x -2x - - -4x - - - - - - - - - - - - | import React, { useEffect } from 'react';
-import { createPortal } from 'react-dom';
-import * as S from './styles';
-
-interface ModalProps {
- isOpen: boolean;
- onClose: () => void;
- children: React.ReactNode;
- title: string;
-}
-
-const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children }) => {
- // Bloqueia o scroll do body quando o modal está aberto
- useEffect(() => {
- if (isOpen) {
- document.body.style.overflow = 'hidden';
- } else {
- document.body.style.overflow = 'unset';
- }
-
- return () => {
- document.body.style.overflow = 'unset';
- };
- }, [isOpen]);
-
- if (!isOpen) {
- return null;
- }
-
- // Impede que o clique no modal feche o modal (só o overlay)
- const handleContentClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- };
-
- return createPortal(
- <S.ModalOverlay onClick={onClose}>
- <S.ModalContent onClick={handleContentClick}>
- <S.CloseButton onClick={onClose}>×</S.CloseButton>
- <S.ModalTitle>{title}</S.ModalTitle>
- {children}
- </S.ModalContent>
- </S.ModalOverlay>,
- document.body
- );
-};
-
-export default Modal; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 100% | -7/7 | -100% | -0/0 | -100% | -4/4 | -100% | -7/7 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 | - - - - - - - - - - - - - -1x - - -4x -4x -1x - - - -4x -4x - - - -4x - - - - - - - - - - | import React, { useEffect } from 'react';
-import { ToastContainer, ToastMessage, CloseButton } from './styles';
-
-interface ToastProps {
- message: string;
- type: 'success' | 'error';
- onClose: () => void; // Função para fechar o toast
-}
-
-export interface NotificationState {
- message: string;
- type: 'success' | 'error';
-}
-
-const Toast: React.FC<ToastProps> = ({ message, type, onClose }) => {
-
- // Efeito para fechar o toast automaticamente após 5 segundos
- useEffect(() => {
- const timer = setTimeout(() => {
- onClose();
- }, 5000);
-
- // Limpa o timer se o componente for desmontado
- return () => {
- clearTimeout(timer);
- };
- }, [onClose]);
-
- return (
- <ToastContainer type={type}>
- <ToastMessage>{message}</ToastMessage>
- <CloseButton onClick={onClose}>
- ×
- </CloseButton>
- </ToastContainer>
- );
-};
-
-export default Toast; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| styles.ts | -
-
- |
- 87.32% | -62/71 | -85.71% | -12/14 | -84.48% | -49/58 | -86.95% | -60/69 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 | - - -2x -20x - - - - - - - -20x - - - - - - - - - - - - - - - - -2x - - - - - - - - - - - - - - - - - - - -2x - - - -86x - - - - - - - - - - - - - - - - - -86x - - - - - - - - - - - -2x - - - - -33x - - -33x - - - - - -33x - - - - -33x -33x - - - - -33x -33x -33x - - - - - -2x - - - - - - - -13x - - - - - - - - - -13x - - - -13x - - - - - -2x - - - - -13x - - -13x - - - - - - - - - - - - -13x -13x - - - - -13x -13x -13x - - - - - -13x - - - - -13x -13x - - - - -2x - - - - - -2x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -2x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -2x - - - - - - - - - - - - - - - - - -2x - - - -20x -20x - - - - - -20x - - - - - - - - - - - - - - - - - - - - - -20x - - - - - - - - - - -20x - - - - - - - - -2x - - - - - - -20x - - -20x - - - - - - - -20x - - - - -20x -20x - - - - -20x -20x -20x - - - - - - - - - - -20x - - - - -20x - - - - -20x - - - - -2x - - -20x - - - - -20x - - - - - - -20x -20x - - - -20x -20x - - | import styled from 'styled-components';
-
-// O <form> container
-export const FormContainer = styled.form`
- background: ${props => props.theme.white};
- border-radius: 24px;
- padding: 40px;
- width: 100%;
- max-width: 800px;
- margin: 0 auto;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08),
- 0 2px 8px rgba(0, 0, 0, 0.04);
- border: 1px solid ${props => props.theme.placeholder}20;
-
- animation: slideUp 0.5s ease-out;
-
- @keyframes slideUp {
- from {
- opacity: 0;
- transform: translateY(20px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-`;
-
-// Wrapper para cada par de (label + input)
-export const InputGroup = styled.div`
- margin-bottom: 28px;
- width: 100%;
-
- animation: fadeIn 0.4s ease-out backwards;
- animation-delay: calc(var(--index, 0) * 0.05s);
-
- @keyframes fadeIn {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-`;
-
-// Label
-export const Label = styled.label`
- display: block;
- font-size: 0.95em;
- font-weight: 700;
- color: ${props => props.theme.title};
- margin-bottom: 10px;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- font-size: 0.85rem;
- padding-left: 4px;
-
- /* Indicador visual opcional */
- position: relative;
-
- &::after {
- content: '';
- position: absolute;
- left: -12px;
- top: 50%;
- transform: translateY(-50%);
- width: 3px;
- height: 14px;
- background: linear-gradient(180deg, ${props => props.theme.button} 0%, ${props => props.theme['hover-button'] || props.theme.button} 100%);
- border-radius: 2px;
- opacity: 0;
- transition: opacity 0.2s ease;
- }
-
- ${InputGroup}:focus-within &::after {
- opacity: 1;
- }
-`;
-
-// Input
-export const Input = styled.input`
- width: 100%;
- padding: 14px 20px;
- font-size: 1em;
-
- background-color: ${props => props.theme['gray-100']};
- border: 2px solid transparent;
- border-radius: 12px;
- color: ${props => props.theme.title};
- box-sizing: border-box;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- font-weight: 500;
-
- &::placeholder {
- color: ${props => props.theme.placeholder};
- font-weight: 400;
- }
-
- &:hover {
- background-color: ${props => props.theme.white};
- border-color: ${props => props.theme.placeholder}40;
- }
-
- &:focus {
- outline: none;
- background-color: ${props => props.theme.white};
- border-color: ${props => props.theme.button};
- box-shadow: 0 0 0 4px ${props => props.theme.button}15,
- 0 2px 8px rgba(0, 0, 0, 0.04);
- transform: translateY(-1px);
- }
-`;
-
-export const SelectWrapper = styled.div`
- position: relative;
- width: 100%;
-
- /* A seta de seleção (CSS) */
- &::after {
- content: '▼';
- font-size: 0.85em;
- color: ${props => props.theme.placeholder};
- position: absolute;
- right: 20px;
- top: 50%;
- transform: translateY(-50%);
- pointer-events: none;
- transition: all 0.3s ease;
- }
-
- &:hover::after {
- color: ${props => props.theme.button};
- }
-
- &:focus-within::after {
- color: ${props => props.theme.button};
- transform: translateY(-50%) rotate(180deg);
- }
-`;
-
-// Componente de Select
-export const Select = styled.select`
- width: 100%;
- padding: 14px 20px;
- font-size: 1em;
-
- background-color: ${props => props.theme['gray-100']};
- border: 2px solid transparent;
- border-radius: 12px;
- color: ${props => props.theme.title};
- box-sizing: border-box;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- font-weight: 500;
- cursor: pointer;
-
- appearance: none;
- -webkit-appearance: none;
- -moz-appearance: none;
-
- padding-right: 50px;
-
- &:hover {
- background-color: ${props => props.theme.white};
- border-color: ${props => props.theme.placeholder}40;
- }
-
- &:focus {
- outline: none;
- background-color: ${props => props.theme.white};
- border-color: ${props => props.theme.button};
- box-shadow: 0 0 0 4px ${props => props.theme.button}15,
- 0 2px 8px rgba(0, 0, 0, 0.04);
- transform: translateY(-1px);
- }
-
- &:invalid {
- color: ${props => props.theme.placeholder};
- }
-
- option {
- padding: 10px;
- background: ${props => props.theme.white};
- color: ${props => props.theme.title};
- }
-`;
-
-// Wrapper para o input de busca E a lista de resultados
-export const SearchWrapper = styled.div`
- position: relative;
- width: 100%;
-`;
-
-// Lista de resultados de busca que aparece abaixo do input
-export const SearchResultsList = styled.ul`
- position: absolute;
- top: calc(100% + 4px);
- left: 0;
- right: 0;
- background: ${props => props.theme.white};
- border: 2px solid ${props => props.theme.button}20;
- border-radius: 12px;
- max-height: 240px;
- overflow-y: auto;
- margin: 0;
- padding: 6px;
- z-index: 100;
- list-style: none;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12),
- 0 2px 8px rgba(0, 0, 0, 0.08);
-
- animation: slideDown 0.2s ease-out;
-
- @keyframes slideDown {
- from {
- opacity: 0;
- transform: translateY(-8px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-
- /* Estilização da scrollbar */
- &::-webkit-scrollbar {
- width: 8px;
- }
-
- &::-webkit-scrollbar-track {
- background: transparent;
- }
-
- &::-webkit-scrollbar-thumb {
- background: ${props => props.theme['gray-300']};
- border-radius: 4px;
- }
-
- &::-webkit-scrollbar-thumb:hover {
- background: ${props => props.theme['gray-400']};
- }
-`;
-
-// Item individual na lista de resultados
-export const SearchResultItem = styled.li`
- padding: 12px 16px;
- cursor: pointer;
- border-radius: 8px;
- color: ${props => props.theme.title};
- font-weight: 500;
- transition: all 0.2s ease;
- position: relative;
-
- /* Barra lateral que aparece no hover */
- &::before {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 3px;
- background: ${props => props.theme.button};
- border-radius: 0 2px 2px 0;
- transform: scaleY(0);
- transition: transform 0.2s ease;
- }
-
- &:hover {
- background-color: ${props => props.theme['gray-100']};
- padding-left: 20px;
-
- &::before {
- transform: scaleY(1);
- }
- }
-
- &:active {
- background-color: ${props => props.theme['gray-100']};
- }
-`;
-
-// Mensagem de erro
-export const ErrorMessage = styled.span`
- font-size: 0.85em;
- margin-top: 8px;
- color: ${props => props.theme['red-500'] || '#ef4444'};
- font-weight: 600;
- display: flex;
- align-items: center;
- gap: 6px;
- padding-left: 4px;
-
- /* Ícone de alerta */
- &::before {
- content: '⚠';
- font-size: 1.1em;
- }
-`;
-
-// Botão de submissão
-export const SubmitButton = styled.button`
- padding: 16px 40px;
- font-size: 1.05em;
- font-weight: 700;
- color: ${props => props.theme.white};
- background: linear-gradient(135deg, ${props => props.theme.button} 0%, ${props => props.theme['hover-button'] || props.theme.button} 100%);
- border: none;
- border-radius: 12px;
- cursor: pointer;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- margin-top: 16px;
- box-shadow: 0 4px 16px ${props => props.theme.button}40;
- position: relative;
- overflow: hidden;
-
- /* Efeito de brilho deslizante */
- &::before {
- content: '';
- position: absolute;
- top: 0;
- left: -100%;
- width: 100%;
- height: 100%;
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
- transition: left 0.5s;
- }
-
- &:hover::before {
- left: 100%;
- }
-
- &:hover {
- transform: translateY(-3px);
- box-shadow: 0 8px 24px ${props => props.theme.button}50;
- }
-
- &:active {
- transform: translateY(-1px);
- }
-
- &:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- transform: none;
- box-shadow: 0 2px 8px ${props => props.theme.button}20;
-
- &:hover {
- transform: none;
- }
- }
-`;
-
-// Estilo para o campo de texto de várias linhas (Descrição)
-export const TextArea = styled.textarea`
- width: 100%;
- padding: 16px 20px;
- font-size: 1em;
- font-family: inherit;
- line-height: 1.6;
-
- background-color: ${props => props.theme['gray-100']};
- border: 2px solid transparent;
- border-radius: 12px;
- color: ${props => props.theme.title};
- box-sizing: border-box;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- resize: vertical;
- min-height: 150px;
- font-weight: 500;
-
- &::placeholder {
- color: ${props => props.theme.placeholder};
- font-weight: 400;
- }
-
- &:hover {
- background-color: ${props => props.theme.white};
- border-color: ${props => props.theme.placeholder}40;
- }
-
- &:focus {
- outline: none;
- background-color: ${props => props.theme.white};
- border-color: ${props => props.theme.button};
- box-shadow: 0 0 0 4px ${props => props.theme.button}15,
- 0 2px 8px rgba(0, 0, 0, 0.04);
- transform: translateY(-1px);
- }
-
- /* Estilização da scrollbar */
- &::-webkit-scrollbar {
- width: 10px;
- }
-
- &::-webkit-scrollbar-track {
- background: ${props => props.theme['gray-100']};
- border-radius: 0 10px 10px 0;
- }
-
- &::-webkit-scrollbar-thumb {
- background: ${props => props.theme['gray-300']};
- border-radius: 10px;
- }
-
- &::-webkit-scrollbar-thumb:hover {
- background: ${props => props.theme.button};
- }
-`;
-
-// Contador de caracteres
-export const CharacterCount = styled.div`
- text-align: right;
- font-size: 0.85em;
- color: ${props => props.theme['gray-500'] || props.theme.placeholder};
- margin-top: 6px;
- margin-right: 4px;
- font-weight: 600;
- padding: 4px 8px;
- background: ${props => props.theme['gray-100']};
- border-radius: 6px;
- display: inline-block;
- float: right;
-
- /* Muda de cor quando próximo do limite */
- &[data-warning="true"] {
- color: ${props => props.theme['yellow-600'] || '#d97706'};
- background: ${props => props.theme['yellow-50'] || '#fef3c7'};
- }
-
- &[data-limit="true"] {
- color: ${props => props.theme['red-500'] || '#ef4444'};
- background: ${props => '#fee2e2'};
- }
-`; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 69.23% | -72/104 | -58.9% | -43/73 | -64.28% | -18/28 | -72.16% | -70/97 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 | - - - - - - - - - - - - - -1x - - - - - - - -1x -21x - - - - - - -1x -12x - - - - - - - - - - - - - -21x - -21x -21x -21x -21x -21x - - -21x -21x -21x -21x -21x - -21x - -21x - -21x -10x -2x - - - - -2x -2x -2x - - - - - - -21x - - - - - -13x -4x - -13x -13x - - - -21x -1x - -1x -1x - - - - - -21x -2x -2x - -2x - -2x - - -2x -1x - - -1x -1x - - -1x - - - -21x -1x - -1x - - - - -1x - -1x -1x -1x - -1x -1x -1x - - - - - - -1x - - - -21x - - - - - - - - - - - - -21x - -12x - - -12x - - -12x - - - - - - -12x - - - - - - - - -21x -1x - - - - - -21x - - - - - - - - - - - - - - - - - - -4x - - - - - - - - - -2x -2x - - - - - - - - - - -2x - - - - - - - - - - - -1x - - - - - - - - - - - - - - - - - - - - - -1x - -1x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | import React, { useState, useRef, useEffect } from 'react';
-import * as S from './styles';
-import * as D from '../../common/Dropdown/styles';
-import { useNavigate } from 'react-router-dom';
-import type { ProjectProps } from '../../../API/Project';
-import { DeleteProject } from '../../../API/Project';
-import Modal from '../../common/Modal';
-import type { NotificationState } from '../../common/Toast';
-import Toast from '../../common/Toast';
-import * as ModalS from '../../common/Modal/styles';
-import { CreateComment, GetComments, DeleteComment } from '../../../API/Comment';
-import type { CommentProps } from '../../../API/Comment';
-import { useAuth } from '../../../API/AuthContext';
-
-const TrashIcon = () => (
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <polyline points="3 6 5 6 21 6"></polyline>
- <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
- </svg>
-);
-
-// Ícone de Comentário (Balão)
-const CommentIcon = () => (
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
- </svg>
-);
-
-
-// --- Ícone de Menu (Ellipsis) ---
-const EllipsisIcon = () => (
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" />
- </svg>
-);
-
-interface PostcardProps {
- post: ProjectProps;
- showMenu: boolean; // Verifica se o menu deve ser mostrado (se é dono do post)
- onDelete?: (id: string) => Promise<void>;
- deleteLabel?: string;
-}
-
-// --- Componente Postcard ---
-export default function Postcard({ post, showMenu, onDelete, deleteLabel = 'Projeto' }: PostcardProps) {
- const { currentUser } = useAuth();
-
- const [isMenuOpen, setIsMenuOpen] = useState(false);
- const menuRef = useRef<HTMLDivElement>(null);
- const navigate = useNavigate(); // Inicialize o hook de navegação
- const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); // Estado para o modal de exclusão
- const [notification, setNotification] = useState<NotificationState | null>(null);
-
- // Estados para o formulário de comentário
- const [isCommentBoxOpen, setIsCommentBoxOpen] = useState(false);
- const [commentText, setCommentText] = useState("");
- const [isSubmittingComment, setIsSubmittingComment] = useState(false);
- const MAX_COMMENT_CHARS = 500;
- const [comments, setComments] = useState<CommentProps[]>([]);
-
- const [commentToDelete, setCommentToDelete] = useState<string | null>(null);
-
- const projectId = (post as any).id || (post as any).projectID;
-
- useEffect(() => {
- if (isCommentBoxOpen && projectId) {
- loadComments();
- }
- }, [isCommentBoxOpen, projectId]);
-
- async function loadComments() {
- try {
- const data = await GetComments(projectId);
- setComments(data);
- } catch (error) {
- console.error("Erro ao carregar comentários", error);
- }
- }
-
- // Lógica para fechar o menu ao clicar fora (sem alterações)
- useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
- setIsMenuOpen(false);
- }
- }
- if (isMenuOpen) {
- document.addEventListener('mousedown', handleClickOutside);
- }
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, [isMenuOpen]);
-
- const handleEditClick = () => {
- const projectId = (post as any).id || (post as any).projectID;
-
- if (projectId) {
- navigate(`/editProject/${projectId}`, { state: { projectToEdit: post } });
- } else E{
- console.error("Erro: ID do projeto não encontrado no objeto:", post);
- }
- };
-
- const handleDeleteMainItem = async () => {
- try {
- const id = (post as any).id || (post as any).projectID || (post as any).commentID;
-
- Iif (!id) throw new Error("ID não encontrado.");
-
- Iif (onDelete) {
- await onDelete(id);
- } else {
- await DeleteProject(id);
- setTimeout(() => navigate('/feed'), 1000);
- }
-
- setNotification({ message: `${deleteLabel} excluído com sucesso!`, type: 'success' });
- setIsDeleteModalOpen(false);
-
- } catch (error) {
- setNotification({ message: `Erro ao excluir ${deleteLabel?.toLowerCase()}.`, type: 'error' });
- }
- };
-
- const handleCommentSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
-
- Iif (!projectId) {
- setNotification({ message: 'Erro: Projeto sem ID.', type: 'error' });
- return;
- }
-
- Iif (!commentText.trim()) return;
-
- try {
- setIsSubmittingComment(true);
- await CreateComment(projectId, commentText);
-
- setNotification({ message: 'Comentário enviado!', type: 'success' });
- setCommentText(""); // Limpa o campo
- setIsCommentBoxOpen(false);
-
- } catch (error) {
- if (error instanceof Error) {
- setNotification({ message: error.message, type: 'error' });
- }
- } finally {
- setIsSubmittingComment(false);
- }
- };
-
- const handleConfirmDeleteInternalComment = async () => {
- if (!commentToDelete) return;
- try {
- await DeleteComment(commentToDelete);
- setNotification({ message: 'Comentário excluído.', type: 'success' });
- setCommentToDelete(null);
- await loadComments();
- } catch (error: any) {
- setNotification({ message: error.message, type: 'error' });
- setCommentToDelete(null);
- }
- };
-
- const handleCardClick = (e: React.MouseEvent) => {
- // Não navega se estiver na aba de comentários do perfil (deleteLabel 'Comentário')
- Iif (deleteLabel !== 'Projeto') return;
-
- // Não navega se o clique foi em um elemento interativo
- const target = e.target as HTMLElement;
-
- // Verifica se clicou em botões, inputs, links ou no próprio menu
- Eif (
- target.closest('button') ||
- target.closest('input') ||
- target.closest('textarea') ||
- target.closest('a') ||
- target.closest(D.DropdownMenu as any) // Garante que itens do dropdown não disparem
- ) {
- return;
- }
-
- // Realiza a navegação
- if (projectId) {
- navigate(`/project/${projectId}`);
- }
- };
-
- const formatDate = (dateString?: string) => {
- Eif (!dateString) return "";
- return new Date(dateString).toLocaleDateString('pt-BR', {
- day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit'
- });
- };
-
- return (
- <S.PostCardWrapper onClick={handleCardClick}>
- {notification && (
- <Toast
- message={notification.message}
- type={notification.type}
- onClose={() => setNotification(null)}
- />
- )}
-
- <S.PostHeader>
- <img src={(post as any).avatarUrl || 'url_placeholder_avatar.png'} alt={post.title} />
- <span>{post.title}</span>
- <small>• {(post as any).author?.title || 'Autor'}</small>
- </S.PostHeader>
-
- <S.PostContent>
- {showMenu && (
- <S.MenuWrapper ref={menuRef}>
- <S.MenuButton onClick={() => setIsMenuOpen(prev => !prev)}>
- <EllipsisIcon />
- </S.MenuButton>
-
- {isMenuOpen && (
- <D.DropdownMenu>
- {deleteLabel === 'Projeto' && (
- <D.MenuItem onClick={handleEditClick}>Editar</D.MenuItem>
- )}
- <D.DangerMenuItem onClick={() => {
- setIsMenuOpen(false);
- setIsDeleteModalOpen(true);}}>
- Excluir
- </D.DangerMenuItem>
- </D.DropdownMenu>
- )}
- </S.MenuWrapper>
- )}
-
- <p>{post.description}</p>
-
- <S.ActionRow>
- <S.ActionButton onClick={() => setIsCommentBoxOpen(!isCommentBoxOpen)}>
- <CommentIcon />
- <span>Comentar</span>
- </S.ActionButton>
- </S.ActionRow>
-
- {isCommentBoxOpen && (
- <>
- <S.CommentForm onSubmit={handleCommentSubmit}>
- <S.CommentTextArea
- placeholder="Escreva seu comentário..."
- value={commentText}
- onChange={(e) => setCommentText(e.target.value)}
- maxLength={MAX_COMMENT_CHARS}
- disabled={isSubmittingComment}
- />
- <S.CommentFooter>
- <S.CharacterCount isLimit={commentText.length >= MAX_COMMENT_CHARS}>
- {commentText.length} / {MAX_COMMENT_CHARS}
- </S.CharacterCount>
- <S.SubmitCommentButton
- type="submit"
- disabled={isSubmittingComment || !commentText.trim()}
- >
- {isSubmittingComment ? 'Enviando...' : 'Enviar'}
- </S.SubmitCommentButton>
- </S.CommentFooter>
- </S.CommentForm>
-
- {/* LISTA DE COMENTÁRIOS */}
- {comments.length > 0 && (
- <S.CommentsSection>
- {comments.map((comment) => {
- // Verifica se o usuário atual é o autor do comentário
- const isAuthor = currentUser?.id && String(currentUser.id) === String(comment.authorID);
-
- return (
- <S.CommentItem key={comment.commentID || Math.random()}>
- <S.CommentAvatar />
- <S.CommentBubble>
- <S.CommentHeader>
- <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
- <strong>{comment.username || "Usuário"}</strong>
- <span>{formatDate(comment.createdAt)}</span>
- </div>
-
- {/* Botão de Deletar (Só aparece para o dono) */}
- {isAuthor && (
- <S.DeleteCommentButton
- onClick={() => setCommentToDelete(comment.commentID!)}
- title="Excluir comentário"
- >
- <TrashIcon />
- </S.DeleteCommentButton>
- )}
- </S.CommentHeader>
- <S.CommentText>{comment.content}</S.CommentText>
- </S.CommentBubble>
- </S.CommentItem>
- );
- })}
- </S.CommentsSection>
- )}
- </>
- )}
-
- </S.PostContent>
- <Modal
- isOpen={isDeleteModalOpen}
- onClose={() => setIsDeleteModalOpen(false)}
- title={`Excluir ${deleteLabel}`}
- >
- <div style={{ textAlign: 'center' }}>
- <p style={{ marginBottom: '24px', color: '#555' }}>
- Tem certeza que deseja excluir este {deleteLabel?.toLowerCase()}?
- </p>
- <ModalS.ModalActions>
- <ModalS.ChoiceButton onClick={() => setIsDeleteModalOpen(false)}>
- Cancelar
- </ModalS.ChoiceButton>
- <ModalS.ChoiceButton onClick={handleDeleteMainItem} style={{ backgroundColor: '#e74c3c' }}>
- Excluir
- </ModalS.ChoiceButton>
- </ModalS.ModalActions>
- </div>
- </Modal>
-
- <Modal
- isOpen={!!commentToDelete}
- onClose={() => setCommentToDelete(null)}
- title="Excluir Comentário"
- >
- <div style={{ textAlign: 'center' }}>
- <p style={{ marginBottom: '24px', color: '#555' }}>
- Deseja realmente apagar este comentário?
- </p>
- <ModalS.ModalActions>
- <ModalS.ChoiceButton onClick={() => setCommentToDelete(null)}>
- Cancelar
- </ModalS.ChoiceButton>
- <ModalS.ChoiceButton
- onClick={handleConfirmDeleteInternalComment}
- style={{ backgroundColor: '#e74c3c' }}
- >
- Excluir
- </ModalS.ChoiceButton>
- </ModalS.ModalActions>
- </div>
- </Modal>
-
- </S.PostCardWrapper>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 91.07% | -51/56 | -83.33% | -30/36 | -78.94% | -15/19 | -90.9% | -50/55 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 | - - - - - - - - - - -47x -47x - -47x - -47x -47x - - -47x -47x - - -47x -47x - -47x -6x -6x -6x -6x - -6x - - - - -6x -6x - - - -6x - - - - - -47x -20x -8x -8x -8x - - -12x - - -12x -3x - - - - - -12x -3x - - - -12x -12x - - - - -47x - -1x -1x - - -6x -6x - - - -47x -1x -1x -1x - - -47x -1x -1x -1x - - -47x - -47x - - - - - - -7x - - - - - - - - - - - - - - - - - -1x - -1x - - - - - - - - - - - - - - -3x - -1x - - - - - - - - - - - - - - - | import React, { useState, useEffect, useRef } from 'react';
-import { FiSearch } from 'react-icons/fi';
-import * as S from './styles';
-import { useNavigate } from 'react-router-dom';
-
-import { GetAllCommunities } from '../../../API/Community';
-import { GetFeedProjects } from '../../../API/Project';
-import type { CommunityProps } from '../../../API/Community';
-import type { ProjectProps } from '../../../API/Project';
-
-export default function Searchbar() {
- const [query, setQuery] = useState('');
- const navigate = useNavigate();
-
- const wrapperRef = useRef<HTMLDivElement>(null);
-
- const [isFocused, setIsFocused] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
-
- // Dados brutos (cache local)
- const [allCommunities, setAllCommunities] = useState<CommunityProps[]>([]);
- const [allProjects, setAllProjects] = useState<ProjectProps[]>([]);
-
- // Dados filtrados para exibição
- const [filteredCommunities, setFilteredCommunities] = useState<CommunityProps[]>([]);
- const [filteredProjects, setFilteredProjects] = useState<ProjectProps[]>([]);
-
- const handleFocus = async () => {
- setIsFocused(true);
- Eif (allCommunities.length === 0 && allProjects.length === 0) {
- setIsLoading(true);
- try {
- // Busca em paralelo
- const [communitiesData, projectsData] = await Promise.all([
- GetAllCommunities().catch(() => []), // Se falhar, retorna array vazio
- GetFeedProjects().catch(() => [])
- ]);
-
- setAllCommunities(communitiesData);
- setAllProjects(projectsData);
- } catch (error) {
- console.error("Erro ao carregar dados para pesquisa", error);
- } finally {
- setIsLoading(false);
- }
- }
- };
-
- // Efeito para filtrar sempre que a query ou os dados mudarem
- useEffect(() => {
- if (!query.trim()) {
- setFilteredCommunities([]);
- setFilteredProjects([]);
- return;
- }
-
- const lowerQuery = query.toLowerCase();
-
- // Filtra Comunidades (pelo nome ou descrição ou tecnologias)
- const filteredComms = allCommunities.filter(comm =>
- comm.name.toLowerCase().includes(lowerQuery) ||
- comm.description?.toLowerCase().includes(lowerQuery) ||
- comm.technologies?.some(tech => tech.toLowerCase().includes(lowerQuery))
- );
-
- // Filtra Projetos (pelo título ou tecnologias)
- const filteredProjs = allProjects.filter(proj =>
- proj.title.toLowerCase().includes(lowerQuery) ||
- proj.technologies?.some(tech => tech.toLowerCase().includes(lowerQuery))
- );
-
- setFilteredCommunities(filteredComms.slice(0, 5)); // Limita a 5 resultados
- setFilteredProjects(filteredProjs.slice(0, 5)); // Limita a 5 resultados
-
- }, [query, allCommunities, allProjects]);
-
- // Fecha ao clicar fora
- useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- Eif (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
- setIsFocused(false);
- }
- }
- document.addEventListener('mousedown', handleClickOutside);
- return () => document.removeEventListener('mousedown', handleClickOutside);
- }, []);
-
- // Navegação
- const handleNavigateToCommunity = (communityId: string) => {
- navigate(`/r/${communityId}`);
- setIsFocused(false);
- setQuery('');
- };
-
- const handleNavigateToProject = (projectId: string) => {
- navigate(`/project/${projectId}`);
- console.log("Navegar para projeto:", projectId);
- setIsFocused(false);
- };
-
- const hasResults = filteredCommunities.length > 0 || filteredProjects.length > 0;
-
- return (
- <S.SearchWrapper ref={wrapperRef}>
- <FiSearch size={20} />
- <S.SearchInput
- type="text"
- placeholder="Busque comunidades e projetos..."
- value={query}
- onChange={(e) => setQuery(e.target.value)}
- onFocus={handleFocus}
- />
-
- {/* Renderiza o dropdown apenas se estiver focado e houver texto */}
- {isFocused && query.length > 0 && (
- <S.ResultsDropdown>
- {isLoading ? (
- <S.NoResults>Carregando...</S.NoResults>
- ) : !hasResults ? (
- <S.NoResults>Nenhum resultado encontrado.</S.NoResults>
- ) : (
- <>
- {/* Seção de Comunidades */}
- {filteredCommunities.length > 0 && (
- <S.ResultSection>
- <h4>Comunidades</h4>
- {filteredCommunities.map(comm => (
- <S.ResultItem
- key={comm.communityID}
- onClick={() => handleNavigateToCommunity(comm.communityID)}
- >
- <span>{comm.name}</span>
- {/* Opcional: mostrar tecnologia principal */}
- {comm.technologies?.[0] && <small>{comm.technologies[0]}</small>}
- </S.ResultItem>
- ))}
- </S.ResultSection>
- )}
-
- {/* Seção de Projetos */}
- {filteredProjects.length > 0 && (
- <S.ResultSection>
- <h4>Projetos</h4>
- {filteredProjects.map((proj: any) => (
- <S.ResultItem
- key={proj.id || proj.projectID}
- onClick={() => handleNavigateToProject(proj.id || proj.projectID)}
- >
- <span>{proj.title}</span>
- <small>por {proj.authorUsername || 'Usuário'}</small>
- </S.ResultItem>
- ))}
- </S.ResultSection>
- )}
- </>
- )}
- </S.ResultsDropdown>
- )}
- </S.SearchWrapper>
- );
-}
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 94.28% | -33/35 | -76.19% | -16/21 | -92.85% | -13/14 | -93.93% | -31/33 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 | - - - - - - - - - - - - - - - - - - - - - -20x -20x -20x -20x - -20x -9x -3x -11x - - -3x - -6x - - - - -20x - - - - - -5x -5x -5x - - - - -20x - -2x -1x -1x - -1x -1x -1x -1x -1x - - - - -20x -2x -1x - - -20x - - - - -18x - -1x - - - - - - - - - - - - - -3x - - - - - - -4x - - - - - - - - - - | import React, { useState, useEffect, useRef } from 'react';
-import * as S from '../CreationForm/styles';
-import { Keyword, KeywordContainer } from '../../common/Keyword';
-
-interface TagInputProps {
- value: string[];
- onChange: (value: string[]) => void;
-
- searchList: string[];
- limit: number;
- placeholder: string;
-}
-
-
-export default function TagInput({
- value: selectedTags = [],
- onChange,
- searchList,
- limit,
- placeholder
-}: TagInputProps) {
-
- const [searchQuery, setSearchQuery] = useState('');
- const [searchResults, setSearchResults] = useState<string[]>([]);
- const [error, setError] = useState('');
- const wrapperRef = useRef<HTMLDivElement>(null);
-
- useEffect(() => {
- if (searchQuery.length > 0) {
- const filtered = searchList.filter(item =>
- item.toLowerCase().includes(searchQuery.toLowerCase()) &&
- !selectedTags.includes(item) // Não mostra o que já foi selecionado
- );
- setSearchResults(filtered);
- } else {
- setSearchResults([]);
- }
- }, [searchQuery, selectedTags, searchList]);
-
- // Lógica para fechar a lista de resultados ao clicar fora
- useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
- setSearchResults([]); // Fecha a lista
- }
- }
- document.addEventListener('mousedown', handleClickOutside);
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, [wrapperRef]);
-
- // Adiciona uma tag
- const handleAddTag = (tag: string) => {
- // Usa o 'limit' da prop
- if (selectedTags.length >= limit) {
- setError(`Limite de ${limit} itens atingido.`);
- return;
- }
- Eif (!selectedTags.includes(tag)) {
- onChange([...selectedTags, tag]); // Atualiza o react-hook-form
- setSearchQuery('');
- setSearchResults([]);
- setError('');
- }
- };
-
- // Remove uma tag
- const handleRemoveTag = (tagToRemove: string) => {
- onChange(selectedTags.filter(tag => tag !== tagToRemove));
- setError('');
- };
-
- return (
- <S.SearchWrapper ref={wrapperRef}>
- {selectedTags.length > 0 && (
- <KeywordContainer>
- {selectedTags.map(tag => (
- <Keyword
- key={tag}
- onRemove={() => handleRemoveTag(tag)}
- >
- {tag}
- </Keyword>
- ))}
- </KeywordContainer>
- )}
-
- <div style={{ height: selectedTags.length > 0 ? '12px' : '0' }} />
-
- <S.Input
- type="text"
- placeholder={placeholder}
- value={searchQuery}
- onChange={(e) => setSearchQuery(e.target.value)}
- disabled={selectedTags.length >= limit}
- />
-
- {searchResults.length > 0 && (
- <S.SearchResultsList>
- {searchResults.map(tag => (
- <S.SearchResultItem key={tag} onClick={() => handleAddTag(tag)}>
- {tag}
- </S.SearchResultItem>
- ))}
- </S.SearchResultsList>
- )}
-
- {error && <S.ErrorMessage>{error}</S.ErrorMessage>}
- </S.SearchWrapper>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 100% | -1/1 | -100% | -0/0 | -100% | -1/1 | -100% | -1/1 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 | - - - - - - - - - -2x - - - - - - - - - - - - - - - - - | import { FiPlus } from 'react-icons/fi';
-import Searchbar from '../../domain/Searchbar';
-import * as S from './styles';
-
-
-interface HeaderProps {
- onCreateClick: () => void;
-}
-
-export default function Header({ onCreateClick }: HeaderProps) {
- return (
- <S.HeaderContainer>
- <S.SearchContainer>
- <Searchbar />
- </S.SearchContainer>
-
- <S.ActionsContainer>
- <S.CreateButton onClick={onCreateClick}>
- <FiPlus size={20} />
- <span>Create</span>
- </S.CreateButton>
-
- <S.ProfileIcon to={`/profile`}>
- </S.ProfileIcon>
- </S.ActionsContainer>
- </S.HeaderContainer>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 100% | -1/1 | -100% | -0/0 | -100% | -1/1 | -100% | -1/1 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 | - - -1x - - - - - - - - - - - - - - | import * as S from './styles';
-
-export default function HeaderHome() {
- return (
- <S.HeaderContainer>
-
- <S.ActionsContainer>
- <S.Button to="/login">
- Login
- </S.Button>
-
- <S.Button to="/register">
- Register
- </S.Button>
- </S.ActionsContainer>
- </S.HeaderContainer>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 93.75% | -15/16 | -75% | -3/4 | -100% | -5/5 | -93.75% | -15/16 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 | - - - - - - - - - -9x -9x -9x - -9x -2x - - -9x - -8x -8x - -8x -8x -8x - - - - - - -8x -8x - - -9x - - - - - - - - - - - - - - - - -4x - - - - - - - - - - - - - - - - - - | import { useState } from 'react';
-import { FiHome, FiChevronDown } from 'react-icons/fi';
-import { GetUserCommunities } from '../../../API/Community';
-import * as S from './styles';
-import type { CommunityProps } from '../../../API/Community';
-import { useAuth } from '../../../API/AuthContext';
-import { useEffect } from 'react';
-
-export default function Sidebar() {
- // Estado para controlar se a lista de comunidades está visível
- const [isCommunitiesOpen, setIsCommunitiesOpen] = useState(true);
- const { currentUser } = useAuth();
- const [userCommunities, setUserCommunities] = useState<CommunityProps[]>([]);
-
- const toggleCommunities = () => {
- setIsCommunitiesOpen(!isCommunitiesOpen);
- };
-
- useEffect(() => {
- // Função assíncrona para buscar todos os dados
- const fetchCommunities = async () => {
- try {;
-
- const apiUserCommunities = await GetUserCommunities();
- console.log("Comunidades do usuário:", apiUserCommunities);
- setUserCommunities(apiUserCommunities);
-
- } catch (error) {
- console.error("Falha ao buscar comunidades:", error);
- }
-
- };
- Eif(currentUser)
- fetchCommunities();
- }, [currentUser]);
-
- return (
- <S.SidebarContainer>
- <S.SidebarNav>
-
- <S.HomeLink to="/feed">
- <FiHome size={22} />
- <span>Home</span>
- </S.HomeLink>
-
- <S.CommunitiesHeader onClick={toggleCommunities} isOpen={isCommunitiesOpen}>
- <span>COMUNIDADES</span>
- <FiChevronDown size={20} />
- </S.CommunitiesHeader>
-
- {isCommunitiesOpen && (
- <S.CommunitiesList>
- {userCommunities.map((community) => (
- <S.CommunityLink
- to={`/r/${community.communityID}`}
- key={community.communityID}
- >
- <S.CommunityIcon
- alt={`${community.name} icon`}
- />
- <span>{community.name}</span>
- </S.CommunityLink>
- ))}
- </S.CommunitiesList>
- )}
-
- </S.SidebarNav>
- </S.SidebarContainer>
- );
-}
-
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| App.tsx | -
-
- |
- 100% | -1/1 | -100% | -0/0 | -100% | -1/1 | -100% | -1/1 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 | - - - - - - - - - - - - - - -14x -14x - -14x -14x -14x - -14x -14x -14x - -14x - -14x - -4x -4x -4x -4x -4x -4x - - - - -4x - - -14x - - - - - -6x -6x - - -14x -1x - -1x -1x - - -1x - - - - - -1x - - - - - - - - -14x - - - -14x -1x -1x -1x -1x -1x - - -1x - - - - - - - - - - - -14x -1x - -1x -1x - - -1x - - - - - -1x -1x - - - - - - - - - -14x - -10x - - - - - - - - - - - - - - - - - - - - - - - - - - - - -1x - - - - - -1x - - - - - - - - - - -1x -1x - - - - - - - - - - - - - -10x - - - - - - - - - - - - -10x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | import { useParams, useNavigate } from 'react-router-dom';
-import Sidebar from '../../components/layout/Sidebar';
-import Postcard from '../../components/domain/Postcard';
-import * as S from './styles';
-import { FiMoreHorizontal } from 'react-icons/fi';
-import { useEffect, useState, useRef } from 'react';
-import { GetCommunityById, JoinCommunity, DeleteCommunity, LeaveCommunity } from '../../API/Community';
-import type { NotificationState } from '../../components/common/Toast';
-import Toast from '../../components/common/Toast';
-import type { CommunityProps } from '../../API/Community';
-import * as D from '../../components/common/Dropdown/styles';
-import Modal from '../../components/common/Modal';
-import * as ModalS from '../../components/common/Modal/styles';
-
-export default function CommunityPage() {
- const { communityID } = useParams<{ communityID: string }>();
- const navigate = useNavigate();
-
- const [notification, setNotification] = useState<NotificationState | null>(null);
- const [community, setCommunity] = useState<any>(null);
- const [posts, setPosts] = useState<any[]>([]);
-
- const [isMenuOpen, setIsMenuOpen] = useState(false);
- const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
- const menuRef = useRef<HTMLDivElement>(null);
-
- const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false);
-
- useEffect(() => {
- async function loadData() {
- console.log("Carregando dados da comunidade para ID:", communityID);
- Iif (!communityID) return;
- try {
- const data = await GetCommunityById(communityID);
- setCommunity(data.community);
- setPosts(data.posts);
- } catch (error) {
- console.error(error);
- }
- }
- loadData();
- }, [communityID]);
-
- useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
- setIsMenuOpen(false);
- }
- }
- if (isMenuOpen) document.addEventListener('mousedown', handleClickOutside);
- return () => document.removeEventListener('mousedown', handleClickOutside);
- }, [isMenuOpen]);
-
- const handleJoin = async () => {
- Iif (!community) return;
-
- try {
- await JoinCommunity(community.communityID);
-
- // Atualiza a interface após entrar na comunidade
- setCommunity((prev: CommunityProps | null) => prev ? ({
- ...prev,
- isMember: true, // Esconde o botão
- memberCount: (prev.memberCount || 0) + 1 // Atualiza o contador visualmente
- }) : null);
-
- setNotification({ message: 'Você entrou na comunidade!', type: 'success' });
-
- } catch (error) {
- if (error instanceof Error) {
- setNotification({ message: error.message, type: 'error' });
- }
- }
- };
-
- const handleEdit = () => {
- navigate('/editCommunity', { state: { communityToEdit: community } });
- };
-
- const handleDelete = async () => {
- Iif (!community) return;
- try {
- await DeleteCommunity(community.communityID);
- setNotification({ message: 'Comunidade excluída com sucesso.', type: 'success' });
- setIsDeleteModalOpen(false);
-
- // Redireciona para home após excluir
- setTimeout(() => {
- navigate('/feed');
- }, 1500);
-
- } catch (error) {
- if (error instanceof Error) {
- setNotification({ message: error.message, type: 'error' });
- }
- setIsDeleteModalOpen(false);
- }
- };
-
- const handleLeave = async () => {
- Iif (!community) return;
-
- try {
- await LeaveCommunity(community.communityID);
-
- // Atualiza a interface otimisticamente
- setCommunity((prev: CommunityProps | null) => prev ? ({
- ...prev,
- isMember: false, // O usuário não é mais membro
- memberCount: Math.max((prev.memberCount || 0) - 1, 0) // Decrementa contador
- }) : null);
-
- setNotification({ message: 'Você saiu da comunidade.', type: 'success' });
- setIsLeaveModalOpen(false); // Fecha o modal
-
- } catch (error) {
- if (error instanceof Error) {
- setNotification({ message: error.message, type: 'error' });
- }
- setIsLeaveModalOpen(false);
- }
- };
-
- if (!community) return <div>Comunidade não encontrada</div>;
-
- return (
- <S.PageWrapper>
- <Sidebar />
-
- {notification && (
- <Toast
- message={notification.message}
- type={notification.type}
- onClose={() => setNotification(null)}
- />
- )}
-
- <S.MainContent>
- <S.Banner />
-
- <S.HeaderContainer>
- <S.Avatar />
- <S.HeaderInfo>
- <h1>{community.name}</h1>
- <span>{community.memberCount} membros</span>
- </S.HeaderInfo>
-
- <S.HeaderActions>
- {!community.isMember && (
- <S.JoinButton onClick={handleJoin}>
- Join
- </S.JoinButton>
- )}
- {community.isMember && !community.isAdmin && (
- <S.LeaveButton onClick={() => setIsLeaveModalOpen(true)}>
- Sair
- </S.LeaveButton>
- )}
- {community.isAdmin && (
- <S.MenuWrapper ref={menuRef}>
- <S.OptionsButton onClick={() => setIsMenuOpen(!isMenuOpen)}>
- <FiMoreHorizontal />
- </S.OptionsButton>
-
- {isMenuOpen && (
- <D.DropdownMenu>
- <D.MenuItem onClick={handleEdit}>
- Editar Comunidade
- </D.MenuItem>
- <D.Separator />
- <D.DangerMenuItem onClick={() => {
- setIsMenuOpen(false);
- setIsDeleteModalOpen(true);
- }}>
- Excluir Comunidade
- </D.DangerMenuItem>
- </D.DropdownMenu>
- )}
- </S.MenuWrapper>
- )}
- </S.HeaderActions>
- </S.HeaderContainer>
-
- <S.ContentGrid>
- <S.FeedColumn>
- {posts.map(post => (
- <S.FeedCardWrapper key={post.projectID || post.id}>
- <Postcard post={post} showMenu={false} />
- </S.FeedCardWrapper>
- ))}
- </S.FeedColumn>
-
- <S.InfoSidebar>
- <h3>DESCRIPTION</h3>
- <p>{community.description}</p>
-
- <h3>KEYWORDS</h3>
- <S.KeywordsContainer>
- {community.technologies?.map((keyword: string) => (
- <S.KeywordTag key={keyword}>{keyword}</S.KeywordTag>
- ))}
- </S.KeywordsContainer>
- </S.InfoSidebar>
- </S.ContentGrid>
- </S.MainContent>
-
- <Modal
- isOpen={isDeleteModalOpen}
- onClose={() => setIsDeleteModalOpen(false)}
- title="Excluir Comunidade"
- >
- <div style={{ textAlign: 'center' }}>
- <p style={{ marginBottom: '24px', color: '#555' }}>
- Tem certeza que deseja excluir a comunidade <strong>{community.name}</strong>?<br/>
- Todos os posts e vínculos serão removidos.
- </p>
- <ModalS.ModalActions>
- <ModalS.ChoiceButton onClick={() => setIsDeleteModalOpen(false)}>
- Cancelar
- </ModalS.ChoiceButton>
- <ModalS.ChoiceButton onClick={handleDelete} style={{ backgroundColor: '#e74c3c' }}>
- Excluir
- </ModalS.ChoiceButton>
- </ModalS.ModalActions>
- </div>
- </Modal>
-
- <Modal
- isOpen={isLeaveModalOpen}
- onClose={() => setIsLeaveModalOpen(false)}
- title="Sair da Comunidade"
- >
- <div style={{ textAlign: 'center' }}>
- <p style={{ marginBottom: '24px', color: '#555' }}>
- Tem certeza que deseja sair da comunidade <strong>{community.name}</strong>?
- </p>
- <ModalS.ModalActions>
- <ModalS.ChoiceButton onClick={() => setIsLeaveModalOpen(false)}>
- Cancelar
- </ModalS.ChoiceButton>
- <ModalS.ChoiceButton
- onClick={handleLeave}
- style={{ backgroundColor: '#e74c3c' }} // Botão vermelho
- >
- Sair
- </ModalS.ChoiceButton>
- </ModalS.ModalActions>
- </div>
- </Modal>
- </S.PageWrapper>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 -437 -438 -439 -440 -441 -442 -443 -444 -445 -446 -447 -448 | - -1x - - -10x - - -1x - - - - - - - - - - - - - - - - - - - -1x - - -10x - - - - - - - - - - - - - - - - - - - - - - - - - - - -1x - - - - - - - - - - - - - - - - - - - - - - - - -1x - - - -10x -10x - - - - - - - - - - - - - - - - - - - - - - - - - - -1x - - - - - - - - - - -10x - - - - - -10x - - - - - - - - - - - - - - - - -10x - - - - - -10x - - - -10x - - - - - - - - - - - - - - -1x - - - - - - -1x - - -3x -3x -3x - - - - -3x - - - - - - - - - - - - - - - - - - - - - -3x -3x - - - - - - - -1x - - -3x - -3x - - - - - - - - - - - - - - -3x - - - - - - - - - - - - -3x -3x -3x - - - - - - - - - - - - - -1x - - - -4x -4x -4x - - - - - - - - - -4x -4x - -4x - - - - - - - - - -1x - - - - - - - - - - - - - -1x - - - - - - - - - - - - - - - - - - -1x -10x - - - - - - - - - - - - - -1x -10x -10x - - - - - - - - - - - - - - - - - - - - - - - - -10x - - - - - - - - - - - - - - - - - - -10x - - - - - - - - - -10x - - - - - - - - - - - -1x - - - - - - -1x -10x -10x - - - - -10x - - - - -10x - -10x - - - -1x - - - - - | import styled from 'styled-components';
-
-export const PageWrapper = styled.div`
- display: flex;
- min-height: 100vh;
- background: linear-gradient(135deg, ${props => props.theme.white} 0%, ${props => props.theme['gray-100'] || props.theme['gray-100']} 100%);
-`;
-
-export const MainContent = styled.main`
- flex: 1;
- margin-left: 250px;
- display: flex;
- flex-direction: column;
-
- animation: fadeIn 0.5s ease-out;
-
- @keyframes fadeIn {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
- }
-`;
-
-// --- Área do Topo (Banner + Avatar + Título) ---
-
-export const Banner = styled.div`
- width: 100%;
- height: 220px;
- background: linear-gradient(135deg, ${props => props.theme.button} 0%, ${props => props.theme['hover-button'] || props.theme.placeholder} 100%);
- position: relative;
- overflow: hidden;
-
- /* Padrão decorativo com formas geométricas */
- &::before {
- content: '';
- position: absolute;
- top: -50%;
- right: -10%;
- width: 600px;
- height: 600px;
- background: radial-gradient(circle, rgba(255, 255, 255, 0.15) 0%, transparent 70%);
- border-radius: 50%;
- }
-
- &::after {
- content: '';
- position: absolute;
- bottom: -30%;
- left: -5%;
- width: 400px;
- height: 400px;
- background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
- border-radius: 50%;
- }
-`;
-
-export const HeaderContainer = styled.div`
- display: flex;
- align-items: flex-end;
- padding: 0 40px;
- margin-top: -60px;
- margin-bottom: 32px;
- gap: 24px;
- position: relative;
- z-index: 2;
- flex-wrap: wrap;
-
- animation: slideUp 0.6s ease-out;
-
- @keyframes slideUp {
- from {
- opacity: 0;
- transform: translateY(30px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-`;
-
-export const Avatar = styled.div`
- width: 130px;
- height: 130px;
- border-radius: 24px;
- background: linear-gradient(135deg, ${props => props.theme.sidebar} 0%, ${props => props.theme.button} 100%);
- border: 5px solid ${props => props.theme.white};
- flex-shrink: 0;
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2),
- 0 4px 12px rgba(0, 0, 0, 0.15);
- position: relative;
-
- /* Efeito de brilho */
- &::after {
- content: '';
- position: absolute;
- top: 15%;
- left: 15%;
- width: 35%;
- height: 35%;
- background: radial-gradient(circle, rgba(255, 255, 255, 0.4) 0%, transparent 70%);
- border-radius: 50%;
- }
-
- transition: transform 0.3s ease, box-shadow 0.3s ease;
-
- &:hover {
- transform: scale(1.05) rotate(2deg);
- box-shadow: 0 15px 40px rgba(0, 0, 0, 0.25),
- 0 6px 16px rgba(0, 0, 0, 0.2);
- }
-`;
-
-export const HeaderInfo = styled.div`
- display: flex;
- flex-direction: column;
- padding-bottom: 12px;
- flex: 1;
- gap: 14px;
- min-width: 0;
-
- h1 {
- font-size: 2.8rem;
- font-weight: 800;
- color: ${props => props.theme.black};
- margin: 0;
- margin-bottom: 8px;
- line-height: 1.1;
- letter-spacing: -0.02em;
-
- background: linear-gradient(135deg, ${props => props.theme.white} 0%, ${props => props.theme.button} 100%);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
-
- position: relative;
- word-wrap: break-word;
- overflow-wrap: break-word;
-
- /* Underline decorativo */
- &::after {
- content: '';
- position: absolute;
- bottom: -10px;
- left: 0;
- width: 80px;
- height: 5px;
- background: linear-gradient(90deg, ${props => props.theme.button} 0%, transparent 100%);
- border-radius: 3px;
- }
- }
-
- span {
- color: ${props => props.theme['gray-600'] || props.theme['gray-500']};
- font-size: 1rem;
- font-weight: 600;
- padding: 6px 14px;
- background: ${props => props.theme['gray-100']};
- border-radius: 20px;
- width: fit-content;
- display: flex;
- align-items: center;
- gap: 6px;
-
- /* Ícone decorativo */
- &::before {
- content: '👥';
- font-size: 0.9em;
- }
- }
-`;
-
-export const HeaderActions = styled.div`
- display: flex;
- align-items: center;
- gap: 12px;
- padding-bottom: 15px;
-`;
-
-export const JoinButton = styled.button`
- padding: 12px 32px;
- border-radius: 12px;
- border: 2px solid ${props => props.theme.button};
- background: ${props => props.theme.button};
- color: ${props => props.theme.white};
- font-weight: 700;
- font-size: 1rem;
- cursor: pointer;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- box-shadow: 0 4px 12px ${props => props.theme.button}40;
- position: relative;
- overflow: hidden;
-
- /* Efeito de brilho deslizante */
- &::before {
- content: '';
- position: absolute;
- top: 0;
- left: -100%;
- width: 100%;
- height: 100%;
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
- transition: left 0.5s;
- }
-
- &:hover::before {
- left: 100%;
- }
-
- &:hover {
- transform: translateY(-3px);
- box-shadow: 0 8px 20px ${props => props.theme.button}50;
- background: ${props => props.theme['hover-button'] || props.theme.button};
- }
-
- &:active {
- transform: translateY(-1px);
- }
-`;
-
-export const LeaveButton = styled.button`
- padding: 12px 32px;
- border-radius: 12px;
- border: 2px solid ${props => props.theme['red-500']};
- background: transparent;
- color: ${props => props.theme['red-500']};
- font-weight: 700;
- font-size: 1rem;
- cursor: pointer;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- position: relative;
- overflow: hidden;
-
- &::before {
- content: '';
- position: absolute;
- top: 50%;
- left: 50%;
- width: 0;
- height: 0;
- background: ${props => props.theme['red-500']};
- border-radius: 50%;
- transform: translate(-50%, -50%);
- transition: width 0.4s, height 0.4s;
- z-index: -1;
- }
-
- &:hover::before {
- width: 300%;
- height: 300%;
- }
-
- &:hover {
- color: ${props => props.theme.white};
- border-color: ${props => props.theme['red-500']};
- box-shadow: 0 4px 16px ${props => props.theme['red-500']}40;
- transform: translateY(-2px);
- }
-
- &:active {
- transform: translateY(0);
- }
-
- > * {
- position: relative;
- z-index: 1;
- }
-`;
-
-export const OptionsButton = styled.button`
- width: 48px;
- height: 48px;
- border-radius: 12px;
- border: 2px solid ${props => props.theme.button};
- background: ${props => props.theme.white};
- color: ${props => props.theme.button};
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- font-size: 1.3rem;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
-
- &:hover {
- background-color: ${props => props.theme.button};
- color: ${props => props.theme.white};
- transform: scale(1.1) rotate(90deg);
- box-shadow: 0 4px 16px ${props => props.theme.button}30;
- }
-
- &:active {
- transform: scale(1.05) rotate(90deg);
- }
-`;
-
-// --- Grid de Conteúdo (Feed + Info Lateral) ---
-
-export const ContentGrid = styled.div`
- display: grid;
- grid-template-columns: 1fr 340px;
- gap: 28px;
- padding: 0 40px 40px 40px;
- max-width: 1400px;
- width: 100%;
- box-sizing: border-box;
-
- @media (max-width: 900px) {
- grid-template-columns: 1fr;
- }
-`;
-
-export const FeedColumn = styled.div`
- display: flex;
- flex-direction: column;
- gap: 20px;
-
- animation: slideInLeft 0.5s ease-out;
-
- @keyframes slideInLeft {
- from {
- opacity: 0;
- transform: translateX(-20px);
- }
- to {
- opacity: 1;
- transform: translateX(0);
- }
- }
-`;
-
-export const FeedCardWrapper = styled.div`
- background: linear-gradient(135deg, ${props => props.theme.background} 0%, ${props => props.theme['gray-100'] || props.theme.background} 100%);
- border-radius: 18px;
- padding: 10px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
- transition: all 0.3s ease;
-
- &:hover {
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
- transform: translateY(-2px);
- }
-`;
-
-// --- Sidebar de Informações (Direita) ---
-
-export const InfoSidebar = styled.aside`
- background: ${props => props.theme.white};
- border: 2px solid ${props => props.theme.button}25;
- border-radius: 20px;
- padding: 28px;
- height: fit-content;
- position: sticky;
- top: 20px;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
- overflow-wrap: break-word;
- word-wrap: break-word;
- word-break: break-word;
-
- animation: slideInRight 0.5s ease-out;
-
- @keyframes slideInRight {
- from {
- opacity: 0;
- transform: translateX(20px);
- }
- to {
- opacity: 1;
- transform: translateX(0);
- }
- }
-
- h3 {
- color: ${props => props.theme.title};
- font-size: 0.85rem;
- font-weight: 800;
- letter-spacing: 1.5px;
- text-transform: uppercase;
- margin-bottom: 16px;
- margin-top: 28px;
- position: relative;
- padding-left: 12px;
-
- /* Barra lateral decorativa */
- &::before {
- content: '';
- position: absolute;
- left: 0;
- top: 50%;
- transform: translateY(-50%);
- width: 4px;
- height: 18px;
- background: linear-gradient(180deg, ${props => props.theme.button} 0%, ${props => props.theme['hover-button'] || props.theme.button} 100%);
- border-radius: 2px;
- }
- }
-
- h3:first-child {
- margin-top: 0;
- }
-
- p {
- color: ${props => props.theme['gray-700'] || props.theme.black};
- font-size: 0.95rem;
- line-height: 1.7;
- margin-bottom: 16px;
- padding-left: 12px;
- word-wrap: break-word;
- overflow-wrap: break-word;
- word-break: break-word;
- white-space: pre-wrap;
- }
-`;
-
-export const KeywordsContainer = styled.div`
- display: flex;
- flex-wrap: wrap;
- gap: 10px;
- padding-left: 12px;
-`;
-
-export const KeywordTag = styled.span`
- border: 2px solid ${props => props.theme.keyword};
- color: ${props => props.theme.keyword};
- border-radius: 10px;
- padding: 8px 18px;
- font-size: 0.85rem;
- font-weight: 700;
- background: ${props => props.theme.keyword}10;
- transition: all 0.2s ease;
- cursor: default;
-
- &:hover {
- background: ${props => props.theme.keyword}20;
- transform: translateY(-2px);
- box-shadow: 0 4px 8px ${props => props.theme.keyword}20;
- }
-`;
-
-export const MenuWrapper = styled.div`
- position: relative;
- display: flex;
- align-items: center;
- z-index: 10;
-`; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 81.81% | -27/33 | -90.9% | -20/22 | -71.42% | -5/7 | -81.81% | -27/33 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 | - - - - - - - - - - - - - -7x -7x - -7x -7x - -7x - - - - - - - - - -7x -7x -7x -7x -7x - -7x - -2x -2x -2x - - - - -2x - - -7x -2x -2x - -1x -1x -1x - - -1x -1x -1x - - -2x - - - - - - - - - - - - -7x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -10x - - - - - - - - - - - - - - - - - | import {useState, useEffect} from 'react';
-import { useNavigate, useLocation } from 'react-router-dom';
-import { useForm, Controller } from 'react-hook-form';
-import Sidebar from '../../components/layout/Sidebar';
-import { PageWrapper, ContentWrapper } from '../Feed/styles';
-import * as S from '../../components/domain/CreationForm/styles';
-import TagInput from '../../components/domain/TagInput';
-import type { NotificationState } from '../../components/common/Toast';
-import { GetKeywords } from '../../API/Keywords';
-import Toast from '../../components/common/Toast';
-import type { CommunityProps } from '../../API/Community';
-import { NewCommunity, UpdateCommunity } from '../../API/Community';
-
-export default function CreateCommunity() {
- const navigate = useNavigate();
- const location = useLocation();
-
- const communityToEdit = location.state?.communityToEdit as CommunityProps | undefined;
- const isEditMode = !!communityToEdit;
-
- const { register, handleSubmit, control, watch } = useForm<CommunityProps>({
- // Preenche os valores iniciais com os dados da comunidade se estiver editando
- defaultValues: {
- communityID: communityToEdit?.communityID || "",
- name: communityToEdit?.name || "",
- description: communityToEdit?.description || "",
- technologies: communityToEdit?.technologies || []
- }
- });
-
- const descriptionValue = watch('description');
- const descriptionLength = descriptionValue ? descriptionValue.length : 0;
- const MAX_CHARS = 500;
- const [notification, setNotification] = useState<NotificationState | null>(null);
- const [keywords, setKeywords] = useState<string[]>([]);
-
- useEffect(() => {
- async function loadTechs() {
- try {
- const techsFromDB = await GetKeywords();
- setKeywords(techsFromDB);
- } catch (error) {
- console.error("Falha ao carregar tecnologias:", error);
- }
- }
- loadTechs();
- }, []);
-
- const onSubmit = (data: CommunityProps) => {
- try{
- if (isEditMode && communityToEdit) {
- // --- MODO EDIÇÃO ---
- console.log("Atualizando Comunidade:", communityToEdit.communityID, data);
- UpdateCommunity(communityToEdit.communityID, data);
- setNotification({ message: 'Comunidade atualizada com sucesso!', type: 'success' });
- } else {
- // --- MODO CRIAÇÃO ---
- console.log("Criando Comunidade:", data);
- NewCommunity(data);
- setNotification({ message: 'Comunidade criada com sucesso!', type: 'success' });
- }
-
- setTimeout(() => {
- navigate('/feed');
- }, 1000);
-
- } catch (error) {
- console.error('Erro ao criar comunidade:', error);
- if (error instanceof Error){
- setNotification({ message: error.message, type: 'error' });
- }
- }
-
- };
-
- return (
- <PageWrapper>
- <Sidebar />
-
- {notification && (
- <Toast
- message={notification.message}
- type={notification.type}
- onClose={() => setNotification(null)}
- />
- )}
-
- <ContentWrapper>
- <S.FormContainer onSubmit={handleSubmit(onSubmit)}>
-
- <h2>{isEditMode ? 'Editar Comunidade' : 'Criar Comunidade'}</h2>
-
- <S.InputGroup>
- <S.Label htmlFor="name">Nome da Comunidade</S.Label>
- <S.Input id="name" {...register('name', { required: true })} />
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="description">Descrição do Projeto</S.Label>
- <S.TextArea
- id="description"
- placeholder="Descreva sua comunidade..."
- maxLength={MAX_CHARS}
- {...register('description', {
- maxLength: {
- value: MAX_CHARS,
- message: `A descrição não pode exceder ${MAX_CHARS} caracteres`
- }
- })}
- />
- <S.CharacterCount>
- {descriptionLength} / {MAX_CHARS}
- </S.CharacterCount>
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="keywords">Palavras-chave</S.Label>
-
- <Controller
- name="technologies"
- control={control}
- render={({ field }) => (
- <TagInput
- value={field.value}
- onChange={field.onChange}
- searchList={keywords}
- limit={10}
- placeholder="Adicione até 10 palavras-chave..."
- />
- )}
- />
- </S.InputGroup>
-
- <S.SubmitButton type="submit">{isEditMode ? 'Salvar Alterações' : 'Criar Comunidade'}</S.SubmitButton>
-
- </S.FormContainer>
- </ContentWrapper>
- </PageWrapper>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 84.44% | -38/45 | -87.09% | -27/31 | -80% | -8/10 | -86.04% | -37/43 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 | - - - - - - - - - - - - - - - - - - - -13x -13x -13x - -13x - - -13x - -13x - -13x - - - -13x -13x -6x - -6x -6x - - -13x - - - - - - - - - - -13x -13x -13x -13x - - -13x - -4x -4x -4x - - - - -4x - - - -13x - -2x - - - - -2x -2x - -1x -1x -1x - - -1x -1x -1x - - - -2x - - - - - - - - - - - -13x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -15x - - - - - -2x - - - - - - - - - - - - -15x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | import { useState, useEffect } from 'react';
-import { useForm, Controller } from 'react-hook-form';
-import Sidebar from '../../components/layout/Sidebar';
-import { PageWrapper, ContentWrapper } from '../Feed/styles';
-import * as S from '../../components/domain/CreationForm/styles';
-import TagInput from '../../components/domain/TagInput';
-import { IMaskInput } from 'react-imask';
-import { NewProject, UpdateProject } from '../../API/Project';
-import type { ProjectProps } from '../../API/Project';
-import Toast from '../../components/common/Toast';
-import { useNavigate, useLocation, useParams } from 'react-router-dom';
-import type { NotificationState } from '../../components/common/Toast';
-import { GetKeywords } from '../../API/Keywords';
-import { parseDate } from '../../API/Project';
-
-interface ProjectFormProps extends Omit<ProjectProps, 'startDate'> {
- startDate: string;
-}
-
-export default function CreateProject() {
- const navigate = useNavigate();
- const location = useLocation(); // Hook para ler o state
- const [keywords, setKeywords] = useState<string[]>([]);
-
- const { projectId: paramId } = useParams<{ projectId?: string }>();
-
- // Recupera o projeto do estado
- const projectToEdit = location.state?.projectToEdit as (ProjectProps & { id?: string; projectID?: string }) | undefined;
- // Define se é modo edição
- const isEditMode = !!projectToEdit;
-
- const validProjectId = projectToEdit?.id
- || projectToEdit?.projectID
- || (paramId !== 'undefined' ? paramId : undefined);
-
- const formatDateToString = (date?: Date | string) => {
- if (!date) return "";
- const d = new Date(date);
- // Verifica se é data válida
- Iif (isNaN(d.getTime())) return "";
- return d.toLocaleDateString('pt-BR'); // Retorna "20/11/2025"
- };
-
- const { register, handleSubmit, control, watch } = useForm<ProjectFormProps>({
- // Preenche os valores padrão se estiver em modo de edição
- defaultValues: {
- title: projectToEdit?.title || "",
- description: projectToEdit?.description || "",
- technologies: projectToEdit?.technologies || [],
- status: projectToEdit?.status || "",
- startDate: formatDateToString(projectToEdit?.startDate)
- }
- });
-
- const descriptionValue = watch('description');
- const descriptionLength = descriptionValue ? descriptionValue.length : 0;
- const MAX_CHARS = 2500;
- const [notification, setNotification] = useState<NotificationState | null>(null);
-
- // Carrega as tecnologias disponíveis do backend
- useEffect(() => {
- async function loadTechs() {
- try {
- const techsFromDB = await GetKeywords();
- setKeywords(techsFromDB);
- } catch (error) {
- console.error("Falha ao carregar tecnologias:", error);
- }
- }
- loadTechs();
- }, []);
-
- // Lida com CRIAR ou ATUALIZAR
- const onSubmit = (data: ProjectFormProps) => {
-
- const finalData: ProjectProps = {
- ...data,
- startDate: parseDate(data.startDate)
- };
-
- try {
- if (isEditMode && validProjectId) {
- // MODO DE EDIÇÃO
- console.log("Atualizando Projeto:", validProjectId, finalData);
- UpdateProject(validProjectId, finalData);
- setNotification({ message: 'Projeto atualizado com sucesso!', type: 'success' });
- } else {
- // MODO DE CRIAÇÃO
- console.log("Criando Projeto:", finalData);
- NewProject(finalData);
- setNotification({ message: 'Projeto criado com sucesso!', type: 'success' });
- }
-
- // Redireciona após o sucesso
- setTimeout(() => {
- navigate('/feed');
- }, 1000);
-
- } catch(error) {
- console.error('Erro ao salvar projeto:', error);
- if (error instanceof Error){
- setNotification({ message: error.message, type: 'error' });
- }
- }
- };
-
- return (
- <PageWrapper>
- <Sidebar />
- {notification && (
- <Toast
- message={notification.message}
- type={notification.type}
- onClose={() => setNotification(null)}
- />
- )}
-
- <ContentWrapper>
- <S.FormContainer onSubmit={handleSubmit(onSubmit)}>
-
- <h2>{isEditMode ? 'Editar Projeto' : 'Criar Projeto'}</h2>
-
- <S.InputGroup>
- <S.Label htmlFor="title">Nome do Projeto</S.Label>
- <S.Input id="title" {...register('title', { required: true })} />
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="description">Descrição do Projeto</S.Label>
- <S.TextArea
- id="description"
- placeholder="Descreva seu projeto..."
- maxLength={MAX_CHARS}
- {...register('description', {
- maxLength: {
- value: MAX_CHARS,
- message: `A descrição não pode exceder ${MAX_CHARS} caracteres`
- }
- })}
- />
- <S.CharacterCount>
- {descriptionLength} / {MAX_CHARS}
- </S.CharacterCount>
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="startDate">Data de Início</S.Label>
- <Controller
- name="startDate"
- control={control}
- render={({ field: { onChange, value } }) => (
- <S.Input
- as={IMaskInput}
- mask="00/00/0000"
- id="startDate"
- placeholder="DD/MM/AAAA"
- value={value}
- onAccept={(value: string) => onChange(value)}
- disabled={isEditMode}
- />
- )}
- />
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="technologies">Tecnologias (Palavras-chave)</S.Label>
- <Controller
- name="technologies"
- control={control}
- render={({ field }) => (
- <TagInput
- value={field.value}
- onChange={field.onChange}
- searchList={keywords}
- limit={6}
- placeholder="Adicione até 6 tecnologias..."
- />
- )}
- />
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="status">Status</S.Label>
- <S.SelectWrapper>
- <S.Select
- id="status"
- {...register('status', { required: true })}
- required
- >
- <option value="" disabled>Selecione um status...</option>
- <option value="em-andamento">Em andamento</option>
- <option value="pausado">Pausado</option>
- <option value="finalizado">Finalizado</option>
- </S.Select>
- </S.SelectWrapper>
- </S.InputGroup>
-
- {/* 10. Botão de submit dinâmico */}
- <S.SubmitButton type="submit">
- {isEditMode ? 'Atualizar Projeto' : 'Criar Projeto'}
- </S.SubmitButton>
-
- </S.FormContainer>
- </ContentWrapper>
- </PageWrapper>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 87.09% | -27/31 | -55.55% | -10/18 | -66.66% | -4/6 | -93.1% | -27/29 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 | - - - - - - - - - - - - -8x -8x -8x - - -8x -3x -3x -3x -3x - - -8x - - -8x -3x -3x -3x -3x -3x -3x - - - -8x -2x -2x -2x - -1x - -1x - - -1x - - - - -1x -1x -1x - - - - -8x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | import { useState, useEffect } from 'react';
-import { useForm } from 'react-hook-form';
-import { useNavigate } from 'react-router-dom';
-import Sidebar from '../../components/layout/Sidebar';
-import { PageWrapper, ContentWrapper } from '../Feed/styles';
-import * as S from '../../components/domain/CreationForm/styles';
-import Toast from '../../components/common/Toast';
-import type { NotificationState } from '../../components/common/Toast';
-import { useAuth } from '../../API/AuthContext';
-import { UpdateProfile } from '../../API/User';
-import type { UserProfileData } from '../../API/User';
-
-export default function EditProfile() {
- const { currentUser, updateUser } = useAuth();
- const navigate = useNavigate();
- const [notification, setNotification] = useState<NotificationState | null>(null);
-
- // Formata a data para o input (YYYY-MM-DD)
-const formatDateForInput = (dateString?: string) => {
- Iif (!dateString) return "";
- const date = new Date(dateString);
- Iif (isNaN(date.getTime())) return "";
- return date.toISOString().split('T')[0];
-};
-
- const { register, handleSubmit, setValue } = useForm<UserProfileData>();
-
- // Preenche o formulário com os dados atuais
- useEffect(() => {
- Eif (currentUser) {
- setValue('nomeCompleto', currentUser.nomeCompleto || '');
- setValue('username', currentUser.username || '');
- setValue('email', currentUser.email || '');
- setValue('telefone', (currentUser as any).phone || '');
- setValue('dataNascimento', formatDateForInput((currentUser as any).birthDate));
- }
- }, [currentUser, setValue]);
-
- const onSubmit = async (data: UserProfileData) => {
- try {
- console.log("Atualizando perfil:", data);
- await UpdateProfile(data);
-
- setNotification({ message: 'Perfil atualizado com sucesso!', type: 'success' });
-
- updateUser(data);
-
- // Redireciona
- setTimeout(() => {
- navigate('/profile');
- }, 1000);
-
- } catch (error) {
- console.error(error);
- Eif (error instanceof Error) {
- setNotification({ message: error.message, type: 'error' });
- }
- }
- };
-
- return (
- <PageWrapper>
- <Sidebar />
- {notification && (
- <Toast
- message={notification.message}
- type={notification.type}
- onClose={() => setNotification(null)}
- />
- )}
-
- <ContentWrapper>
- <S.FormContainer onSubmit={handleSubmit(onSubmit)}>
- <h2>Editar Perfil</h2>
-
- <S.InputGroup>
- <S.Label htmlFor="nomeCompleto">Nome Completo</S.Label>
- <S.Input
- id="nomeCompleto"
- {...register('nomeCompleto', { required: true })}
- />
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="username">Nome de Usuário</S.Label>
- <S.Input
- id="username"
- {...register('username', { required: true })}
- />
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="email">Email</S.Label>
- <S.Input
- id="email"
- type="email"
- {...register('email', { required: true })}
- />
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="telefone">Telefone</S.Label>
- <S.Input
- id="telefone"
- {...register('telefone')}
- />
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="dataNascimento">Data de Nascimento</S.Label>
- <S.Input
- id="dataNascimento"
- type="date"
- {...register('dataNascimento')}
- />
- </S.InputGroup>
-
- <S.SubmitButton type="submit">
- Salvar Alterações
- </S.SubmitButton>
-
- </S.FormContainer>
- </ContentWrapper>
- </PageWrapper>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 | - - - - - - - - - - - - - -18x -18x -18x -18x -18x - -18x - -6x -6x -6x -5x - -1x -1x -1x - - -6x - - -6x - - -18x -1x -1x - - -18x -1x -1x - - -18x - - - - - - - - - -2x - - - - - - - - - - - - -2x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | import { useState, useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
-import Modal from '../../components/common/Modal';
-import Postcard from '../../components/domain/Postcard';
-import Header from '../../components/layout/Header';
-import Sidebar from '../../components/layout/Sidebar';
-import * as S from './styles';
-import * as ModalS from '../../components/common/Modal/styles';
-import { GetFeedProjects } from '../../API/Project';
-import type { ProjectProps } from '../../API/Project';
-import Toast from '../../components/common/Toast';
-import type { NotificationState } from '../../components/common/Toast';
-
-export default function Feed() {
- const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
- const [isLoading, setIsLoading] = useState(true);
- const navigate = useNavigate();
- const [posts, setPosts] = useState<ProjectProps[]>([]);
- const [notification, setNotification] = useState<NotificationState | null>(null);
-
- useEffect(() => {
- async function loadFeed() {
- setIsLoading(true);
- try {
- const feedData = await GetFeedProjects();
- setPosts(feedData || []);
- } catch (error) {
- console.error("Erro ao carregar feed:", error);
- Eif (error instanceof Error) {
- setNotification({ message: "Não foi possível carregar o feed.", type: 'error' });
- }
- } finally {
- setIsLoading(false);
- }
- }
- loadFeed();
- }, []);
-
- const handleCreateProject = () => {
- setIsCreateModalOpen(false);
- navigate('/createProject');
- };
-
- const handleCreateCommunity = () => {
- setIsCreateModalOpen(false);
- navigate('/createCommunity');
- };
-
- return (
- <S.PageWrapper>
- {notification && (
- <Toast
- message={notification.message}
- type={notification.type}
- onClose={() => setNotification(null)}
- />
- )}
-
- <Header onCreateClick={() => setIsCreateModalOpen(true)}/>
- <Sidebar />
-
- <S.ContentWrapper>
- <S.FeedContainer>
- <S.PostList>
- {isLoading ? (
- <S.LoadingContainer>
- <div className="spinner"></div>
- <p>Carregando feed...</p>
- </S.LoadingContainer>
- ) : posts.length > 0 ? (
- posts.map((post, index) => (
- <Postcard
- key={(post as unknown as ProjectProps).id || (post as any).projectID || index}
- post={post}
- showMenu={false}
- />
- ))
- ) : (
- <S.EmptyFeedMessage>
- <svg
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- strokeWidth="2"
- strokeLinecap="round"
- strokeLinejoin="round"
- >
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
- <line x1="9" y1="9" x2="15" y2="15"/>
- <line x1="15" y1="9" x2="9" y2="15"/>
- </svg>
- <h3>Nenhum post encontrado</h3>
- <p style={{ color: '#ccc', textAlign: 'center', marginTop: '20px' }}>
- Nenhum post encontrado. Entre em comunidades para ver atualizações!
- </p>
- </S.EmptyFeedMessage>
- )}
- </S.PostList>
- </S.FeedContainer>
- </S.ContentWrapper>
-
- <Modal
- isOpen={isCreateModalOpen}
- onClose={() => setIsCreateModalOpen(false)}
- title="O que você deseja criar?"
- >
- <ModalS.ModalActions>
- <ModalS.ChoiceButton onClick={handleCreateProject}>
- Criar Projeto
- </ModalS.ChoiceButton>
- <ModalS.ChoiceButton onClick={handleCreateCommunity}>
- Criar Comunidade
- </ModalS.ChoiceButton>
- </ModalS.ModalActions>
- </Modal>
- </S.PageWrapper>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 | - - -4x -44x - - - - - - - - - - -44x -44x - - - - - - -4x - - - - - - - - - - - - - - - - - - - - - - - - - - -4x - - - -16x - - - - - - -16x - - - - - - - - - - - - - - - - - - - - - - - - -16x - - - - - - - - - -4x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -4x - - -5x - - - - - - - - - - - - - -5x - - - - - - -5x - - - - - - - - -4x - - - - - - - - - - - - - - - - - - - - - - - -4x - - - - - - - - - - -10x -10x - - - - - - - - - -10x - - - | import styled from 'styled-components';
-
-// Wrapper para a página inteira
-export const PageWrapper = styled.div`
- background: linear-gradient(135deg, ${props => props.theme['gray-100']} 0%, ${props => props.theme['gray-100']}f0 100%);
- min-height: 100vh;
- position: relative;
-
- &::before {
- content: '';
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: radial-gradient(circle at 20% 50%, ${props => props.theme.keyword}08 0%, transparent 50%),
- radial-gradient(circle at 80% 80%, ${props => props.theme.button}08 0%, transparent 50%);
- pointer-events: none;
- z-index: 0;
- }
-`;
-
-// Wrapper para o conteúdo principal
-export const ContentWrapper = styled.main`
- margin-left: 250px;
- margin-top: 60px;
- padding: 32px 24px;
- box-sizing: border-box;
- position: relative;
- z-index: 1;
- animation: fadeIn 0.5s ease-out;
-
- @keyframes fadeIn {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-
- @media (max-width: 768px) {
- margin-left: 0;
- padding: 20px 16px;
- }
-`;
-
-// Container principal do feed com posts
-export const FeedContainer = styled.main`
- width: 100%;
- max-width: 800px;
- margin: 0 auto;
- background: ${props => props.theme.background};
- backdrop-filter: blur(10px);
- border-radius: 20px;
- padding: 28px;
- box-sizing: border-box;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12),
- 0 2px 8px rgba(0, 0, 0, 0.08),
- 0 0 0 1px ${props => props.theme.placeholder}15;
- position: relative;
- transition: all 0.3s ease;
-
- &::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 3px;
-
- background-size: 200% 100%;
- border-radius: 20px 20px 0 0;
- animation: gradientShift 3s ease infinite;
- }
-
- @keyframes gradientShift {
- 0%, 100% { background-position: 0% 50%; }
- 50% { background-position: 100% 50%; }
- }
-
- &:hover {
- box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15),
- 0 4px 12px rgba(0, 0, 0, 0.1),
- 0 0 0 1px ${props => props.theme.placeholder}25;
- }
-
- @media (max-width: 768px) {
- padding: 20px;
- border-radius: 16px;
- }
-`;
-
-// Lista que agrupa os posts
-export const PostList = styled.div`
- display: flex;
- flex-direction: column;
- gap: 20px;
- position: relative;
-
- /* Animação de entrada para cada post */
- & > * {
- animation: slideInUp 0.4s ease-out backwards;
- }
-
- & > *:nth-child(1) { animation-delay: 0.05s; }
- & > *:nth-child(2) { animation-delay: 0.1s; }
- & > *:nth-child(3) { animation-delay: 0.15s; }
- & > *:nth-child(4) { animation-delay: 0.2s; }
- & > *:nth-child(5) { animation-delay: 0.25s; }
-
- @keyframes slideInUp {
- from {
- opacity: 0;
- transform: translateY(20px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-`;
-
-// Mensagem de feed vazio
-export const EmptyFeedMessage = styled.div`
- text-align: center;
- padding: 60px 24px;
- color: ${props => props.theme.placeholder};
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 16px;
-
- svg {
- width: 80px;
- height: 80px;
- opacity: 0.5;
- margin-bottom: 8px;
- }
-
- h3 {
- color: ${props => props.theme.subtitle};
- font-size: 1.3rem;
- font-weight: 600;
- margin: 0 0 8px 0;
- }
-
- p {
- color: ${props => props.theme.placeholder};
- font-size: 0.95rem;
- line-height: 1.5;
- max-width: 400px;
- margin: 0;
- }
-`;
-
-// Header do feed
-export const FeedHeader = styled.div`
- margin-bottom: 24px;
- padding-bottom: 20px;
- border-bottom: 1px solid ${props => props.theme.placeholder}20;
-
- h1 {
- color: ${props => props.theme.white};
- font-size: 1.8rem;
- font-weight: 700;
- margin: 0 0 8px 0;
- background: linear-gradient(135deg, ${props => props.theme.white}, ${props => props.theme.subtitle});
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
- }
-
- p {
- color: ${props => props.theme.placeholder};
- font-size: 0.9rem;
- margin: 0;
- }
-`;
-
-// Container para loading state
-export const LoadingContainer = styled.div`
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 60px 24px;
- gap: 16px;
-
- .spinner {
- width: 40px;
- height: 40px;
- border: 3px solid ${props => props.theme.placeholder}30;
- border-top-color: ${props => props.theme.keyword};
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- }
-
- @keyframes spin {
- to { transform: rotate(360deg); }
- }
-
- p {
- color: ${props => props.theme.placeholder};
- font-size: 0.9rem;
- }
-`; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 100% | -1/1 | -100% | -0/0 | -100% | -1/1 | -100% | -1/1 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 | - - - -1x - - - - - - - - - | import HeaderHome from '../../components/layout/HeaderHome';
-import * as S from './styles';
-
-export default function Home() {
- return (
- <S.HomePageContainer>
- <HeaderHome />
- <S.ContentWrapper>
- <S.SiteTitle>CTable</S.SiteTitle>
- <S.Tagline>Descubra, compartilhe, aprenda, converse: um novo mundo se abre para os amantes de computação.</S.Tagline>
- </S.ContentWrapper>
- </S.HomePageContainer>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 87.5% | -14/16 | -75% | -3/4 | -50% | -2/4 | -87.5% | -14/16 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 | - - - - - - - - - - - - - - - - - -10x -10x - -10x -10x - - -2x -2x -2x - -1x - - -1x - -1x - - - - -1x - - -1x -1x - - - - -10x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | import {useState} from 'react';
-import {
- FormPageContainer,
- FormWrapper,
- FormTitle,
- StyledForm,
- StyledInput,
- SubmitButton,
- RedirectLink
-} from '../../components/domain/Form/styles';
-import { useForm} from 'react-hook-form';
-import Toast from '../../components/common/Toast';
-import { useNavigate } from 'react-router-dom';
-import type { LoginProps } from '../../API/Auth';
-import { useAuth } from '../../API/AuthContext';
-import type { NotificationState } from '../../components/common/Toast';
-
-export default function LoginPage() {
- const [notification, setNotification] = useState<NotificationState | null>(null);
- const { login } = useAuth();
-
- const { register, handleSubmit, formState: {isSubmitting} } = useForm<LoginProps>();
- const navigate = useNavigate();
-
- async function onSubmit(data: LoginProps) {
- console.log(data);
- try{
- await login(data);
-
- console.log('Usuário registrado com sucesso');
-
- // Define estado para mostrar notificação de sucesso
- setNotification({ message: 'Usuário registrado com sucesso!', type: 'success' });
-
- setTimeout(() => {
- navigate('/feed'); // Navega para a página de feed
- }, 1000);
-
- } catch (error) {
- console.error('Erro ao registrar usuário:', error);
-
- // Define estado para mostrar notificação de erro
- Eif (error instanceof Error){
- setNotification({ message: error.message, type: 'error' });
- }
- }
- }
-
- return (
- <FormPageContainer>
- {notification && (
- <Toast
- message={notification.message}
- type={notification.type}
- onClose={() => setNotification(null)}
- />
- )}
- <FormWrapper>
- <FormTitle>Login</FormTitle>
-
- <StyledForm onSubmit={handleSubmit(onSubmit)}>
- <StyledInput
- type="text"
- placeholder="Email ou usuário"
- required
- {...register('username')}
- />
- <StyledInput
- type="password"
- placeholder="Senha"
- required
- {...register('senha')}
- />
-
- <SubmitButton disabled = {isSubmitting} type="submit">Entrar</SubmitButton>
- </StyledForm>
-
- <RedirectLink>
- Não tem uma conta? <a href="/register">Cadastre-se</a>
- </RedirectLink>
- </FormWrapper>
- </FormPageContainer>
- );
-}
-
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 | - - - - - - - - - - - - - - - - -1x -40x - - - - -1x -40x - - - -1x -40x - - - - - - - - -42x -42x -42x -42x - -42x - - -42x -42x - -42x -42x -42x - -42x - -40x -40x -40x - -40x -40x - -39x - -39x -39x - - -1x - - - -40x -40x - - - -42x - -1x -1x - - -25x -7x - -25x -25x - - - -42x -1x -1x - - -42x -1x - - -1x -1x - - - -42x -1x -1x - - -42x - -3x -3x - -3x -3x -1x - -1x - -1x - - - - - -2x -2x -1x -1x -1x - -1x - -1x - -3x -3x - - - - -42x -42x -37x -35x - - -2x -2x - - - - - -5x - -5x -2x - - - - - - - - - - - - - -1x - - - - - - - - -42x - - - - - - - - - - - - - - - - - - - - - - - - - - -2x - - - - - -7x - - - - - - - - - - - -4x -4x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -1x - - - - - - - - - - - - - - - - - | import { useState, useRef, useEffect } from 'react';
-import Sidebar from '../../components/layout/Sidebar';
-import Postcard from '../../components/domain/Postcard';
-import * as S from './styles';
-import * as D from '../../components/common/Dropdown/styles';
-import type { ProjectProps } from '../../API/Project';
-import type { CommentProps } from '../../API/Comment';
-import { GetUserProjects } from '../../API/Project';
-import { GetUserComments, DeleteComment } from '../../API/Comment';
-import { useAuth } from '../../API/AuthContext';
-import { useNavigate } from 'react-router-dom';
-import { DeleteProfile } from '../../API/User';
-import Toast from '../../components/common/Toast';
-import Modal from '../../components/common/Modal';
-import * as ModalS from '../../components/common/Modal/styles';
-
-// --- ÍCONES ---
-const PostsIcon = () => (
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
- <polyline points="14 2 14 8 20 8" /><line x1="16" y1="13" x2="8" y2="13" /><line x1="16" y1="17" x2="8" y2="17" /><line x1="10" y1="9" x2="8" y2="9" />
- </svg>
-);
-const CommentsIcon = () => (
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
- </svg>
-);
-const SettingsIcon = () => (
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" />
- </svg>
-);
-// -----------------------
-
-type ViewState = 'posts' | 'comments';
-
-export default function Profile() {
- const { currentUser, logout } = useAuth();
- const [view, setView] = useState<ViewState>('posts');
- const [isMenuOpen, setIsMenuOpen] = useState(false);
- const menuRef = useRef<HTMLDivElement>(null);
-
- const navigate = useNavigate();
-
- // Estados para os dados da API
- const [userPosts, setUserPosts] = useState<ProjectProps[]>([]);
- const [userComments, setUserComments] = useState<CommentProps[]>([]);
-
- const [isDeleting, setIsDeleting] = useState(false);
- const [isDeleteProfileModalOpen, setIsDeleteProfileModalOpen] = useState(false);
- const [notification, setNotification] = useState<{message: string, type: 'success' | 'error'} | null>(null);
-
- useEffect(() => {
- // Função assíncrona para buscar todos os dados
- console.log("Efeito de busca de dados do perfil disparado.");
- const fetchProfileData = async () => {
- try {;
-
- console.log("Usuário atual no Profile:", currentUser);
- const apiUserPosts = await GetUserProjects();
-
- const apiUserComments: CommentProps[] = await GetUserComments();
-
- setUserPosts(apiUserPosts);
- setUserComments(apiUserComments);
-
- } catch (error) {
- console.error("Falha ao buscar dados do perfil:", error);
- }
-
- };
- Eif(currentUser)
- fetchProfileData();
- }, [currentUser]);
-
- // Lógica para fechar o menu ao clicar fora
- useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- Eif (menuRef.current && !menuRef.current.contains(event.target as Node)) {
- setIsMenuOpen(false);
- }
- }
- if (isMenuOpen) {
- document.addEventListener('mousedown', handleClickOutside);
- }
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, [isMenuOpen]);
-
- const handleLogout = () => {
- logout(); // Limpa o estado e o localStorage
- navigate('/login'); // Redireciona para a tela de login
- };
-
- const handleDeleteComment = async (commentId: string) => {
- await DeleteComment(commentId);
-
- // Remove o comentário deletado do estado local para sumir da tela instantaneamente
- setUserComments((prevComments) =>
- prevComments.filter(comment => comment.commentID !== commentId)
- );
- };
-
- const handleEditProfile = () => {
- setIsMenuOpen(false);
- navigate('/editProfile');
- };
-
- const handleDeleteProfile = async () => {
-
- Iif (isDeleting) return;
- setIsDeleting(true);
-
- try {
- await DeleteProfile();
- setNotification({ message: "Perfil excluído com sucesso. Até logo!", type: 'success' });
-
- setIsDeleteProfileModalOpen(false);
-
- setTimeout(() => {
- logout(); // Desloga o usuário e limpa o storage
- navigate('/login'); // Manda para login
- }, 2000);
-
- } catch (error) {
- Eif (error instanceof Error) {
- if (error.message.includes("não encontrado") || error.message.includes("not found")) {
- setNotification({ message: "Conta já encerrada. Redirecionando...", type: 'success' });
- setTimeout(() => { logout(); navigate('/login'); }, 2000);
- return;
- }
- setNotification({ message: error.message, type: 'error' });
- }
- setIsDeleteProfileModalOpen(false);
- } finally {
- setIsDeleting(false);
- setIsDeleteProfileModalOpen(false);
- }
- };
-
- // Função para renderizar o feed (posts ou comentários)
- const renderFeed = () => {
- if (view === 'posts') {
- if (userPosts.length === 0) {
- return <p style={{ color: '#fff', padding: '20px' }}>Nenhum projeto encontrado.</p>;
- }
-
- return userPosts.map((post, index) => (
- <S.PostContainer key={index}>
- <Postcard post={post} showMenu={true} />
- </S.PostContainer>
- ));
- }
-
- Eif (view === 'comments') {
- // Mapeia 'userComments'
- return userComments.map(comment => (
- <S.PostContainer key={comment.commentID || Math.random()}>
- <Postcard
- post={{
- id: comment.commentID,
- title: `Comentou em: ${comment.projectTitle || 'Projeto'}`,
- description: comment.content,
- technologies: [],
- status: '',
- startDate: comment.createdAt,
- // Passa o usuário atual como autor para o cabeçalho do card
- author: { title: currentUser?.username || 'Você' }
- } as unknown as ProjectProps}
- showMenu={true}
- deleteLabel="Comentário"
- onDelete={() => handleDeleteComment(comment.commentID!)}
- />
- </S.PostContainer>
- ));
- }
-
- return null;
- };
-
- return (
- <S.PageWrapper>
- <Sidebar />
-
- {notification && (
- <Toast
- message={notification.message}
- type={notification.type}
- onClose={() => setNotification(null)}
- />
- )}
-
- <S.ContentWrapper>
-
- <S.ProfileHeader>
- <S.ActionsWrapper ref={menuRef}>
- <S.ProfileActions>
- <S.IconButton
- title="Ver publicações"
- $active={view === 'posts'}
- onClick={() => setView('posts')}
- >
- <PostsIcon />
- </S.IconButton>
- <S.IconButton
- title="Ver comentários"
- $active={view === 'comments'}
- onClick={() => setView('comments')}
- >
- <CommentsIcon />
- </S.IconButton>
- <S.IconButton
- title="Configurações"
- onClick={() => setIsMenuOpen(prev => !prev)}
- >
- <SettingsIcon />
- </S.IconButton>
- </S.ProfileActions>
-
- {isMenuOpen && (
- <D.DropdownMenu>
- <D.MenuItem onClick={handleEditProfile}>Editar Perfil</D.MenuItem>
- <D.MenuItem onClick={handleLogout}>Sair</D.MenuItem>
- <D.Separator />
- <D.DangerMenuItem onClick={() => {
- setIsMenuOpen(false);
- setIsDeleteProfileModalOpen(true);
- }}>
- Excluir Perfil
- </D.DangerMenuItem>
- </D.DropdownMenu>
- )}
-
- </S.ActionsWrapper>
- </S.ProfileHeader>
-
- <S.ProfileInfo>
- <S.ProfileAvatar
- style={{
- backgroundImage: undefined
- }}
- />
- <S.Username>{currentUser?.username || 'Carregando...'}</S.Username>
- </S.ProfileInfo>
-
- <S.PostList>
- {renderFeed()}
- </S.PostList>
-
- </S.ContentWrapper>
-
- <Modal
- isOpen={isDeleteProfileModalOpen}
- onClose={() => !isDeleting && setIsDeleteProfileModalOpen(false)}
- title="Excluir Conta"
- >
- <div style={{ textAlign: 'center' }}>
- <p style={{ marginBottom: '24px', color: '#555' }}>
- Tem certeza que deseja excluir sua conta? <br/>
- <strong>Todos os seus projetos, comunidades e comentários serão apagados permanentemente.</strong>
- </p>
- <ModalS.ModalActions>
- <ModalS.ChoiceButton
- onClick={() => setIsDeleteProfileModalOpen(false)}
- disabled={isDeleting}>
- Cancelar
- </ModalS.ChoiceButton>
- <ModalS.ChoiceButton
- onClick={handleDeleteProfile}
- style={{ backgroundColor: '#e74c3c' }}
- disabled={isDeleting}
- >
- {isDeleting ? 'Excluindo...' : 'Excluir Conta'}
- </ModalS.ChoiceButton>
- </ModalS.ModalActions>
- </div>
- </Modal>
-
- </S.PageWrapper>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 | - - -1x -40x - - - - -1x - - - - - - -1x - - - - -3x - - - - - - - - - - - - - - - - - -1x - - - - - - - - - - - - - - - - - - - - -1x -40x - - - - - - - - - - - - - - - - - - - - - - - - - - - - -1x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -1x - - - -40x -40x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -1x - - -40x - - -40x - - - - - - - - - - - - - - - -40x - - - - - -1x - - - - - - - - - - - - - - - -1x - - - - - - - - - - - -120x - - -120x - - - -120x - - - - - - - - - - - - - - -120x - - - - - - - - - - - - - -120x - - -120x - - - - - - - - - - - - - - - - - -120x - - - - -1x - - - | import styled from 'styled-components';
-
-// Wrapper para a página inteira
-export const PageWrapper = styled.div`
- background: linear-gradient(135deg, ${props => props.theme['gray-100']} 0%, ${props => props.theme['gray-100'] || props.theme['gray-100']} 100%);
- min-height: 100vh;
-`;
-
-// Wrapper para o conteúdo principal
-export const ContentWrapper = styled.main`
- margin-left: 250px;
- box-sizing: border-box;
- padding-bottom: 60px;
-`;
-
-// Container principal com posts
-export const PostContainer = styled.main`
- width: 100%;
- max-width: 800px;
- margin: 0 auto;
-
- background-color: ${props => props.theme.background};
- border-radius: 16px;
- padding: 24px;
- box-sizing: border-box;
-
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04),
- 0 1px 2px rgba(0, 0, 0, 0.06);
-
- transition: all 0.3s ease;
-
- &:hover {
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08),
- 0 2px 4px rgba(0, 0, 0, 0.06);
- transform: translateY(-2px);
- }
-`;
-
-// Lista que agrupa os posts do perfil
-export const PostList = styled.div`
- display: flex;
- flex-direction: column;
- gap: 20px;
- padding: 32px 24px;
-
- animation: fadeIn 0.4s ease-out;
-
- @keyframes fadeIn {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-`;
-
-// Cabeçalho do perfil
-export const ProfileHeader = styled.header`
- background: linear-gradient(135deg, ${props => props.theme.button} 0%, ${props => props.theme['hover-button'] || props.theme.button} 100%);
- height: 160px;
- position: relative;
- z-index: 0;
-
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
-
- /* Padrão decorativo sutil */
- &::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-image:
- radial-gradient(circle at 20% 50%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
- radial-gradient(circle at 80% 80%, rgba(255, 255, 255, 0.1) 0%, transparent 50%);
- pointer-events: none;
- }
-
- /* Posiciona os botões de ação */
- display: flex;
- justify-content: flex-end;
- align-items: flex-end;
- padding: 20px 32px;
-`;
-
-// Container da foto + nome
-export const ProfileInfo = styled.div`
- display: flex;
- align-items: flex-end;
- gap: 20px;
-
- margin-top: -70px;
- margin-left: 32px;
- padding-bottom: 24px;
- position: relative;
- z-index: 1;
- pointer-events: none;
-
- animation: slideUp 0.5s ease-out;
-
- @keyframes slideUp {
- from {
- opacity: 0;
- transform: translateY(20px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-
- /* Reativa pointer-events apenas nos elementos internos */
- > * {
- pointer-events: auto;
- }
-`;
-
-// A foto de perfil circular
-export const ProfileAvatar = styled.div`
- width: 120px;
- height: 120px;
- border-radius: 50%;
- background: linear-gradient(135deg, ${props => props.theme.sidebar} 0%, ${props => props.theme.button} 100%);
- border: 5px solid ${props => props.theme.background};
- background-size: cover;
- background-position: center;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15),
- 0 2px 8px rgba(0, 0, 0, 0.1);
-
- position: relative;
- flex-shrink: 0;
-
- /* Efeito de brilho sutil */
- &::after {
- content: '';
- position: absolute;
- top: 10%;
- left: 10%;
- width: 40%;
- height: 40%;
- background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 0%, transparent 70%);
- border-radius: 50%;
- }
-
- transition: transform 0.3s ease, box-shadow 0.3s ease;
-
- &:hover {
- transform: scale(1.05);
- box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2),
- 0 4px 12px rgba(0, 0, 0, 0.15);
- }
-`;
-
-// O nome do usuário
-export const Username = styled.h1`
- font-size: 2.5em;
- font-weight: 700;
- color: ${props => props.theme.title};
- margin: 0 0 10px 0; /* Margem inferior para alinhar com a base do avatar */
-
- background: linear-gradient(135deg, ${props => props.theme.title} 0%, ${props => props.theme.subtitle} 100%);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
-
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
-
- position: relative;
-
- &::after {
- content: '';
- position: absolute;
- bottom: -8px;
- left: 0;
- width: 60px;
- height: 4px;
- background: linear-gradient(90deg, ${props => props.theme.button} 0%, transparent 100%);
- border-radius: 2px;
- }
-`;
-
-// Container dos 3 botões de ação (dentro do banner)
-export const ProfileActions = styled.div`
- display: flex;
- align-items: center;
- gap: 12px;
- position: relative;
- z-index: 3;
-
- /* Backdrop blur para destacar os botões */
- background: rgba(255, 255, 255, 0.1);
- backdrop-filter: blur(10px);
- padding: 8px;
- border-radius: 50px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
-`;
-
-// Botão de ícone
-export const IconButton = styled.button<{ $active?: boolean }>`
- display: flex;
- align-items: center;
- justify-content: center;
- width: 48px;
- height: 48px;
- border-radius: 50%;
- border: none;
- cursor: pointer;
- position: relative;
- overflow: hidden;
-
- background-color: ${props => props.$active
- ? props.theme['hover-button'] || props.theme.button
- : props.theme.background};
- color: ${props => props.$active
- ? '#FFFFFF'
- : props.theme.button};
-
- box-shadow: ${props => props.$active
- ? '0 4px 12px rgba(0, 0, 0, 0.15), inset 0 1px 2px rgba(255, 255, 255, 0.1)'
- : '0 2px 8px rgba(0, 0, 0, 0.08)'};
-
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-
- /* Efeito ripple */
- &::before {
- content: '';
- position: absolute;
- top: 50%;
- left: 50%;
- width: 0;
- height: 0;
- border-radius: 50%;
- background: ${props => props.$active
- ? 'rgba(255, 255, 255, 0.3)'
- : 'rgba(0, 0, 0, 0.1)'};
- transform: translate(-50%, -50%);
- transition: width 0.6s, height 0.6s;
- }
-
- &:hover::before {
- width: 100%;
- height: 100%;
- }
-
- &:hover {
- transform: translateY(-3px) scale(1.05);
- box-shadow: ${props => props.$active
- ? '0 6px 20px rgba(0, 0, 0, 0.2)'
- : '0 4px 16px rgba(0, 0, 0, 0.12)'};
- background-color: ${props => props.$active
- ? props.theme['hover-button'] || props.theme.button
- : props.theme['gray-500'] || props.theme.background};
- }
-
- &:active {
- transform: translateY(-1px) scale(1.02);
- }
-
- svg {
- width: 22px;
- height: 22px;
- position: relative;
- z-index: 1;
- transition: transform 0.2s ease;
- }
-
- &:hover svg {
- transform: ${props => props.$active ? 'rotate(5deg)' : 'scale(1.1)'};
- }
-`;
-
-// Menus de ações flutuantes
-export const ActionsWrapper = styled.div`
- position: relative;
- z-index: 5; /* Aumentado para ficar acima de tudo */
-`; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 90% | -18/20 | -75% | -6/8 | -100% | -4/4 | -94.11% | -16/17 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 | - - - - - - - - -4x -4x -4x - -4x - -2x -2x -2x -2x -2x - - - -2x - - -2x - - -4x -2x - - -1x - -1x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -2x - - - - - - - - - - - | import { useEffect, useState } from 'react';
-import { useParams } from 'react-router-dom';
-import Sidebar from '../../components/layout/Sidebar';
-import * as S from './styles';
-import { GetProjectById } from '../../API/Project';
-import type { ProjectProps } from '../../API/Project';
-import { FiCalendar, FiUser } from 'react-icons/fi';
-
-export default function ProjectPage() {
- const { projectId } = useParams<{ projectId: string }>();
- const [project, setProject] = useState<ProjectProps | null>(null);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- async function loadData() {
- Iif (!projectId) return;
- try {
- setLoading(true);
- const data = await GetProjectById(projectId);
- setProject(data);
- } catch (error) {
- console.error("Erro ao carregar projeto:", error);
- } finally {
- setLoading(false);
- }
- }
- loadData();
- }, [projectId]);
-
- if (loading) return <div>Carregando...</div>;
- if (!project) return <div>Projeto não encontrado</div>;
-
- // Formata data
- const startDate = new Date(project.startDate).toLocaleDateString('pt-BR');
-
- return (
- <S.PageWrapper>
- <Sidebar />
-
- <S.MainContent>
- <S.Banner />
-
- <S.HeaderContainer>
- {/* Ícone com a inicial do projeto */}
- <S.ProjectIcon>
- {project.title.charAt(0).toUpperCase()}
- </S.ProjectIcon>
-
- <S.HeaderInfo>
- <h1>{project.title}</h1>
- <span>
- <FiUser style={{ marginRight: 5, verticalAlign: 'middle' }}/>
- Criado por <strong>{project.authorUsername || project.authorName}</strong>
- </span>
- </S.HeaderInfo>
- </S.HeaderContainer>
-
- <S.ContentGrid>
-
- {/* Coluna Principal: Descrição */}
- <S.MainColumn>
- <h2>Sobre o Projeto</h2>
- <S.DescriptionBox>
- {project.description}
- </S.DescriptionBox>
-
- </S.MainColumn>
-
- <S.InfoSidebar>
- <h3>Status</h3>
- <S.StatusBadge status={project.status}>
- {project.status.replace('-', ' ')}
- </S.StatusBadge>
-
- <h3>Início</h3>
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: '#555' }}>
- <FiCalendar />
- <span>{startDate}</span>
- </div>
-
- <h3>Tecnologias</h3>
- <S.KeywordsContainer>
- {project.technologies?.map((tech) => (
- <S.KeywordTag key={tech}>
- {tech}
- </S.KeywordTag>
- ))}
- </S.KeywordsContainer>
- </S.InfoSidebar>
-
- </S.ContentGrid>
- </S.MainContent>
- </S.PageWrapper>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 87.5% | -14/16 | -75% | -3/4 | -60% | -3/5 | -87.5% | -14/16 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 | - - - - - - - - - - - -8x - - -8x -8x - - -2x -2x -2x - -1x - - -1x -1x - - - - -1x - - -1x -1x - - - - -8x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -10x - - - - - - - - - - - - - - - - - - - - - | import { useState } from 'react';
-import { FormPageContainer, FormWrapper, FormTitle, StyledForm, StyledInput, SubmitButton, RedirectLink } from '../../components/domain/Form/styles';
-import { useForm, Controller} from 'react-hook-form';
-import { IMaskInput } from 'react-imask';
-import Toast from '../../components/common/Toast';
-import { useNavigate } from 'react-router-dom';
-import {Register as RegisterAPI} from '../../API/Auth'
-import type { RegisterProps } from '../../API/Auth';
-import type { NotificationState } from '../../components/common/Toast';
-
-export default function Register() {
- // Estado para a notificação - usado no Toast
- const [notification, setNotification] = useState<NotificationState | null>(null);
-
-
- const { register, handleSubmit, formState: {isSubmitting}, control } = useForm<RegisterProps>();
- const navigate = useNavigate();
-
- async function onSubmit(data: RegisterProps) {
- console.log(data);
- try{
- await RegisterAPI(data);
-
- console.log('Usuário registrado com sucesso:');
-
- // Define estado para mostrar notificação de sucesso
- setNotification({ message: 'Usuário registrado com sucesso!', type: 'success' });
- setTimeout(() => {
- navigate('/feed'); // Navega para a página de feed
- }, 1000);
-
- } catch (error) {
- console.error('Erro ao registrar usuário:', error);
-
- // Define estado para mostrar notificação de erro
- Eif (error instanceof Error){
- setNotification({ message: error.message, type: 'error' });
- }
- }
- }
-
- return (
- <FormPageContainer>
-
- {notification && (
- <Toast
- message={notification.message}
- type={notification.type}
- onClose={() => setNotification(null)}
- />
- )}
-
- <FormWrapper>
- <FormTitle>Criar conta</FormTitle>
-
- <StyledForm onSubmit={handleSubmit(onSubmit)}>
- <StyledInput
- type="text"
- placeholder="Nome completo"
- required
- {...register('nomeCompleto')}
- />
- <StyledInput
- type="text"
- placeholder="Nome de usuário (user)"
- required
- {...register('username')}
- />
- <StyledInput
- type="email"
- placeholder="Email"
- required
- {...register('email')}
- />
- <StyledInput
- type="password"
- placeholder="Senha"
- required
- {...register('senha')}
- />
- <StyledInput
- type="tel"
- placeholder="Telefone"
- required
- {...register('telefone')}
- />
- <Controller
- name="dataNascimento"
- control={control}
- rules={{ required: true }}
- render={({ field }) => (
- <StyledInput
- {...field}
- as={IMaskInput}
- mask="00/00/0000"
- placeholder="Data de nascimento"
- required
- />
- )}
- />
-
- <SubmitButton disabled = {isSubmitting} type="submit">Cadastrar</SubmitButton>
- </StyledForm>
-
- <RedirectLink>
- Já tem uma conta? <a href="/login">Faça login</a>
- </RedirectLink>
- </FormWrapper>
- </FormPageContainer>
- );
-}
-
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 | - - - - - - - - - - - - -1x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | import api from './api';
-import { isAxiosError } from 'axios';
-export interface CommentProps {
- projectTitle?: string;
- commentID?: string;
- content: string;
- createdAt?: string;
- authorID?: string;
- projectID?: string;
- username?: string;
- fullName?: string;
-}
-
-const getAuthHeader = () => {
- return {
- headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
- }
- };
-};
-
-export async function CreateComment(projectId: string, content: string) {
- try{
-
- const response = await api.post(`/api/project/${projectId}/comments`,
- {content},
- getAuthHeader()
- );
-
- return response.data;
-
- }catch(error){
-
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao enviar comentário.");
-
- }
-}
-
-export async function GetComments(projectId: string): Promise<CommentProps[]> {
- try{
-
- const response = await api.get(`/api/project/${projectId}/comments`, getAuthHeader());
-
- return await response.data;
- }catch(error){
- console.error("Erro ao buscar comentários:", error);
- throw new Error("Erro ao carregar comentários.");
- }
-
-
-}
-
-export async function GetUserComments(): Promise<CommentProps[]> {
- try{
- const response = await api.get('/api/user/comments', getAuthHeader());
-
- return response.data;
- }catch(error){
- console.error("Erro ao buscar comentários do usuário:", error);
- throw new Error("Erro ao carregar comentários do usuário.");
- }
-}
-
-export async function DeleteComment(commentId: string) {
- try{
-
- const response = await api.delete(`/api/project/${commentId}/deletecomment`, getAuthHeader());
-
- return response.data;
- }catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao excluir comentário.");
- }
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 | - - - - - - - - - - - - - - -2x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | import api from './api';
-import { isAxiosError } from 'axios';
-
-export interface CommunityProps {
- communityID: string;
- name: string;
- description: string;
- technologies: string[];
- createdAt: Date;
- updatedAt: Date;
- memberCount?: number;
- isMember?: boolean;
- isAdmin?: boolean;
-}
-
-const getAuthHeader = () => {
- return {
- headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
- }
- };
-};
-
-export async function NewCommunity(data: CommunityProps) {
-
- try{
- console.log("Enviando dados da comunidade:", data);
- const response = await api.post('/api/newcommunity', data, getAuthHeader());
- return response.data
- }catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao criar comunidade.");
- }
-}
-
-export async function GetUserCommunities(): Promise<CommunityProps[]> {
- try{
- const response = await api.get<CommunityProps[]>('/api/user/communities', getAuthHeader());
- return response.data
- }catch(error){
- console.error("Erro ao obter comunidades:", error);
- throw new Error('Erro ao obter comunidades');
- }
-}
-
-export async function GetCommunityById(communityId: string) {
- try{
- const response = await api.get(`/api/communities/data/${communityId}`, getAuthHeader());
-
- return await response.data;
- }catch(error){
- if (isAxiosError(error) && error.response?.status === 404) {
- throw new Error('Comunidade não encontrada');
- }
- throw new Error('Erro ao carregar a comunidade');
- }
-}
-
-export async function JoinCommunity(communityId: string) {
- try{
- const response = await api.post(`/api/communities/${communityId}/join`, {}, getAuthHeader());
-
- return await response.data;
- }catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error('Erro ao entrar na comunidade');
- }
-}
-
-export async function DeleteCommunity(communityId: string) {
- try{
- const response = await api.delete(`/api/communities/deletecommunity/${communityId}`, getAuthHeader());
- return response.data
- }catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao excluir comunidade.");
- }
-}
-
-export async function UpdateCommunity(communityId: string, data: CommunityProps) {
- try{
- const response = await api.put(`/api/communities/updatecommunity/${communityId}`,
- data,
- getAuthHeader()
- );
-
- return response.data;
- }catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao atualizar comunidade.");
- }
-}
-
-export async function LeaveCommunity(communityId: string) {
- try{
- const response = await api.delete(`/api/user/leavecommunity/${communityId}`, getAuthHeader());
-
- return await response.data;
- }catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao sair da comunidade.");
- }
-}
-
-export async function GetAllCommunities(): Promise<CommunityProps[]> {
- try{
- const response = await api.get<CommunityProps[]>('/api/communities', getAuthHeader());
- return response.data
- }catch(error){
- console.error("Erro ao obter todas as comunidades:", error);
- throw new Error('Erro ao obter todas as comunidades');
- }
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 | - - - - - - - - - - - - - - - - -2x - - -2x - - -4x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | import api from './api';
-import { isAxiosError } from 'axios';
-
-export interface ProjectProps {
- id?: string;
- title: string;
- description: string;
- technologies: string[];
- status: string;
- startDate: Date;
- authorUsername?: string;
- authorName?: string;
- creatorID?: number;
-}
-
-export function parseDate(dataString: string): Date {
- // Divide a string "20/11/2025" em partes
- const [dia, mes, ano] = dataString.split('/');
-
- // Cria a data: new Date(ano, mês - 1, dia)
- return new Date(Number(ano), Number(mes) - 1, Number(dia));
-}
-
-const getAuthHeader = () => {
- return {
- headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
- }
- };
-};
-
-export async function NewProject(data: ProjectProps) {
- try{
- const response = await api.post('/api/user/newproject', data, getAuthHeader());
-
- return response.data;
- }catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao criar projeto.");
- }
-}
-
-export async function UpdateProject(projectId: string, data: ProjectProps) {
- try{
- const response = await api.put(`/api/user/updateproject/${projectId}`,
- data,
- getAuthHeader()
- );
- return response.data;
- }catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao atualizar projeto.");
- }
-
-}
-
-export async function GetFeedProjects(): Promise<ProjectProps[]> {
-
- try{
- const response = await api.get('/api/user/home', getAuthHeader());
-
- console.log("Dados do feed de projetos:", response.data);
-
- return response.data.feed;
- }catch(error){
- console.error("Erro ao buscar feed:", error);
- throw new Error("Erro ao carregar o feed de projetos.");
- }
-}
-
-export async function DeleteProject(projectId: string) {
- try{
- await api.delete(`/api/user/deleteproject/${projectId}`, getAuthHeader());
- }
- catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao excluir projeto.");
- }
-}
-
-export async function GetUserProjects(): Promise<ProjectProps[]> {
- try{
- const response = await api.get('/api/user/projects', getAuthHeader());
-
- console.log("Dados dos projetos do usuário:", response.data.projects);
-
- return response.data.projects;
- }catch(error){
- if (isAxiosError(error) && error.response) {
- throw new Error(error.response.data.message);
- }
- throw new Error("Erro ao buscar projetos do usuário.");
- }
-}
-
-export async function GetProjectById(projectId: string): Promise<ProjectProps> {
- try{
- const response = await api.get(`/api/projects/${projectId}`, getAuthHeader());
-
- return await response.data;
- }catch(error){
- console.error("Erro ao buscar projeto:", error);
- throw new Error('Erro ao carregar projeto');
- }
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 | - -5x - - - - | import axios from 'axios';
-
-const api = axios.create({
- baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000',
-});
-
-export default api; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| Comment.ts | -
-
- |
- 4.16% | -1/24 | -0% | -0/8 | -0% | -0/5 | -4.16% | -1/24 | -
| Community.ts | -
-
- |
- 2.04% | -1/49 | -0% | -0/24 | -0% | -0/9 | -2.04% | -1/49 | -
| Project.ts | -
-
- |
- 7.69% | -3/39 | -0% | -0/16 | -12.5% | -1/8 | -7.69% | -3/39 | -
| api.ts | -
-
- |
- 100% | -1/1 | -100% | -2/2 | -100% | -0/0 | -100% | -1/1 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 | - - - - - - - -2x - - - - - - - - - - - | import { ThemeProvider } from "styled-components"
-import { defaultTheme } from './styles/themes/default'
-import { GlobalStyle } from './styles/global'
-import { Router } from "./Router"
-import { BrowserRouter } from "react-router-dom"
-
-function App() {
-
- return (
- <ThemeProvider theme={defaultTheme}>
- <BrowserRouter>
- <GlobalStyle/>
- <Router />
- </BrowserRouter>
- </ThemeProvider>
- )
-}
-
-export default App
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 100% | -3/3 | -100% | -0/0 | -100% | -1/1 | -100% | -3/3 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 | - - - - - - - - -1x -2x - - - - - - - - - -1x | import React from 'react';
-import * as S from './styles';
-
-// Props que o componente Keyword aceita
-interface KeywordProps {
- children: React.ReactNode;
- onRemove: () => void;
-}
-
-export const Keyword: React.FC<KeywordProps> = ({ children, onRemove }) => {
- return (
- <S.KeywordTag>
- {children}
- <S.KeywordRemoveButton onClick={onRemove} aria-label={`Remover ${children}`}>
- ×
- </S.KeywordRemoveButton>
- </S.KeywordTag>
- );
-};
-
-export const KeywordContainer = S.KeywordContainer; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 100% | -12/12 | -100% | -4/4 | -100% | -4/4 | -100% | -12/12 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 | - - - - - - - - - - -1x - -5x -5x -4x - -1x - - -5x -5x - - - -5x -1x - - - -4x -2x - - -4x - - - - - - - - - - - - | import React, { useEffect } from 'react';
-import { createPortal } from 'react-dom';
-import * as S from './styles';
-
-interface ModalProps {
- isOpen: boolean;
- onClose: () => void;
- children: React.ReactNode;
- title: string;
-}
-
-const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children }) => {
- // Bloqueia o scroll do body quando o modal está aberto
- useEffect(() => {
- if (isOpen) {
- document.body.style.overflow = 'hidden';
- } else {
- document.body.style.overflow = 'unset';
- }
-
- return () => {
- document.body.style.overflow = 'unset';
- };
- }, [isOpen]);
-
- if (!isOpen) {
- return null;
- }
-
- // Impede que o clique no modal feche o modal (só o overlay)
- const handleContentClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- };
-
- return createPortal(
- <S.ModalOverlay onClick={onClose}>
- <S.ModalContent onClick={handleContentClick}>
- <S.CloseButton onClick={onClose}>×</S.CloseButton>
- <S.ModalTitle>{title}</S.ModalTitle>
- {children}
- </S.ModalContent>
- </S.ModalOverlay>,
- document.body
- );
-};
-
-export default Modal; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 100% | -7/7 | -100% | -0/0 | -100% | -4/4 | -100% | -7/7 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 | - - - - - - - - - - - - - -1x - - -4x -4x -1x - - - -4x -4x - - - -4x - - - - - - - - - - | import React, { useEffect } from 'react';
-import { ToastContainer, ToastMessage, CloseButton } from './styles';
-
-interface ToastProps {
- message: string;
- type: 'success' | 'error';
- onClose: () => void; // Função para fechar o toast
-}
-
-export interface NotificationState {
- message: string;
- type: 'success' | 'error';
-}
-
-const Toast: React.FC<ToastProps> = ({ message, type, onClose }) => {
-
- // Efeito para fechar o toast automaticamente após 5 segundos
- useEffect(() => {
- const timer = setTimeout(() => {
- onClose();
- }, 5000);
-
- // Limpa o timer se o componente for desmontado
- return () => {
- clearTimeout(timer);
- };
- }, [onClose]);
-
- return (
- <ToastContainer type={type}>
- <ToastMessage>{message}</ToastMessage>
- <CloseButton onClick={onClose}>
- ×
- </CloseButton>
- </ToastContainer>
- );
-};
-
-export default Toast; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| styles.ts | -
-
- |
- 87.32% | -62/71 | -85.71% | -12/14 | -84.48% | -49/58 | -86.95% | -60/69 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 | - - -2x -20x - - - - - - - -20x - - - - - - - - - - - - - - - - -2x - - - - - - - - - - - - - - - - - - - -2x - - - -86x - - - - - - - - - - - - - - - - - -86x - - - - - - - - - - - -2x - - - - -33x - - -33x - - - - - -33x - - - - -33x -33x - - - - -33x -33x -33x - - - - - -2x - - - - - - - -13x - - - - - - - - - -13x - - - -13x - - - - - -2x - - - - -13x - - -13x - - - - - - - - - - - - -13x -13x - - - - -13x -13x -13x - - - - - -13x - - - - -13x -13x - - - - -2x - - - - - -2x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -2x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -2x - - - - - - - - - - - - - - - - - -2x - - - -20x -20x - - - - - -20x - - - - - - - - - - - - - - - - - - - - - -20x - - - - - - - - - - -20x - - - - - - - - -2x - - - - - - -20x - - -20x - - - - - - - -20x - - - - -20x -20x - - - - -20x -20x -20x - - - - - - - - - - -20x - - - - -20x - - - - -20x - - - - -2x - - -20x - - - - -20x - - - - - - -20x -20x - - - -20x -20x - - | import styled from 'styled-components';
-
-// O <form> container
-export const FormContainer = styled.form`
- background: ${props => props.theme.white};
- border-radius: 24px;
- padding: 40px;
- width: 100%;
- max-width: 800px;
- margin: 0 auto;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08),
- 0 2px 8px rgba(0, 0, 0, 0.04);
- border: 1px solid ${props => props.theme.placeholder}20;
-
- animation: slideUp 0.5s ease-out;
-
- @keyframes slideUp {
- from {
- opacity: 0;
- transform: translateY(20px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-`;
-
-// Wrapper para cada par de (label + input)
-export const InputGroup = styled.div`
- margin-bottom: 28px;
- width: 100%;
-
- animation: fadeIn 0.4s ease-out backwards;
- animation-delay: calc(var(--index, 0) * 0.05s);
-
- @keyframes fadeIn {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-`;
-
-// Label
-export const Label = styled.label`
- display: block;
- font-size: 0.95em;
- font-weight: 700;
- color: ${props => props.theme.title};
- margin-bottom: 10px;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- font-size: 0.85rem;
- padding-left: 4px;
-
- /* Indicador visual opcional */
- position: relative;
-
- &::after {
- content: '';
- position: absolute;
- left: -12px;
- top: 50%;
- transform: translateY(-50%);
- width: 3px;
- height: 14px;
- background: linear-gradient(180deg, ${props => props.theme.button} 0%, ${props => props.theme['hover-button'] || props.theme.button} 100%);
- border-radius: 2px;
- opacity: 0;
- transition: opacity 0.2s ease;
- }
-
- ${InputGroup}:focus-within &::after {
- opacity: 1;
- }
-`;
-
-// Input
-export const Input = styled.input`
- width: 100%;
- padding: 14px 20px;
- font-size: 1em;
-
- background-color: ${props => props.theme['gray-100']};
- border: 2px solid transparent;
- border-radius: 12px;
- color: ${props => props.theme.title};
- box-sizing: border-box;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- font-weight: 500;
-
- &::placeholder {
- color: ${props => props.theme.placeholder};
- font-weight: 400;
- }
-
- &:hover {
- background-color: ${props => props.theme.white};
- border-color: ${props => props.theme.placeholder}40;
- }
-
- &:focus {
- outline: none;
- background-color: ${props => props.theme.white};
- border-color: ${props => props.theme.button};
- box-shadow: 0 0 0 4px ${props => props.theme.button}15,
- 0 2px 8px rgba(0, 0, 0, 0.04);
- transform: translateY(-1px);
- }
-`;
-
-export const SelectWrapper = styled.div`
- position: relative;
- width: 100%;
-
- /* A seta de seleção (CSS) */
- &::after {
- content: '▼';
- font-size: 0.85em;
- color: ${props => props.theme.placeholder};
- position: absolute;
- right: 20px;
- top: 50%;
- transform: translateY(-50%);
- pointer-events: none;
- transition: all 0.3s ease;
- }
-
- &:hover::after {
- color: ${props => props.theme.button};
- }
-
- &:focus-within::after {
- color: ${props => props.theme.button};
- transform: translateY(-50%) rotate(180deg);
- }
-`;
-
-// Componente de Select
-export const Select = styled.select`
- width: 100%;
- padding: 14px 20px;
- font-size: 1em;
-
- background-color: ${props => props.theme['gray-100']};
- border: 2px solid transparent;
- border-radius: 12px;
- color: ${props => props.theme.title};
- box-sizing: border-box;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- font-weight: 500;
- cursor: pointer;
-
- appearance: none;
- -webkit-appearance: none;
- -moz-appearance: none;
-
- padding-right: 50px;
-
- &:hover {
- background-color: ${props => props.theme.white};
- border-color: ${props => props.theme.placeholder}40;
- }
-
- &:focus {
- outline: none;
- background-color: ${props => props.theme.white};
- border-color: ${props => props.theme.button};
- box-shadow: 0 0 0 4px ${props => props.theme.button}15,
- 0 2px 8px rgba(0, 0, 0, 0.04);
- transform: translateY(-1px);
- }
-
- &:invalid {
- color: ${props => props.theme.placeholder};
- }
-
- option {
- padding: 10px;
- background: ${props => props.theme.white};
- color: ${props => props.theme.title};
- }
-`;
-
-// Wrapper para o input de busca E a lista de resultados
-export const SearchWrapper = styled.div`
- position: relative;
- width: 100%;
-`;
-
-// Lista de resultados de busca que aparece abaixo do input
-export const SearchResultsList = styled.ul`
- position: absolute;
- top: calc(100% + 4px);
- left: 0;
- right: 0;
- background: ${props => props.theme.white};
- border: 2px solid ${props => props.theme.button}20;
- border-radius: 12px;
- max-height: 240px;
- overflow-y: auto;
- margin: 0;
- padding: 6px;
- z-index: 100;
- list-style: none;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12),
- 0 2px 8px rgba(0, 0, 0, 0.08);
-
- animation: slideDown 0.2s ease-out;
-
- @keyframes slideDown {
- from {
- opacity: 0;
- transform: translateY(-8px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-
- /* Estilização da scrollbar */
- &::-webkit-scrollbar {
- width: 8px;
- }
-
- &::-webkit-scrollbar-track {
- background: transparent;
- }
-
- &::-webkit-scrollbar-thumb {
- background: ${props => props.theme['gray-300']};
- border-radius: 4px;
- }
-
- &::-webkit-scrollbar-thumb:hover {
- background: ${props => props.theme['gray-400']};
- }
-`;
-
-// Item individual na lista de resultados
-export const SearchResultItem = styled.li`
- padding: 12px 16px;
- cursor: pointer;
- border-radius: 8px;
- color: ${props => props.theme.title};
- font-weight: 500;
- transition: all 0.2s ease;
- position: relative;
-
- /* Barra lateral que aparece no hover */
- &::before {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 3px;
- background: ${props => props.theme.button};
- border-radius: 0 2px 2px 0;
- transform: scaleY(0);
- transition: transform 0.2s ease;
- }
-
- &:hover {
- background-color: ${props => props.theme['gray-100']};
- padding-left: 20px;
-
- &::before {
- transform: scaleY(1);
- }
- }
-
- &:active {
- background-color: ${props => props.theme['gray-100']};
- }
-`;
-
-// Mensagem de erro
-export const ErrorMessage = styled.span`
- font-size: 0.85em;
- margin-top: 8px;
- color: ${props => props.theme['red-500'] || '#ef4444'};
- font-weight: 600;
- display: flex;
- align-items: center;
- gap: 6px;
- padding-left: 4px;
-
- /* Ícone de alerta */
- &::before {
- content: '⚠';
- font-size: 1.1em;
- }
-`;
-
-// Botão de submissão
-export const SubmitButton = styled.button`
- padding: 16px 40px;
- font-size: 1.05em;
- font-weight: 700;
- color: ${props => props.theme.white};
- background: linear-gradient(135deg, ${props => props.theme.button} 0%, ${props => props.theme['hover-button'] || props.theme.button} 100%);
- border: none;
- border-radius: 12px;
- cursor: pointer;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- margin-top: 16px;
- box-shadow: 0 4px 16px ${props => props.theme.button}40;
- position: relative;
- overflow: hidden;
-
- /* Efeito de brilho deslizante */
- &::before {
- content: '';
- position: absolute;
- top: 0;
- left: -100%;
- width: 100%;
- height: 100%;
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
- transition: left 0.5s;
- }
-
- &:hover::before {
- left: 100%;
- }
-
- &:hover {
- transform: translateY(-3px);
- box-shadow: 0 8px 24px ${props => props.theme.button}50;
- }
-
- &:active {
- transform: translateY(-1px);
- }
-
- &:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- transform: none;
- box-shadow: 0 2px 8px ${props => props.theme.button}20;
-
- &:hover {
- transform: none;
- }
- }
-`;
-
-// Estilo para o campo de texto de várias linhas (Descrição)
-export const TextArea = styled.textarea`
- width: 100%;
- padding: 16px 20px;
- font-size: 1em;
- font-family: inherit;
- line-height: 1.6;
-
- background-color: ${props => props.theme['gray-100']};
- border: 2px solid transparent;
- border-radius: 12px;
- color: ${props => props.theme.title};
- box-sizing: border-box;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- resize: vertical;
- min-height: 150px;
- font-weight: 500;
-
- &::placeholder {
- color: ${props => props.theme.placeholder};
- font-weight: 400;
- }
-
- &:hover {
- background-color: ${props => props.theme.white};
- border-color: ${props => props.theme.placeholder}40;
- }
-
- &:focus {
- outline: none;
- background-color: ${props => props.theme.white};
- border-color: ${props => props.theme.button};
- box-shadow: 0 0 0 4px ${props => props.theme.button}15,
- 0 2px 8px rgba(0, 0, 0, 0.04);
- transform: translateY(-1px);
- }
-
- /* Estilização da scrollbar */
- &::-webkit-scrollbar {
- width: 10px;
- }
-
- &::-webkit-scrollbar-track {
- background: ${props => props.theme['gray-100']};
- border-radius: 0 10px 10px 0;
- }
-
- &::-webkit-scrollbar-thumb {
- background: ${props => props.theme['gray-300']};
- border-radius: 10px;
- }
-
- &::-webkit-scrollbar-thumb:hover {
- background: ${props => props.theme.button};
- }
-`;
-
-// Contador de caracteres
-export const CharacterCount = styled.div`
- text-align: right;
- font-size: 0.85em;
- color: ${props => props.theme['gray-500'] || props.theme.placeholder};
- margin-top: 6px;
- margin-right: 4px;
- font-weight: 600;
- padding: 4px 8px;
- background: ${props => props.theme['gray-100']};
- border-radius: 6px;
- display: inline-block;
- float: right;
-
- /* Muda de cor quando próximo do limite */
- &[data-warning="true"] {
- color: ${props => props.theme['yellow-600'] || '#d97706'};
- background: ${props => props.theme['yellow-50'] || '#fef3c7'};
- }
-
- &[data-limit="true"] {
- color: ${props => props.theme['red-500'] || '#ef4444'};
- background: ${props => '#fee2e2'};
- }
-`; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 69.23% | -72/104 | -58.9% | -43/73 | -64.28% | -18/28 | -72.16% | -70/97 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 | - - - - - - - - - - - - - -1x - - - - - - - -1x -21x - - - - - - -1x -12x - - - - - - - - - - - - - -21x - -21x -21x -21x -21x -21x - - -21x -21x -21x -21x -21x - -21x - -21x - -21x -10x -2x - - - - -2x -2x -2x - - - - - - -21x - - - - - -13x -4x - -13x -13x - - - -21x -1x - -1x -1x - - - - - -21x -2x -2x - -2x - -2x - - -2x -1x - - -1x -1x - - -1x - - - -21x -1x - -1x - - - - -1x - -1x -1x -1x - -1x -1x -1x - - - - - - -1x - - - -21x - - - - - - - - - - - - -21x - -12x - - -12x - - -12x - - - - - - -12x - - - - - - - - -21x -1x - - - - - -21x - - - - - - - - - - - - - - - - - - -4x - - - - - - - - - -2x -2x - - - - - - - - - - -2x - - - - - - - - - - - -1x - - - - - - - - - - - - - - - - - - - - - -1x - -1x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | import React, { useState, useRef, useEffect } from 'react';
-import * as S from './styles';
-import * as D from '../../common/Dropdown/styles';
-import { useNavigate } from 'react-router-dom';
-import type { ProjectProps } from '../../../API/Project';
-import { DeleteProject } from '../../../API/Project';
-import Modal from '../../common/Modal';
-import type { NotificationState } from '../../common/Toast';
-import Toast from '../../common/Toast';
-import * as ModalS from '../../common/Modal/styles';
-import { CreateComment, GetComments, DeleteComment } from '../../../API/Comment';
-import type { CommentProps } from '../../../API/Comment';
-import { useAuth } from '../../../API/AuthContext';
-
-const TrashIcon = () => (
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <polyline points="3 6 5 6 21 6"></polyline>
- <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
- </svg>
-);
-
-// Ícone de Comentário (Balão)
-const CommentIcon = () => (
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
- </svg>
-);
-
-
-// --- Ícone de Menu (Ellipsis) ---
-const EllipsisIcon = () => (
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" />
- </svg>
-);
-
-interface PostcardProps {
- post: ProjectProps;
- showMenu: boolean; // Verifica se o menu deve ser mostrado (se é dono do post)
- onDelete?: (id: string) => Promise<void>;
- deleteLabel?: string;
-}
-
-// --- Componente Postcard ---
-export default function Postcard({ post, showMenu, onDelete, deleteLabel = 'Projeto' }: PostcardProps) {
- const { currentUser } = useAuth();
-
- const [isMenuOpen, setIsMenuOpen] = useState(false);
- const menuRef = useRef<HTMLDivElement>(null);
- const navigate = useNavigate(); // Inicialize o hook de navegação
- const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); // Estado para o modal de exclusão
- const [notification, setNotification] = useState<NotificationState | null>(null);
-
- // Estados para o formulário de comentário
- const [isCommentBoxOpen, setIsCommentBoxOpen] = useState(false);
- const [commentText, setCommentText] = useState("");
- const [isSubmittingComment, setIsSubmittingComment] = useState(false);
- const MAX_COMMENT_CHARS = 500;
- const [comments, setComments] = useState<CommentProps[]>([]);
-
- const [commentToDelete, setCommentToDelete] = useState<string | null>(null);
-
- const projectId = (post as any).id || (post as any).projectID;
-
- useEffect(() => {
- if (isCommentBoxOpen && projectId) {
- loadComments();
- }
- }, [isCommentBoxOpen, projectId]);
-
- async function loadComments() {
- try {
- const data = await GetComments(projectId);
- setComments(data);
- } catch (error) {
- console.error("Erro ao carregar comentários", error);
- }
- }
-
- // Lógica para fechar o menu ao clicar fora (sem alterações)
- useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
- setIsMenuOpen(false);
- }
- }
- if (isMenuOpen) {
- document.addEventListener('mousedown', handleClickOutside);
- }
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, [isMenuOpen]);
-
- const handleEditClick = () => {
- const projectId = (post as any).id || (post as any).projectID;
-
- if (projectId) {
- navigate(`/editProject/${projectId}`, { state: { projectToEdit: post } });
- } else E{
- console.error("Erro: ID do projeto não encontrado no objeto:", post);
- }
- };
-
- const handleDeleteMainItem = async () => {
- try {
- const id = (post as any).id || (post as any).projectID || (post as any).commentID;
-
- Iif (!id) throw new Error("ID não encontrado.");
-
- Iif (onDelete) {
- await onDelete(id);
- } else {
- await DeleteProject(id);
- setTimeout(() => navigate('/feed'), 1000);
- }
-
- setNotification({ message: `${deleteLabel} excluído com sucesso!`, type: 'success' });
- setIsDeleteModalOpen(false);
-
- } catch (error) {
- setNotification({ message: `Erro ao excluir ${deleteLabel?.toLowerCase()}.`, type: 'error' });
- }
- };
-
- const handleCommentSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
-
- Iif (!projectId) {
- setNotification({ message: 'Erro: Projeto sem ID.', type: 'error' });
- return;
- }
-
- Iif (!commentText.trim()) return;
-
- try {
- setIsSubmittingComment(true);
- await CreateComment(projectId, commentText);
-
- setNotification({ message: 'Comentário enviado!', type: 'success' });
- setCommentText(""); // Limpa o campo
- setIsCommentBoxOpen(false);
-
- } catch (error) {
- if (error instanceof Error) {
- setNotification({ message: error.message, type: 'error' });
- }
- } finally {
- setIsSubmittingComment(false);
- }
- };
-
- const handleConfirmDeleteInternalComment = async () => {
- if (!commentToDelete) return;
- try {
- await DeleteComment(commentToDelete);
- setNotification({ message: 'Comentário excluído.', type: 'success' });
- setCommentToDelete(null);
- await loadComments();
- } catch (error: any) {
- setNotification({ message: error.message, type: 'error' });
- setCommentToDelete(null);
- }
- };
-
- const handleCardClick = (e: React.MouseEvent) => {
- // Não navega se estiver na aba de comentários do perfil (deleteLabel 'Comentário')
- Iif (deleteLabel !== 'Projeto') return;
-
- // Não navega se o clique foi em um elemento interativo
- const target = e.target as HTMLElement;
-
- // Verifica se clicou em botões, inputs, links ou no próprio menu
- Eif (
- target.closest('button') ||
- target.closest('input') ||
- target.closest('textarea') ||
- target.closest('a') ||
- target.closest(D.DropdownMenu as any) // Garante que itens do dropdown não disparem
- ) {
- return;
- }
-
- // Realiza a navegação
- if (projectId) {
- navigate(`/project/${projectId}`);
- }
- };
-
- const formatDate = (dateString?: string) => {
- Eif (!dateString) return "";
- return new Date(dateString).toLocaleDateString('pt-BR', {
- day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit'
- });
- };
-
- return (
- <S.PostCardWrapper onClick={handleCardClick}>
- {notification && (
- <Toast
- message={notification.message}
- type={notification.type}
- onClose={() => setNotification(null)}
- />
- )}
-
- <S.PostHeader>
- <img src={(post as any).avatarUrl || 'url_placeholder_avatar.png'} alt={post.title} />
- <span>{post.title}</span>
- <small>• {(post as any).author?.title || 'Autor'}</small>
- </S.PostHeader>
-
- <S.PostContent>
- {showMenu && (
- <S.MenuWrapper ref={menuRef}>
- <S.MenuButton onClick={() => setIsMenuOpen(prev => !prev)}>
- <EllipsisIcon />
- </S.MenuButton>
-
- {isMenuOpen && (
- <D.DropdownMenu>
- {deleteLabel === 'Projeto' && (
- <D.MenuItem onClick={handleEditClick}>Editar</D.MenuItem>
- )}
- <D.DangerMenuItem onClick={() => {
- setIsMenuOpen(false);
- setIsDeleteModalOpen(true);}}>
- Excluir
- </D.DangerMenuItem>
- </D.DropdownMenu>
- )}
- </S.MenuWrapper>
- )}
-
- <p>{post.description}</p>
-
- <S.ActionRow>
- <S.ActionButton onClick={() => setIsCommentBoxOpen(!isCommentBoxOpen)}>
- <CommentIcon />
- <span>Comentar</span>
- </S.ActionButton>
- </S.ActionRow>
-
- {isCommentBoxOpen && (
- <>
- <S.CommentForm onSubmit={handleCommentSubmit}>
- <S.CommentTextArea
- placeholder="Escreva seu comentário..."
- value={commentText}
- onChange={(e) => setCommentText(e.target.value)}
- maxLength={MAX_COMMENT_CHARS}
- disabled={isSubmittingComment}
- />
- <S.CommentFooter>
- <S.CharacterCount isLimit={commentText.length >= MAX_COMMENT_CHARS}>
- {commentText.length} / {MAX_COMMENT_CHARS}
- </S.CharacterCount>
- <S.SubmitCommentButton
- type="submit"
- disabled={isSubmittingComment || !commentText.trim()}
- >
- {isSubmittingComment ? 'Enviando...' : 'Enviar'}
- </S.SubmitCommentButton>
- </S.CommentFooter>
- </S.CommentForm>
-
- {/* LISTA DE COMENTÁRIOS */}
- {comments.length > 0 && (
- <S.CommentsSection>
- {comments.map((comment) => {
- // Verifica se o usuário atual é o autor do comentário
- const isAuthor = currentUser?.id && String(currentUser.id) === String(comment.authorID);
-
- return (
- <S.CommentItem key={comment.commentID || Math.random()}>
- <S.CommentAvatar />
- <S.CommentBubble>
- <S.CommentHeader>
- <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
- <strong>{comment.username || "Usuário"}</strong>
- <span>{formatDate(comment.createdAt)}</span>
- </div>
-
- {/* Botão de Deletar (Só aparece para o dono) */}
- {isAuthor && (
- <S.DeleteCommentButton
- onClick={() => setCommentToDelete(comment.commentID!)}
- title="Excluir comentário"
- >
- <TrashIcon />
- </S.DeleteCommentButton>
- )}
- </S.CommentHeader>
- <S.CommentText>{comment.content}</S.CommentText>
- </S.CommentBubble>
- </S.CommentItem>
- );
- })}
- </S.CommentsSection>
- )}
- </>
- )}
-
- </S.PostContent>
- <Modal
- isOpen={isDeleteModalOpen}
- onClose={() => setIsDeleteModalOpen(false)}
- title={`Excluir ${deleteLabel}`}
- >
- <div style={{ textAlign: 'center' }}>
- <p style={{ marginBottom: '24px', color: '#555' }}>
- Tem certeza que deseja excluir este {deleteLabel?.toLowerCase()}?
- </p>
- <ModalS.ModalActions>
- <ModalS.ChoiceButton onClick={() => setIsDeleteModalOpen(false)}>
- Cancelar
- </ModalS.ChoiceButton>
- <ModalS.ChoiceButton onClick={handleDeleteMainItem} style={{ backgroundColor: '#e74c3c' }}>
- Excluir
- </ModalS.ChoiceButton>
- </ModalS.ModalActions>
- </div>
- </Modal>
-
- <Modal
- isOpen={!!commentToDelete}
- onClose={() => setCommentToDelete(null)}
- title="Excluir Comentário"
- >
- <div style={{ textAlign: 'center' }}>
- <p style={{ marginBottom: '24px', color: '#555' }}>
- Deseja realmente apagar este comentário?
- </p>
- <ModalS.ModalActions>
- <ModalS.ChoiceButton onClick={() => setCommentToDelete(null)}>
- Cancelar
- </ModalS.ChoiceButton>
- <ModalS.ChoiceButton
- onClick={handleConfirmDeleteInternalComment}
- style={{ backgroundColor: '#e74c3c' }}
- >
- Excluir
- </ModalS.ChoiceButton>
- </ModalS.ModalActions>
- </div>
- </Modal>
-
- </S.PostCardWrapper>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 91.07% | -51/56 | -83.33% | -30/36 | -78.94% | -15/19 | -90.9% | -50/55 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 | - - - - - - - - - - -47x -47x - -47x - -47x -47x - - -47x -47x - - -47x -47x - -47x -6x -6x -6x -6x - -6x - - - - -6x -6x - - - -6x - - - - - -47x -20x -8x -8x -8x - - -12x - - -12x -3x - - - - - -12x -3x - - - -12x -12x - - - - -47x - -1x -1x - - -6x -6x - - - -47x -1x -1x -1x - - -47x -1x -1x -1x - - -47x - -47x - - - - - - -7x - - - - - - - - - - - - - - - - - -1x - -1x - - - - - - - - - - - - - - -3x - -1x - - - - - - - - - - - - - - - | import React, { useState, useEffect, useRef } from 'react';
-import { FiSearch } from 'react-icons/fi';
-import * as S from './styles';
-import { useNavigate } from 'react-router-dom';
-
-import { GetAllCommunities } from '../../../API/Community';
-import { GetFeedProjects } from '../../../API/Project';
-import type { CommunityProps } from '../../../API/Community';
-import type { ProjectProps } from '../../../API/Project';
-
-export default function Searchbar() {
- const [query, setQuery] = useState('');
- const navigate = useNavigate();
-
- const wrapperRef = useRef<HTMLDivElement>(null);
-
- const [isFocused, setIsFocused] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
-
- // Dados brutos (cache local)
- const [allCommunities, setAllCommunities] = useState<CommunityProps[]>([]);
- const [allProjects, setAllProjects] = useState<ProjectProps[]>([]);
-
- // Dados filtrados para exibição
- const [filteredCommunities, setFilteredCommunities] = useState<CommunityProps[]>([]);
- const [filteredProjects, setFilteredProjects] = useState<ProjectProps[]>([]);
-
- const handleFocus = async () => {
- setIsFocused(true);
- Eif (allCommunities.length === 0 && allProjects.length === 0) {
- setIsLoading(true);
- try {
- // Busca em paralelo
- const [communitiesData, projectsData] = await Promise.all([
- GetAllCommunities().catch(() => []), // Se falhar, retorna array vazio
- GetFeedProjects().catch(() => [])
- ]);
-
- setAllCommunities(communitiesData);
- setAllProjects(projectsData);
- } catch (error) {
- console.error("Erro ao carregar dados para pesquisa", error);
- } finally {
- setIsLoading(false);
- }
- }
- };
-
- // Efeito para filtrar sempre que a query ou os dados mudarem
- useEffect(() => {
- if (!query.trim()) {
- setFilteredCommunities([]);
- setFilteredProjects([]);
- return;
- }
-
- const lowerQuery = query.toLowerCase();
-
- // Filtra Comunidades (pelo nome ou descrição ou tecnologias)
- const filteredComms = allCommunities.filter(comm =>
- comm.name.toLowerCase().includes(lowerQuery) ||
- comm.description?.toLowerCase().includes(lowerQuery) ||
- comm.technologies?.some(tech => tech.toLowerCase().includes(lowerQuery))
- );
-
- // Filtra Projetos (pelo título ou tecnologias)
- const filteredProjs = allProjects.filter(proj =>
- proj.title.toLowerCase().includes(lowerQuery) ||
- proj.technologies?.some(tech => tech.toLowerCase().includes(lowerQuery))
- );
-
- setFilteredCommunities(filteredComms.slice(0, 5)); // Limita a 5 resultados
- setFilteredProjects(filteredProjs.slice(0, 5)); // Limita a 5 resultados
-
- }, [query, allCommunities, allProjects]);
-
- // Fecha ao clicar fora
- useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- Eif (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
- setIsFocused(false);
- }
- }
- document.addEventListener('mousedown', handleClickOutside);
- return () => document.removeEventListener('mousedown', handleClickOutside);
- }, []);
-
- // Navegação
- const handleNavigateToCommunity = (communityId: string) => {
- navigate(`/r/${communityId}`);
- setIsFocused(false);
- setQuery('');
- };
-
- const handleNavigateToProject = (projectId: string) => {
- navigate(`/project/${projectId}`);
- console.log("Navegar para projeto:", projectId);
- setIsFocused(false);
- };
-
- const hasResults = filteredCommunities.length > 0 || filteredProjects.length > 0;
-
- return (
- <S.SearchWrapper ref={wrapperRef}>
- <FiSearch size={20} />
- <S.SearchInput
- type="text"
- placeholder="Busque comunidades e projetos..."
- value={query}
- onChange={(e) => setQuery(e.target.value)}
- onFocus={handleFocus}
- />
-
- {/* Renderiza o dropdown apenas se estiver focado e houver texto */}
- {isFocused && query.length > 0 && (
- <S.ResultsDropdown>
- {isLoading ? (
- <S.NoResults>Carregando...</S.NoResults>
- ) : !hasResults ? (
- <S.NoResults>Nenhum resultado encontrado.</S.NoResults>
- ) : (
- <>
- {/* Seção de Comunidades */}
- {filteredCommunities.length > 0 && (
- <S.ResultSection>
- <h4>Comunidades</h4>
- {filteredCommunities.map(comm => (
- <S.ResultItem
- key={comm.communityID}
- onClick={() => handleNavigateToCommunity(comm.communityID)}
- >
- <span>{comm.name}</span>
- {/* Opcional: mostrar tecnologia principal */}
- {comm.technologies?.[0] && <small>{comm.technologies[0]}</small>}
- </S.ResultItem>
- ))}
- </S.ResultSection>
- )}
-
- {/* Seção de Projetos */}
- {filteredProjects.length > 0 && (
- <S.ResultSection>
- <h4>Projetos</h4>
- {filteredProjects.map((proj: any) => (
- <S.ResultItem
- key={proj.id || proj.projectID}
- onClick={() => handleNavigateToProject(proj.id || proj.projectID)}
- >
- <span>{proj.title}</span>
- <small>por {proj.authorUsername || 'Usuário'}</small>
- </S.ResultItem>
- ))}
- </S.ResultSection>
- )}
- </>
- )}
- </S.ResultsDropdown>
- )}
- </S.SearchWrapper>
- );
-}
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 94.28% | -33/35 | -76.19% | -16/21 | -92.85% | -13/14 | -93.93% | -31/33 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 | - - - - - - - - - - - - - - - - - - - - - -20x -20x -20x -20x - -20x -9x -3x -11x - - -3x - -6x - - - - -20x - - - - - -5x -5x -5x - - - - -20x - -2x -1x -1x - -1x -1x -1x -1x -1x - - - - -20x -2x -1x - - -20x - - - - -18x - -1x - - - - - - - - - - - - - -3x - - - - - - -4x - - - - - - - - - - | import React, { useState, useEffect, useRef } from 'react';
-import * as S from '../CreationForm/styles';
-import { Keyword, KeywordContainer } from '../../common/Keyword';
-
-interface TagInputProps {
- value: string[];
- onChange: (value: string[]) => void;
-
- searchList: string[];
- limit: number;
- placeholder: string;
-}
-
-
-export default function TagInput({
- value: selectedTags = [],
- onChange,
- searchList,
- limit,
- placeholder
-}: TagInputProps) {
-
- const [searchQuery, setSearchQuery] = useState('');
- const [searchResults, setSearchResults] = useState<string[]>([]);
- const [error, setError] = useState('');
- const wrapperRef = useRef<HTMLDivElement>(null);
-
- useEffect(() => {
- if (searchQuery.length > 0) {
- const filtered = searchList.filter(item =>
- item.toLowerCase().includes(searchQuery.toLowerCase()) &&
- !selectedTags.includes(item) // Não mostra o que já foi selecionado
- );
- setSearchResults(filtered);
- } else {
- setSearchResults([]);
- }
- }, [searchQuery, selectedTags, searchList]);
-
- // Lógica para fechar a lista de resultados ao clicar fora
- useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
- setSearchResults([]); // Fecha a lista
- }
- }
- document.addEventListener('mousedown', handleClickOutside);
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, [wrapperRef]);
-
- // Adiciona uma tag
- const handleAddTag = (tag: string) => {
- // Usa o 'limit' da prop
- if (selectedTags.length >= limit) {
- setError(`Limite de ${limit} itens atingido.`);
- return;
- }
- Eif (!selectedTags.includes(tag)) {
- onChange([...selectedTags, tag]); // Atualiza o react-hook-form
- setSearchQuery('');
- setSearchResults([]);
- setError('');
- }
- };
-
- // Remove uma tag
- const handleRemoveTag = (tagToRemove: string) => {
- onChange(selectedTags.filter(tag => tag !== tagToRemove));
- setError('');
- };
-
- return (
- <S.SearchWrapper ref={wrapperRef}>
- {selectedTags.length > 0 && (
- <KeywordContainer>
- {selectedTags.map(tag => (
- <Keyword
- key={tag}
- onRemove={() => handleRemoveTag(tag)}
- >
- {tag}
- </Keyword>
- ))}
- </KeywordContainer>
- )}
-
- <div style={{ height: selectedTags.length > 0 ? '12px' : '0' }} />
-
- <S.Input
- type="text"
- placeholder={placeholder}
- value={searchQuery}
- onChange={(e) => setSearchQuery(e.target.value)}
- disabled={selectedTags.length >= limit}
- />
-
- {searchResults.length > 0 && (
- <S.SearchResultsList>
- {searchResults.map(tag => (
- <S.SearchResultItem key={tag} onClick={() => handleAddTag(tag)}>
- {tag}
- </S.SearchResultItem>
- ))}
- </S.SearchResultsList>
- )}
-
- {error && <S.ErrorMessage>{error}</S.ErrorMessage>}
- </S.SearchWrapper>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 100% | -1/1 | -100% | -0/0 | -100% | -1/1 | -100% | -1/1 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 | - - - - - - - - - -2x - - - - - - - - - - - - - - - - - | import { FiPlus } from 'react-icons/fi';
-import Searchbar from '../../domain/Searchbar';
-import * as S from './styles';
-
-
-interface HeaderProps {
- onCreateClick: () => void;
-}
-
-export default function Header({ onCreateClick }: HeaderProps) {
- return (
- <S.HeaderContainer>
- <S.SearchContainer>
- <Searchbar />
- </S.SearchContainer>
-
- <S.ActionsContainer>
- <S.CreateButton onClick={onCreateClick}>
- <FiPlus size={20} />
- <span>Create</span>
- </S.CreateButton>
-
- <S.ProfileIcon to={`/profile`}>
- </S.ProfileIcon>
- </S.ActionsContainer>
- </S.HeaderContainer>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 100% | -1/1 | -100% | -0/0 | -100% | -1/1 | -100% | -1/1 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 | - - -1x - - - - - - - - - - - - - - | import * as S from './styles';
-
-export default function HeaderHome() {
- return (
- <S.HeaderContainer>
-
- <S.ActionsContainer>
- <S.Button to="/login">
- Login
- </S.Button>
-
- <S.Button to="/register">
- Register
- </S.Button>
- </S.ActionsContainer>
- </S.HeaderContainer>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 93.75% | -15/16 | -75% | -3/4 | -100% | -5/5 | -93.75% | -15/16 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 | - - - - - - - - - -9x -9x -9x - -9x -2x - - -9x - -8x -8x - -8x -8x -8x - - - - - - -8x -8x - - -9x - - - - - - - - - - - - - - - - -4x - - - - - - - - - - - - - - - - - - | import { useState } from 'react';
-import { FiHome, FiChevronDown } from 'react-icons/fi';
-import { GetUserCommunities } from '../../../API/Community';
-import * as S from './styles';
-import type { CommunityProps } from '../../../API/Community';
-import { useAuth } from '../../../API/AuthContext';
-import { useEffect } from 'react';
-
-export default function Sidebar() {
- // Estado para controlar se a lista de comunidades está visível
- const [isCommunitiesOpen, setIsCommunitiesOpen] = useState(true);
- const { currentUser } = useAuth();
- const [userCommunities, setUserCommunities] = useState<CommunityProps[]>([]);
-
- const toggleCommunities = () => {
- setIsCommunitiesOpen(!isCommunitiesOpen);
- };
-
- useEffect(() => {
- // Função assíncrona para buscar todos os dados
- const fetchCommunities = async () => {
- try {;
-
- const apiUserCommunities = await GetUserCommunities();
- console.log("Comunidades do usuário:", apiUserCommunities);
- setUserCommunities(apiUserCommunities);
-
- } catch (error) {
- console.error("Falha ao buscar comunidades:", error);
- }
-
- };
- Eif(currentUser)
- fetchCommunities();
- }, [currentUser]);
-
- return (
- <S.SidebarContainer>
- <S.SidebarNav>
-
- <S.HomeLink to="/feed">
- <FiHome size={22} />
- <span>Home</span>
- </S.HomeLink>
-
- <S.CommunitiesHeader onClick={toggleCommunities} isOpen={isCommunitiesOpen}>
- <span>COMUNIDADES</span>
- <FiChevronDown size={20} />
- </S.CommunitiesHeader>
-
- {isCommunitiesOpen && (
- <S.CommunitiesList>
- {userCommunities.map((community) => (
- <S.CommunityLink
- to={`/r/${community.communityID}`}
- key={community.communityID}
- >
- <S.CommunityIcon
- alt={`${community.name} icon`}
- />
- <span>{community.name}</span>
- </S.CommunityLink>
- ))}
- </S.CommunitiesList>
- )}
-
- </S.SidebarNav>
- </S.SidebarContainer>
- );
-}
-
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| App.tsx | -
-
- |
- 100% | -1/1 | -100% | -0/0 | -100% | -1/1 | -100% | -1/1 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 | - - - - - - - - - - - - - - -14x -14x - -14x -14x -14x - -14x -14x -14x - -14x - -14x - -4x -4x -4x -4x -4x -4x - - - - -4x - - -14x - - - - - -6x -6x - - -14x -1x - -1x -1x - - -1x - - - - - -1x - - - - - - - - -14x - - - -14x -1x -1x -1x -1x -1x - - -1x - - - - - - - - - - - -14x -1x - -1x -1x - - -1x - - - - - -1x -1x - - - - - - - - - -14x - -10x - - - - - - - - - - - - - - - - - - - - - - - - - - - - -1x - - - - - -1x - - - - - - - - - - -1x -1x - - - - - - - - - - - - - -10x - - - - - - - - - - - - -10x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | import { useParams, useNavigate } from 'react-router-dom';
-import Sidebar from '../../components/layout/Sidebar';
-import Postcard from '../../components/domain/Postcard';
-import * as S from './styles';
-import { FiMoreHorizontal } from 'react-icons/fi';
-import { useEffect, useState, useRef } from 'react';
-import { GetCommunityById, JoinCommunity, DeleteCommunity, LeaveCommunity } from '../../API/Community';
-import type { NotificationState } from '../../components/common/Toast';
-import Toast from '../../components/common/Toast';
-import type { CommunityProps } from '../../API/Community';
-import * as D from '../../components/common/Dropdown/styles';
-import Modal from '../../components/common/Modal';
-import * as ModalS from '../../components/common/Modal/styles';
-
-export default function CommunityPage() {
- const { communityID } = useParams<{ communityID: string }>();
- const navigate = useNavigate();
-
- const [notification, setNotification] = useState<NotificationState | null>(null);
- const [community, setCommunity] = useState<any>(null);
- const [posts, setPosts] = useState<any[]>([]);
-
- const [isMenuOpen, setIsMenuOpen] = useState(false);
- const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
- const menuRef = useRef<HTMLDivElement>(null);
-
- const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false);
-
- useEffect(() => {
- async function loadData() {
- console.log("Carregando dados da comunidade para ID:", communityID);
- Iif (!communityID) return;
- try {
- const data = await GetCommunityById(communityID);
- setCommunity(data.community);
- setPosts(data.posts);
- } catch (error) {
- console.error(error);
- }
- }
- loadData();
- }, [communityID]);
-
- useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
- setIsMenuOpen(false);
- }
- }
- if (isMenuOpen) document.addEventListener('mousedown', handleClickOutside);
- return () => document.removeEventListener('mousedown', handleClickOutside);
- }, [isMenuOpen]);
-
- const handleJoin = async () => {
- Iif (!community) return;
-
- try {
- await JoinCommunity(community.communityID);
-
- // Atualiza a interface após entrar na comunidade
- setCommunity((prev: CommunityProps | null) => prev ? ({
- ...prev,
- isMember: true, // Esconde o botão
- memberCount: (prev.memberCount || 0) + 1 // Atualiza o contador visualmente
- }) : null);
-
- setNotification({ message: 'Você entrou na comunidade!', type: 'success' });
-
- } catch (error) {
- if (error instanceof Error) {
- setNotification({ message: error.message, type: 'error' });
- }
- }
- };
-
- const handleEdit = () => {
- navigate('/editCommunity', { state: { communityToEdit: community } });
- };
-
- const handleDelete = async () => {
- Iif (!community) return;
- try {
- await DeleteCommunity(community.communityID);
- setNotification({ message: 'Comunidade excluída com sucesso.', type: 'success' });
- setIsDeleteModalOpen(false);
-
- // Redireciona para home após excluir
- setTimeout(() => {
- navigate('/feed');
- }, 1500);
-
- } catch (error) {
- if (error instanceof Error) {
- setNotification({ message: error.message, type: 'error' });
- }
- setIsDeleteModalOpen(false);
- }
- };
-
- const handleLeave = async () => {
- Iif (!community) return;
-
- try {
- await LeaveCommunity(community.communityID);
-
- // Atualiza a interface otimisticamente
- setCommunity((prev: CommunityProps | null) => prev ? ({
- ...prev,
- isMember: false, // O usuário não é mais membro
- memberCount: Math.max((prev.memberCount || 0) - 1, 0) // Decrementa contador
- }) : null);
-
- setNotification({ message: 'Você saiu da comunidade.', type: 'success' });
- setIsLeaveModalOpen(false); // Fecha o modal
-
- } catch (error) {
- if (error instanceof Error) {
- setNotification({ message: error.message, type: 'error' });
- }
- setIsLeaveModalOpen(false);
- }
- };
-
- if (!community) return <div>Comunidade não encontrada</div>;
-
- return (
- <S.PageWrapper>
- <Sidebar />
-
- {notification && (
- <Toast
- message={notification.message}
- type={notification.type}
- onClose={() => setNotification(null)}
- />
- )}
-
- <S.MainContent>
- <S.Banner />
-
- <S.HeaderContainer>
- <S.Avatar />
- <S.HeaderInfo>
- <h1>{community.name}</h1>
- <span>{community.memberCount} membros</span>
- </S.HeaderInfo>
-
- <S.HeaderActions>
- {!community.isMember && (
- <S.JoinButton onClick={handleJoin}>
- Join
- </S.JoinButton>
- )}
- {community.isMember && !community.isAdmin && (
- <S.LeaveButton onClick={() => setIsLeaveModalOpen(true)}>
- Sair
- </S.LeaveButton>
- )}
- {community.isAdmin && (
- <S.MenuWrapper ref={menuRef}>
- <S.OptionsButton onClick={() => setIsMenuOpen(!isMenuOpen)}>
- <FiMoreHorizontal />
- </S.OptionsButton>
-
- {isMenuOpen && (
- <D.DropdownMenu>
- <D.MenuItem onClick={handleEdit}>
- Editar Comunidade
- </D.MenuItem>
- <D.Separator />
- <D.DangerMenuItem onClick={() => {
- setIsMenuOpen(false);
- setIsDeleteModalOpen(true);
- }}>
- Excluir Comunidade
- </D.DangerMenuItem>
- </D.DropdownMenu>
- )}
- </S.MenuWrapper>
- )}
- </S.HeaderActions>
- </S.HeaderContainer>
-
- <S.ContentGrid>
- <S.FeedColumn>
- {posts.map(post => (
- <S.FeedCardWrapper key={post.projectID || post.id}>
- <Postcard post={post} showMenu={false} />
- </S.FeedCardWrapper>
- ))}
- </S.FeedColumn>
-
- <S.InfoSidebar>
- <h3>DESCRIPTION</h3>
- <p>{community.description}</p>
-
- <h3>KEYWORDS</h3>
- <S.KeywordsContainer>
- {community.technologies?.map((keyword: string) => (
- <S.KeywordTag key={keyword}>{keyword}</S.KeywordTag>
- ))}
- </S.KeywordsContainer>
- </S.InfoSidebar>
- </S.ContentGrid>
- </S.MainContent>
-
- <Modal
- isOpen={isDeleteModalOpen}
- onClose={() => setIsDeleteModalOpen(false)}
- title="Excluir Comunidade"
- >
- <div style={{ textAlign: 'center' }}>
- <p style={{ marginBottom: '24px', color: '#555' }}>
- Tem certeza que deseja excluir a comunidade <strong>{community.name}</strong>?<br/>
- Todos os posts e vínculos serão removidos.
- </p>
- <ModalS.ModalActions>
- <ModalS.ChoiceButton onClick={() => setIsDeleteModalOpen(false)}>
- Cancelar
- </ModalS.ChoiceButton>
- <ModalS.ChoiceButton onClick={handleDelete} style={{ backgroundColor: '#e74c3c' }}>
- Excluir
- </ModalS.ChoiceButton>
- </ModalS.ModalActions>
- </div>
- </Modal>
-
- <Modal
- isOpen={isLeaveModalOpen}
- onClose={() => setIsLeaveModalOpen(false)}
- title="Sair da Comunidade"
- >
- <div style={{ textAlign: 'center' }}>
- <p style={{ marginBottom: '24px', color: '#555' }}>
- Tem certeza que deseja sair da comunidade <strong>{community.name}</strong>?
- </p>
- <ModalS.ModalActions>
- <ModalS.ChoiceButton onClick={() => setIsLeaveModalOpen(false)}>
- Cancelar
- </ModalS.ChoiceButton>
- <ModalS.ChoiceButton
- onClick={handleLeave}
- style={{ backgroundColor: '#e74c3c' }} // Botão vermelho
- >
- Sair
- </ModalS.ChoiceButton>
- </ModalS.ModalActions>
- </div>
- </Modal>
- </S.PageWrapper>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 -437 -438 -439 -440 -441 -442 -443 -444 -445 -446 -447 -448 | - -1x - - -10x - - -1x - - - - - - - - - - - - - - - - - - - -1x - - -10x - - - - - - - - - - - - - - - - - - - - - - - - - - - -1x - - - - - - - - - - - - - - - - - - - - - - - - -1x - - - -10x -10x - - - - - - - - - - - - - - - - - - - - - - - - - - -1x - - - - - - - - - - -10x - - - - - -10x - - - - - - - - - - - - - - - - -10x - - - - - -10x - - - -10x - - - - - - - - - - - - - - -1x - - - - - - -1x - - -3x -3x -3x - - - - -3x - - - - - - - - - - - - - - - - - - - - - -3x -3x - - - - - - - -1x - - -3x - -3x - - - - - - - - - - - - - - -3x - - - - - - - - - - - - -3x -3x -3x - - - - - - - - - - - - - -1x - - - -4x -4x -4x - - - - - - - - - -4x -4x - -4x - - - - - - - - - -1x - - - - - - - - - - - - - -1x - - - - - - - - - - - - - - - - - - -1x -10x - - - - - - - - - - - - - -1x -10x -10x - - - - - - - - - - - - - - - - - - - - - - - - -10x - - - - - - - - - - - - - - - - - - -10x - - - - - - - - - -10x - - - - - - - - - - - -1x - - - - - - -1x -10x -10x - - - - -10x - - - - -10x - -10x - - - -1x - - - - - | import styled from 'styled-components';
-
-export const PageWrapper = styled.div`
- display: flex;
- min-height: 100vh;
- background: linear-gradient(135deg, ${props => props.theme.white} 0%, ${props => props.theme['gray-100'] || props.theme['gray-100']} 100%);
-`;
-
-export const MainContent = styled.main`
- flex: 1;
- margin-left: 250px;
- display: flex;
- flex-direction: column;
-
- animation: fadeIn 0.5s ease-out;
-
- @keyframes fadeIn {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
- }
-`;
-
-// --- Área do Topo (Banner + Avatar + Título) ---
-
-export const Banner = styled.div`
- width: 100%;
- height: 220px;
- background: linear-gradient(135deg, ${props => props.theme.button} 0%, ${props => props.theme['hover-button'] || props.theme.placeholder} 100%);
- position: relative;
- overflow: hidden;
-
- /* Padrão decorativo com formas geométricas */
- &::before {
- content: '';
- position: absolute;
- top: -50%;
- right: -10%;
- width: 600px;
- height: 600px;
- background: radial-gradient(circle, rgba(255, 255, 255, 0.15) 0%, transparent 70%);
- border-radius: 50%;
- }
-
- &::after {
- content: '';
- position: absolute;
- bottom: -30%;
- left: -5%;
- width: 400px;
- height: 400px;
- background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
- border-radius: 50%;
- }
-`;
-
-export const HeaderContainer = styled.div`
- display: flex;
- align-items: flex-end;
- padding: 0 40px;
- margin-top: -60px;
- margin-bottom: 32px;
- gap: 24px;
- position: relative;
- z-index: 2;
- flex-wrap: wrap;
-
- animation: slideUp 0.6s ease-out;
-
- @keyframes slideUp {
- from {
- opacity: 0;
- transform: translateY(30px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-`;
-
-export const Avatar = styled.div`
- width: 130px;
- height: 130px;
- border-radius: 24px;
- background: linear-gradient(135deg, ${props => props.theme.sidebar} 0%, ${props => props.theme.button} 100%);
- border: 5px solid ${props => props.theme.white};
- flex-shrink: 0;
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2),
- 0 4px 12px rgba(0, 0, 0, 0.15);
- position: relative;
-
- /* Efeito de brilho */
- &::after {
- content: '';
- position: absolute;
- top: 15%;
- left: 15%;
- width: 35%;
- height: 35%;
- background: radial-gradient(circle, rgba(255, 255, 255, 0.4) 0%, transparent 70%);
- border-radius: 50%;
- }
-
- transition: transform 0.3s ease, box-shadow 0.3s ease;
-
- &:hover {
- transform: scale(1.05) rotate(2deg);
- box-shadow: 0 15px 40px rgba(0, 0, 0, 0.25),
- 0 6px 16px rgba(0, 0, 0, 0.2);
- }
-`;
-
-export const HeaderInfo = styled.div`
- display: flex;
- flex-direction: column;
- padding-bottom: 12px;
- flex: 1;
- gap: 14px;
- min-width: 0;
-
- h1 {
- font-size: 2.8rem;
- font-weight: 800;
- color: ${props => props.theme.black};
- margin: 0;
- margin-bottom: 8px;
- line-height: 1.1;
- letter-spacing: -0.02em;
-
- background: linear-gradient(135deg, ${props => props.theme.white} 0%, ${props => props.theme.button} 100%);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
-
- position: relative;
- word-wrap: break-word;
- overflow-wrap: break-word;
-
- /* Underline decorativo */
- &::after {
- content: '';
- position: absolute;
- bottom: -10px;
- left: 0;
- width: 80px;
- height: 5px;
- background: linear-gradient(90deg, ${props => props.theme.button} 0%, transparent 100%);
- border-radius: 3px;
- }
- }
-
- span {
- color: ${props => props.theme['gray-600'] || props.theme['gray-500']};
- font-size: 1rem;
- font-weight: 600;
- padding: 6px 14px;
- background: ${props => props.theme['gray-100']};
- border-radius: 20px;
- width: fit-content;
- display: flex;
- align-items: center;
- gap: 6px;
-
- /* Ícone decorativo */
- &::before {
- content: '👥';
- font-size: 0.9em;
- }
- }
-`;
-
-export const HeaderActions = styled.div`
- display: flex;
- align-items: center;
- gap: 12px;
- padding-bottom: 15px;
-`;
-
-export const JoinButton = styled.button`
- padding: 12px 32px;
- border-radius: 12px;
- border: 2px solid ${props => props.theme.button};
- background: ${props => props.theme.button};
- color: ${props => props.theme.white};
- font-weight: 700;
- font-size: 1rem;
- cursor: pointer;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- box-shadow: 0 4px 12px ${props => props.theme.button}40;
- position: relative;
- overflow: hidden;
-
- /* Efeito de brilho deslizante */
- &::before {
- content: '';
- position: absolute;
- top: 0;
- left: -100%;
- width: 100%;
- height: 100%;
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
- transition: left 0.5s;
- }
-
- &:hover::before {
- left: 100%;
- }
-
- &:hover {
- transform: translateY(-3px);
- box-shadow: 0 8px 20px ${props => props.theme.button}50;
- background: ${props => props.theme['hover-button'] || props.theme.button};
- }
-
- &:active {
- transform: translateY(-1px);
- }
-`;
-
-export const LeaveButton = styled.button`
- padding: 12px 32px;
- border-radius: 12px;
- border: 2px solid ${props => props.theme['red-500']};
- background: transparent;
- color: ${props => props.theme['red-500']};
- font-weight: 700;
- font-size: 1rem;
- cursor: pointer;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- position: relative;
- overflow: hidden;
-
- &::before {
- content: '';
- position: absolute;
- top: 50%;
- left: 50%;
- width: 0;
- height: 0;
- background: ${props => props.theme['red-500']};
- border-radius: 50%;
- transform: translate(-50%, -50%);
- transition: width 0.4s, height 0.4s;
- z-index: -1;
- }
-
- &:hover::before {
- width: 300%;
- height: 300%;
- }
-
- &:hover {
- color: ${props => props.theme.white};
- border-color: ${props => props.theme['red-500']};
- box-shadow: 0 4px 16px ${props => props.theme['red-500']}40;
- transform: translateY(-2px);
- }
-
- &:active {
- transform: translateY(0);
- }
-
- > * {
- position: relative;
- z-index: 1;
- }
-`;
-
-export const OptionsButton = styled.button`
- width: 48px;
- height: 48px;
- border-radius: 12px;
- border: 2px solid ${props => props.theme.button};
- background: ${props => props.theme.white};
- color: ${props => props.theme.button};
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- font-size: 1.3rem;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
-
- &:hover {
- background-color: ${props => props.theme.button};
- color: ${props => props.theme.white};
- transform: scale(1.1) rotate(90deg);
- box-shadow: 0 4px 16px ${props => props.theme.button}30;
- }
-
- &:active {
- transform: scale(1.05) rotate(90deg);
- }
-`;
-
-// --- Grid de Conteúdo (Feed + Info Lateral) ---
-
-export const ContentGrid = styled.div`
- display: grid;
- grid-template-columns: 1fr 340px;
- gap: 28px;
- padding: 0 40px 40px 40px;
- max-width: 1400px;
- width: 100%;
- box-sizing: border-box;
-
- @media (max-width: 900px) {
- grid-template-columns: 1fr;
- }
-`;
-
-export const FeedColumn = styled.div`
- display: flex;
- flex-direction: column;
- gap: 20px;
-
- animation: slideInLeft 0.5s ease-out;
-
- @keyframes slideInLeft {
- from {
- opacity: 0;
- transform: translateX(-20px);
- }
- to {
- opacity: 1;
- transform: translateX(0);
- }
- }
-`;
-
-export const FeedCardWrapper = styled.div`
- background: linear-gradient(135deg, ${props => props.theme.background} 0%, ${props => props.theme['gray-100'] || props.theme.background} 100%);
- border-radius: 18px;
- padding: 10px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
- transition: all 0.3s ease;
-
- &:hover {
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
- transform: translateY(-2px);
- }
-`;
-
-// --- Sidebar de Informações (Direita) ---
-
-export const InfoSidebar = styled.aside`
- background: ${props => props.theme.white};
- border: 2px solid ${props => props.theme.button}25;
- border-radius: 20px;
- padding: 28px;
- height: fit-content;
- position: sticky;
- top: 20px;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
- overflow-wrap: break-word;
- word-wrap: break-word;
- word-break: break-word;
-
- animation: slideInRight 0.5s ease-out;
-
- @keyframes slideInRight {
- from {
- opacity: 0;
- transform: translateX(20px);
- }
- to {
- opacity: 1;
- transform: translateX(0);
- }
- }
-
- h3 {
- color: ${props => props.theme.title};
- font-size: 0.85rem;
- font-weight: 800;
- letter-spacing: 1.5px;
- text-transform: uppercase;
- margin-bottom: 16px;
- margin-top: 28px;
- position: relative;
- padding-left: 12px;
-
- /* Barra lateral decorativa */
- &::before {
- content: '';
- position: absolute;
- left: 0;
- top: 50%;
- transform: translateY(-50%);
- width: 4px;
- height: 18px;
- background: linear-gradient(180deg, ${props => props.theme.button} 0%, ${props => props.theme['hover-button'] || props.theme.button} 100%);
- border-radius: 2px;
- }
- }
-
- h3:first-child {
- margin-top: 0;
- }
-
- p {
- color: ${props => props.theme['gray-700'] || props.theme.black};
- font-size: 0.95rem;
- line-height: 1.7;
- margin-bottom: 16px;
- padding-left: 12px;
- word-wrap: break-word;
- overflow-wrap: break-word;
- word-break: break-word;
- white-space: pre-wrap;
- }
-`;
-
-export const KeywordsContainer = styled.div`
- display: flex;
- flex-wrap: wrap;
- gap: 10px;
- padding-left: 12px;
-`;
-
-export const KeywordTag = styled.span`
- border: 2px solid ${props => props.theme.keyword};
- color: ${props => props.theme.keyword};
- border-radius: 10px;
- padding: 8px 18px;
- font-size: 0.85rem;
- font-weight: 700;
- background: ${props => props.theme.keyword}10;
- transition: all 0.2s ease;
- cursor: default;
-
- &:hover {
- background: ${props => props.theme.keyword}20;
- transform: translateY(-2px);
- box-shadow: 0 4px 8px ${props => props.theme.keyword}20;
- }
-`;
-
-export const MenuWrapper = styled.div`
- position: relative;
- display: flex;
- align-items: center;
- z-index: 10;
-`; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 81.81% | -27/33 | -90.9% | -20/22 | -71.42% | -5/7 | -81.81% | -27/33 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 | - - - - - - - - - - - - - -7x -7x - -7x -7x - -7x - - - - - - - - - -7x -7x -7x -7x -7x - -7x - -2x -2x -2x - - - - -2x - - -7x -2x -2x - -1x -1x -1x - - -1x -1x -1x - - -2x - - - - - - - - - - - - -7x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -10x - - - - - - - - - - - - - - - - - | import {useState, useEffect} from 'react';
-import { useNavigate, useLocation } from 'react-router-dom';
-import { useForm, Controller } from 'react-hook-form';
-import Sidebar from '../../components/layout/Sidebar';
-import { PageWrapper, ContentWrapper } from '../Feed/styles';
-import * as S from '../../components/domain/CreationForm/styles';
-import TagInput from '../../components/domain/TagInput';
-import type { NotificationState } from '../../components/common/Toast';
-import { GetKeywords } from '../../API/Keywords';
-import Toast from '../../components/common/Toast';
-import type { CommunityProps } from '../../API/Community';
-import { NewCommunity, UpdateCommunity } from '../../API/Community';
-
-export default function CreateCommunity() {
- const navigate = useNavigate();
- const location = useLocation();
-
- const communityToEdit = location.state?.communityToEdit as CommunityProps | undefined;
- const isEditMode = !!communityToEdit;
-
- const { register, handleSubmit, control, watch } = useForm<CommunityProps>({
- // Preenche os valores iniciais com os dados da comunidade se estiver editando
- defaultValues: {
- communityID: communityToEdit?.communityID || "",
- name: communityToEdit?.name || "",
- description: communityToEdit?.description || "",
- technologies: communityToEdit?.technologies || []
- }
- });
-
- const descriptionValue = watch('description');
- const descriptionLength = descriptionValue ? descriptionValue.length : 0;
- const MAX_CHARS = 500;
- const [notification, setNotification] = useState<NotificationState | null>(null);
- const [keywords, setKeywords] = useState<string[]>([]);
-
- useEffect(() => {
- async function loadTechs() {
- try {
- const techsFromDB = await GetKeywords();
- setKeywords(techsFromDB);
- } catch (error) {
- console.error("Falha ao carregar tecnologias:", error);
- }
- }
- loadTechs();
- }, []);
-
- const onSubmit = (data: CommunityProps) => {
- try{
- if (isEditMode && communityToEdit) {
- // --- MODO EDIÇÃO ---
- console.log("Atualizando Comunidade:", communityToEdit.communityID, data);
- UpdateCommunity(communityToEdit.communityID, data);
- setNotification({ message: 'Comunidade atualizada com sucesso!', type: 'success' });
- } else {
- // --- MODO CRIAÇÃO ---
- console.log("Criando Comunidade:", data);
- NewCommunity(data);
- setNotification({ message: 'Comunidade criada com sucesso!', type: 'success' });
- }
-
- setTimeout(() => {
- navigate('/feed');
- }, 1000);
-
- } catch (error) {
- console.error('Erro ao criar comunidade:', error);
- if (error instanceof Error){
- setNotification({ message: error.message, type: 'error' });
- }
- }
-
- };
-
- return (
- <PageWrapper>
- <Sidebar />
-
- {notification && (
- <Toast
- message={notification.message}
- type={notification.type}
- onClose={() => setNotification(null)}
- />
- )}
-
- <ContentWrapper>
- <S.FormContainer onSubmit={handleSubmit(onSubmit)}>
-
- <h2>{isEditMode ? 'Editar Comunidade' : 'Criar Comunidade'}</h2>
-
- <S.InputGroup>
- <S.Label htmlFor="name">Nome da Comunidade</S.Label>
- <S.Input id="name" {...register('name', { required: true })} />
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="description">Descrição do Projeto</S.Label>
- <S.TextArea
- id="description"
- placeholder="Descreva sua comunidade..."
- maxLength={MAX_CHARS}
- {...register('description', {
- maxLength: {
- value: MAX_CHARS,
- message: `A descrição não pode exceder ${MAX_CHARS} caracteres`
- }
- })}
- />
- <S.CharacterCount>
- {descriptionLength} / {MAX_CHARS}
- </S.CharacterCount>
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="keywords">Palavras-chave</S.Label>
-
- <Controller
- name="technologies"
- control={control}
- render={({ field }) => (
- <TagInput
- value={field.value}
- onChange={field.onChange}
- searchList={keywords}
- limit={10}
- placeholder="Adicione até 10 palavras-chave..."
- />
- )}
- />
- </S.InputGroup>
-
- <S.SubmitButton type="submit">{isEditMode ? 'Salvar Alterações' : 'Criar Comunidade'}</S.SubmitButton>
-
- </S.FormContainer>
- </ContentWrapper>
- </PageWrapper>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 84.44% | -38/45 | -87.09% | -27/31 | -80% | -8/10 | -86.04% | -37/43 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 | - - - - - - - - - - - - - - - - - - - -13x -13x -13x - -13x - - -13x - -13x - -13x - - - -13x -13x -6x - -6x -6x - - -13x - - - - - - - - - - -13x -13x -13x -13x - - -13x - -4x -4x -4x - - - - -4x - - - -13x - -2x - - - - -2x -2x - -1x -1x -1x - - -1x -1x -1x - - - -2x - - - - - - - - - - - -13x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -15x - - - - - -2x - - - - - - - - - - - - -15x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | import { useState, useEffect } from 'react';
-import { useForm, Controller } from 'react-hook-form';
-import Sidebar from '../../components/layout/Sidebar';
-import { PageWrapper, ContentWrapper } from '../Feed/styles';
-import * as S from '../../components/domain/CreationForm/styles';
-import TagInput from '../../components/domain/TagInput';
-import { IMaskInput } from 'react-imask';
-import { NewProject, UpdateProject } from '../../API/Project';
-import type { ProjectProps } from '../../API/Project';
-import Toast from '../../components/common/Toast';
-import { useNavigate, useLocation, useParams } from 'react-router-dom';
-import type { NotificationState } from '../../components/common/Toast';
-import { GetKeywords } from '../../API/Keywords';
-import { parseDate } from '../../API/Project';
-
-interface ProjectFormProps extends Omit<ProjectProps, 'startDate'> {
- startDate: string;
-}
-
-export default function CreateProject() {
- const navigate = useNavigate();
- const location = useLocation(); // Hook para ler o state
- const [keywords, setKeywords] = useState<string[]>([]);
-
- const { projectId: paramId } = useParams<{ projectId?: string }>();
-
- // Recupera o projeto do estado
- const projectToEdit = location.state?.projectToEdit as (ProjectProps & { id?: string; projectID?: string }) | undefined;
- // Define se é modo edição
- const isEditMode = !!projectToEdit;
-
- const validProjectId = projectToEdit?.id
- || projectToEdit?.projectID
- || (paramId !== 'undefined' ? paramId : undefined);
-
- const formatDateToString = (date?: Date | string) => {
- if (!date) return "";
- const d = new Date(date);
- // Verifica se é data válida
- Iif (isNaN(d.getTime())) return "";
- return d.toLocaleDateString('pt-BR'); // Retorna "20/11/2025"
- };
-
- const { register, handleSubmit, control, watch } = useForm<ProjectFormProps>({
- // Preenche os valores padrão se estiver em modo de edição
- defaultValues: {
- title: projectToEdit?.title || "",
- description: projectToEdit?.description || "",
- technologies: projectToEdit?.technologies || [],
- status: projectToEdit?.status || "",
- startDate: formatDateToString(projectToEdit?.startDate)
- }
- });
-
- const descriptionValue = watch('description');
- const descriptionLength = descriptionValue ? descriptionValue.length : 0;
- const MAX_CHARS = 2500;
- const [notification, setNotification] = useState<NotificationState | null>(null);
-
- // Carrega as tecnologias disponíveis do backend
- useEffect(() => {
- async function loadTechs() {
- try {
- const techsFromDB = await GetKeywords();
- setKeywords(techsFromDB);
- } catch (error) {
- console.error("Falha ao carregar tecnologias:", error);
- }
- }
- loadTechs();
- }, []);
-
- // Lida com CRIAR ou ATUALIZAR
- const onSubmit = (data: ProjectFormProps) => {
-
- const finalData: ProjectProps = {
- ...data,
- startDate: parseDate(data.startDate)
- };
-
- try {
- if (isEditMode && validProjectId) {
- // MODO DE EDIÇÃO
- console.log("Atualizando Projeto:", validProjectId, finalData);
- UpdateProject(validProjectId, finalData);
- setNotification({ message: 'Projeto atualizado com sucesso!', type: 'success' });
- } else {
- // MODO DE CRIAÇÃO
- console.log("Criando Projeto:", finalData);
- NewProject(finalData);
- setNotification({ message: 'Projeto criado com sucesso!', type: 'success' });
- }
-
- // Redireciona após o sucesso
- setTimeout(() => {
- navigate('/feed');
- }, 1000);
-
- } catch(error) {
- console.error('Erro ao salvar projeto:', error);
- if (error instanceof Error){
- setNotification({ message: error.message, type: 'error' });
- }
- }
- };
-
- return (
- <PageWrapper>
- <Sidebar />
- {notification && (
- <Toast
- message={notification.message}
- type={notification.type}
- onClose={() => setNotification(null)}
- />
- )}
-
- <ContentWrapper>
- <S.FormContainer onSubmit={handleSubmit(onSubmit)}>
-
- <h2>{isEditMode ? 'Editar Projeto' : 'Criar Projeto'}</h2>
-
- <S.InputGroup>
- <S.Label htmlFor="title">Nome do Projeto</S.Label>
- <S.Input id="title" {...register('title', { required: true })} />
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="description">Descrição do Projeto</S.Label>
- <S.TextArea
- id="description"
- placeholder="Descreva seu projeto..."
- maxLength={MAX_CHARS}
- {...register('description', {
- maxLength: {
- value: MAX_CHARS,
- message: `A descrição não pode exceder ${MAX_CHARS} caracteres`
- }
- })}
- />
- <S.CharacterCount>
- {descriptionLength} / {MAX_CHARS}
- </S.CharacterCount>
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="startDate">Data de Início</S.Label>
- <Controller
- name="startDate"
- control={control}
- render={({ field: { onChange, value } }) => (
- <S.Input
- as={IMaskInput}
- mask="00/00/0000"
- id="startDate"
- placeholder="DD/MM/AAAA"
- value={value}
- onAccept={(value: string) => onChange(value)}
- disabled={isEditMode}
- />
- )}
- />
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="technologies">Tecnologias (Palavras-chave)</S.Label>
- <Controller
- name="technologies"
- control={control}
- render={({ field }) => (
- <TagInput
- value={field.value}
- onChange={field.onChange}
- searchList={keywords}
- limit={6}
- placeholder="Adicione até 6 tecnologias..."
- />
- )}
- />
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="status">Status</S.Label>
- <S.SelectWrapper>
- <S.Select
- id="status"
- {...register('status', { required: true })}
- required
- >
- <option value="" disabled>Selecione um status...</option>
- <option value="em-andamento">Em andamento</option>
- <option value="pausado">Pausado</option>
- <option value="finalizado">Finalizado</option>
- </S.Select>
- </S.SelectWrapper>
- </S.InputGroup>
-
- {/* 10. Botão de submit dinâmico */}
- <S.SubmitButton type="submit">
- {isEditMode ? 'Atualizar Projeto' : 'Criar Projeto'}
- </S.SubmitButton>
-
- </S.FormContainer>
- </ContentWrapper>
- </PageWrapper>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 87.09% | -27/31 | -55.55% | -10/18 | -66.66% | -4/6 | -93.1% | -27/29 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 | - - - - - - - - - - - - -8x -8x -8x - - -8x -3x -3x -3x -3x - - -8x - - -8x -3x -3x -3x -3x -3x -3x - - - -8x -2x -2x -2x - -1x - -1x - - -1x - - - - -1x -1x -1x - - - - -8x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | import { useState, useEffect } from 'react';
-import { useForm } from 'react-hook-form';
-import { useNavigate } from 'react-router-dom';
-import Sidebar from '../../components/layout/Sidebar';
-import { PageWrapper, ContentWrapper } from '../Feed/styles';
-import * as S from '../../components/domain/CreationForm/styles';
-import Toast from '../../components/common/Toast';
-import type { NotificationState } from '../../components/common/Toast';
-import { useAuth } from '../../API/AuthContext';
-import { UpdateProfile } from '../../API/User';
-import type { UserProfileData } from '../../API/User';
-
-export default function EditProfile() {
- const { currentUser, updateUser } = useAuth();
- const navigate = useNavigate();
- const [notification, setNotification] = useState<NotificationState | null>(null);
-
- // Formata a data para o input (YYYY-MM-DD)
-const formatDateForInput = (dateString?: string) => {
- Iif (!dateString) return "";
- const date = new Date(dateString);
- Iif (isNaN(date.getTime())) return "";
- return date.toISOString().split('T')[0];
-};
-
- const { register, handleSubmit, setValue } = useForm<UserProfileData>();
-
- // Preenche o formulário com os dados atuais
- useEffect(() => {
- Eif (currentUser) {
- setValue('nomeCompleto', currentUser.nomeCompleto || '');
- setValue('username', currentUser.username || '');
- setValue('email', currentUser.email || '');
- setValue('telefone', (currentUser as any).phone || '');
- setValue('dataNascimento', formatDateForInput((currentUser as any).birthDate));
- }
- }, [currentUser, setValue]);
-
- const onSubmit = async (data: UserProfileData) => {
- try {
- console.log("Atualizando perfil:", data);
- await UpdateProfile(data);
-
- setNotification({ message: 'Perfil atualizado com sucesso!', type: 'success' });
-
- updateUser(data);
-
- // Redireciona
- setTimeout(() => {
- navigate('/profile');
- }, 1000);
-
- } catch (error) {
- console.error(error);
- Eif (error instanceof Error) {
- setNotification({ message: error.message, type: 'error' });
- }
- }
- };
-
- return (
- <PageWrapper>
- <Sidebar />
- {notification && (
- <Toast
- message={notification.message}
- type={notification.type}
- onClose={() => setNotification(null)}
- />
- )}
-
- <ContentWrapper>
- <S.FormContainer onSubmit={handleSubmit(onSubmit)}>
- <h2>Editar Perfil</h2>
-
- <S.InputGroup>
- <S.Label htmlFor="nomeCompleto">Nome Completo</S.Label>
- <S.Input
- id="nomeCompleto"
- {...register('nomeCompleto', { required: true })}
- />
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="username">Nome de Usuário</S.Label>
- <S.Input
- id="username"
- {...register('username', { required: true })}
- />
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="email">Email</S.Label>
- <S.Input
- id="email"
- type="email"
- {...register('email', { required: true })}
- />
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="telefone">Telefone</S.Label>
- <S.Input
- id="telefone"
- {...register('telefone')}
- />
- </S.InputGroup>
-
- <S.InputGroup>
- <S.Label htmlFor="dataNascimento">Data de Nascimento</S.Label>
- <S.Input
- id="dataNascimento"
- type="date"
- {...register('dataNascimento')}
- />
- </S.InputGroup>
-
- <S.SubmitButton type="submit">
- Salvar Alterações
- </S.SubmitButton>
-
- </S.FormContainer>
- </ContentWrapper>
- </PageWrapper>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 | - - - - - - - - - - - - - -18x -18x -18x -18x -18x - -18x - -6x -6x -6x -5x - -1x -1x -1x - - -6x - - -6x - - -18x -1x -1x - - -18x -1x -1x - - -18x - - - - - - - - - -2x - - - - - - - - - - - - -2x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | import { useState, useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
-import Modal from '../../components/common/Modal';
-import Postcard from '../../components/domain/Postcard';
-import Header from '../../components/layout/Header';
-import Sidebar from '../../components/layout/Sidebar';
-import * as S from './styles';
-import * as ModalS from '../../components/common/Modal/styles';
-import { GetFeedProjects } from '../../API/Project';
-import type { ProjectProps } from '../../API/Project';
-import Toast from '../../components/common/Toast';
-import type { NotificationState } from '../../components/common/Toast';
-
-export default function Feed() {
- const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
- const [isLoading, setIsLoading] = useState(true);
- const navigate = useNavigate();
- const [posts, setPosts] = useState<ProjectProps[]>([]);
- const [notification, setNotification] = useState<NotificationState | null>(null);
-
- useEffect(() => {
- async function loadFeed() {
- setIsLoading(true);
- try {
- const feedData = await GetFeedProjects();
- setPosts(feedData || []);
- } catch (error) {
- console.error("Erro ao carregar feed:", error);
- Eif (error instanceof Error) {
- setNotification({ message: "Não foi possível carregar o feed.", type: 'error' });
- }
- } finally {
- setIsLoading(false);
- }
- }
- loadFeed();
- }, []);
-
- const handleCreateProject = () => {
- setIsCreateModalOpen(false);
- navigate('/createProject');
- };
-
- const handleCreateCommunity = () => {
- setIsCreateModalOpen(false);
- navigate('/createCommunity');
- };
-
- return (
- <S.PageWrapper>
- {notification && (
- <Toast
- message={notification.message}
- type={notification.type}
- onClose={() => setNotification(null)}
- />
- )}
-
- <Header onCreateClick={() => setIsCreateModalOpen(true)}/>
- <Sidebar />
-
- <S.ContentWrapper>
- <S.FeedContainer>
- <S.PostList>
- {isLoading ? (
- <S.LoadingContainer>
- <div className="spinner"></div>
- <p>Carregando feed...</p>
- </S.LoadingContainer>
- ) : posts.length > 0 ? (
- posts.map((post, index) => (
- <Postcard
- key={(post as unknown as ProjectProps).id || (post as any).projectID || index}
- post={post}
- showMenu={false}
- />
- ))
- ) : (
- <S.EmptyFeedMessage>
- <svg
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- strokeWidth="2"
- strokeLinecap="round"
- strokeLinejoin="round"
- >
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
- <line x1="9" y1="9" x2="15" y2="15"/>
- <line x1="15" y1="9" x2="9" y2="15"/>
- </svg>
- <h3>Nenhum post encontrado</h3>
- <p style={{ color: '#ccc', textAlign: 'center', marginTop: '20px' }}>
- Nenhum post encontrado. Entre em comunidades para ver atualizações!
- </p>
- </S.EmptyFeedMessage>
- )}
- </S.PostList>
- </S.FeedContainer>
- </S.ContentWrapper>
-
- <Modal
- isOpen={isCreateModalOpen}
- onClose={() => setIsCreateModalOpen(false)}
- title="O que você deseja criar?"
- >
- <ModalS.ModalActions>
- <ModalS.ChoiceButton onClick={handleCreateProject}>
- Criar Projeto
- </ModalS.ChoiceButton>
- <ModalS.ChoiceButton onClick={handleCreateCommunity}>
- Criar Comunidade
- </ModalS.ChoiceButton>
- </ModalS.ModalActions>
- </Modal>
- </S.PageWrapper>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 | - - -4x -44x - - - - - - - - - - -44x -44x - - - - - - -4x - - - - - - - - - - - - - - - - - - - - - - - - - - -4x - - - -16x - - - - - - -16x - - - - - - - - - - - - - - - - - - - - - - - - -16x - - - - - - - - - -4x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -4x - - -5x - - - - - - - - - - - - - -5x - - - - - - -5x - - - - - - - - -4x - - - - - - - - - - - - - - - - - - - - - - - -4x - - - - - - - - - - -10x -10x - - - - - - - - - -10x - - - | import styled from 'styled-components';
-
-// Wrapper para a página inteira
-export const PageWrapper = styled.div`
- background: linear-gradient(135deg, ${props => props.theme['gray-100']} 0%, ${props => props.theme['gray-100']}f0 100%);
- min-height: 100vh;
- position: relative;
-
- &::before {
- content: '';
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: radial-gradient(circle at 20% 50%, ${props => props.theme.keyword}08 0%, transparent 50%),
- radial-gradient(circle at 80% 80%, ${props => props.theme.button}08 0%, transparent 50%);
- pointer-events: none;
- z-index: 0;
- }
-`;
-
-// Wrapper para o conteúdo principal
-export const ContentWrapper = styled.main`
- margin-left: 250px;
- margin-top: 60px;
- padding: 32px 24px;
- box-sizing: border-box;
- position: relative;
- z-index: 1;
- animation: fadeIn 0.5s ease-out;
-
- @keyframes fadeIn {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-
- @media (max-width: 768px) {
- margin-left: 0;
- padding: 20px 16px;
- }
-`;
-
-// Container principal do feed com posts
-export const FeedContainer = styled.main`
- width: 100%;
- max-width: 800px;
- margin: 0 auto;
- background: ${props => props.theme.background};
- backdrop-filter: blur(10px);
- border-radius: 20px;
- padding: 28px;
- box-sizing: border-box;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12),
- 0 2px 8px rgba(0, 0, 0, 0.08),
- 0 0 0 1px ${props => props.theme.placeholder}15;
- position: relative;
- transition: all 0.3s ease;
-
- &::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 3px;
-
- background-size: 200% 100%;
- border-radius: 20px 20px 0 0;
- animation: gradientShift 3s ease infinite;
- }
-
- @keyframes gradientShift {
- 0%, 100% { background-position: 0% 50%; }
- 50% { background-position: 100% 50%; }
- }
-
- &:hover {
- box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15),
- 0 4px 12px rgba(0, 0, 0, 0.1),
- 0 0 0 1px ${props => props.theme.placeholder}25;
- }
-
- @media (max-width: 768px) {
- padding: 20px;
- border-radius: 16px;
- }
-`;
-
-// Lista que agrupa os posts
-export const PostList = styled.div`
- display: flex;
- flex-direction: column;
- gap: 20px;
- position: relative;
-
- /* Animação de entrada para cada post */
- & > * {
- animation: slideInUp 0.4s ease-out backwards;
- }
-
- & > *:nth-child(1) { animation-delay: 0.05s; }
- & > *:nth-child(2) { animation-delay: 0.1s; }
- & > *:nth-child(3) { animation-delay: 0.15s; }
- & > *:nth-child(4) { animation-delay: 0.2s; }
- & > *:nth-child(5) { animation-delay: 0.25s; }
-
- @keyframes slideInUp {
- from {
- opacity: 0;
- transform: translateY(20px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-`;
-
-// Mensagem de feed vazio
-export const EmptyFeedMessage = styled.div`
- text-align: center;
- padding: 60px 24px;
- color: ${props => props.theme.placeholder};
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 16px;
-
- svg {
- width: 80px;
- height: 80px;
- opacity: 0.5;
- margin-bottom: 8px;
- }
-
- h3 {
- color: ${props => props.theme.subtitle};
- font-size: 1.3rem;
- font-weight: 600;
- margin: 0 0 8px 0;
- }
-
- p {
- color: ${props => props.theme.placeholder};
- font-size: 0.95rem;
- line-height: 1.5;
- max-width: 400px;
- margin: 0;
- }
-`;
-
-// Header do feed
-export const FeedHeader = styled.div`
- margin-bottom: 24px;
- padding-bottom: 20px;
- border-bottom: 1px solid ${props => props.theme.placeholder}20;
-
- h1 {
- color: ${props => props.theme.white};
- font-size: 1.8rem;
- font-weight: 700;
- margin: 0 0 8px 0;
- background: linear-gradient(135deg, ${props => props.theme.white}, ${props => props.theme.subtitle});
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
- }
-
- p {
- color: ${props => props.theme.placeholder};
- font-size: 0.9rem;
- margin: 0;
- }
-`;
-
-// Container para loading state
-export const LoadingContainer = styled.div`
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 60px 24px;
- gap: 16px;
-
- .spinner {
- width: 40px;
- height: 40px;
- border: 3px solid ${props => props.theme.placeholder}30;
- border-top-color: ${props => props.theme.keyword};
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- }
-
- @keyframes spin {
- to { transform: rotate(360deg); }
- }
-
- p {
- color: ${props => props.theme.placeholder};
- font-size: 0.9rem;
- }
-`; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 100% | -1/1 | -100% | -0/0 | -100% | -1/1 | -100% | -1/1 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 | - - - -1x - - - - - - - - - | import HeaderHome from '../../components/layout/HeaderHome';
-import * as S from './styles';
-
-export default function Home() {
- return (
- <S.HomePageContainer>
- <HeaderHome />
- <S.ContentWrapper>
- <S.SiteTitle>CTable</S.SiteTitle>
- <S.Tagline>Descubra, compartilhe, aprenda, converse: um novo mundo se abre para os amantes de computação.</S.Tagline>
- </S.ContentWrapper>
- </S.HomePageContainer>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 87.5% | -14/16 | -75% | -3/4 | -50% | -2/4 | -87.5% | -14/16 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 | - - - - - - - - - - - - - - - - - -10x -10x - -10x -10x - - -2x -2x -2x - -1x - - -1x - -1x - - - - -1x - - -1x -1x - - - - -10x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | import {useState} from 'react';
-import {
- FormPageContainer,
- FormWrapper,
- FormTitle,
- StyledForm,
- StyledInput,
- SubmitButton,
- RedirectLink
-} from '../../components/domain/Form/styles';
-import { useForm} from 'react-hook-form';
-import Toast from '../../components/common/Toast';
-import { useNavigate } from 'react-router-dom';
-import type { LoginProps } from '../../API/Auth';
-import { useAuth } from '../../API/AuthContext';
-import type { NotificationState } from '../../components/common/Toast';
-
-export default function LoginPage() {
- const [notification, setNotification] = useState<NotificationState | null>(null);
- const { login } = useAuth();
-
- const { register, handleSubmit, formState: {isSubmitting} } = useForm<LoginProps>();
- const navigate = useNavigate();
-
- async function onSubmit(data: LoginProps) {
- console.log(data);
- try{
- await login(data);
-
- console.log('Usuário registrado com sucesso');
-
- // Define estado para mostrar notificação de sucesso
- setNotification({ message: 'Usuário registrado com sucesso!', type: 'success' });
-
- setTimeout(() => {
- navigate('/feed'); // Navega para a página de feed
- }, 1000);
-
- } catch (error) {
- console.error('Erro ao registrar usuário:', error);
-
- // Define estado para mostrar notificação de erro
- Eif (error instanceof Error){
- setNotification({ message: error.message, type: 'error' });
- }
- }
- }
-
- return (
- <FormPageContainer>
- {notification && (
- <Toast
- message={notification.message}
- type={notification.type}
- onClose={() => setNotification(null)}
- />
- )}
- <FormWrapper>
- <FormTitle>Login</FormTitle>
-
- <StyledForm onSubmit={handleSubmit(onSubmit)}>
- <StyledInput
- type="text"
- placeholder="Email ou usuário"
- required
- {...register('username')}
- />
- <StyledInput
- type="password"
- placeholder="Senha"
- required
- {...register('senha')}
- />
-
- <SubmitButton disabled = {isSubmitting} type="submit">Entrar</SubmitButton>
- </StyledForm>
-
- <RedirectLink>
- Não tem uma conta? <a href="/register">Cadastre-se</a>
- </RedirectLink>
- </FormWrapper>
- </FormPageContainer>
- );
-}
-
- |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 | - - - - - - - - - - - - - - - - -1x -40x - - - - -1x -40x - - - -1x -40x - - - - - - - - -42x -42x -42x -42x - -42x - - -42x -42x - -42x -42x -42x - -42x - -40x -40x -40x - -40x -40x - -39x - -39x -39x - - -1x - - - -40x -40x - - - -42x - -1x -1x - - -25x -7x - -25x -25x - - - -42x -1x -1x - - -42x -1x - - -1x -1x - - - -42x -1x -1x - - -42x - -3x -3x - -3x -3x -1x - -1x - -1x - - - - - -2x -2x -1x -1x -1x - -1x - -1x - -3x -3x - - - - -42x -42x -37x -35x - - -2x -2x - - - - - -5x - -5x -2x - - - - - - - - - - - - - -1x - - - - - - - - -42x - - - - - - - - - - - - - - - - - - - - - - - - - - -2x - - - - - -7x - - - - - - - - - - - -4x -4x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -1x - - - - - - - - - - - - - - - - - | import { useState, useRef, useEffect } from 'react';
-import Sidebar from '../../components/layout/Sidebar';
-import Postcard from '../../components/domain/Postcard';
-import * as S from './styles';
-import * as D from '../../components/common/Dropdown/styles';
-import type { ProjectProps } from '../../API/Project';
-import type { CommentProps } from '../../API/Comment';
-import { GetUserProjects } from '../../API/Project';
-import { GetUserComments, DeleteComment } from '../../API/Comment';
-import { useAuth } from '../../API/AuthContext';
-import { useNavigate } from 'react-router-dom';
-import { DeleteProfile } from '../../API/User';
-import Toast from '../../components/common/Toast';
-import Modal from '../../components/common/Modal';
-import * as ModalS from '../../components/common/Modal/styles';
-
-// --- ÍCONES ---
-const PostsIcon = () => (
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
- <polyline points="14 2 14 8 20 8" /><line x1="16" y1="13" x2="8" y2="13" /><line x1="16" y1="17" x2="8" y2="17" /><line x1="10" y1="9" x2="8" y2="9" />
- </svg>
-);
-const CommentsIcon = () => (
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
- </svg>
-);
-const SettingsIcon = () => (
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" />
- </svg>
-);
-// -----------------------
-
-type ViewState = 'posts' | 'comments';
-
-export default function Profile() {
- const { currentUser, logout } = useAuth();
- const [view, setView] = useState<ViewState>('posts');
- const [isMenuOpen, setIsMenuOpen] = useState(false);
- const menuRef = useRef<HTMLDivElement>(null);
-
- const navigate = useNavigate();
-
- // Estados para os dados da API
- const [userPosts, setUserPosts] = useState<ProjectProps[]>([]);
- const [userComments, setUserComments] = useState<CommentProps[]>([]);
-
- const [isDeleting, setIsDeleting] = useState(false);
- const [isDeleteProfileModalOpen, setIsDeleteProfileModalOpen] = useState(false);
- const [notification, setNotification] = useState<{message: string, type: 'success' | 'error'} | null>(null);
-
- useEffect(() => {
- // Função assíncrona para buscar todos os dados
- console.log("Efeito de busca de dados do perfil disparado.");
- const fetchProfileData = async () => {
- try {;
-
- console.log("Usuário atual no Profile:", currentUser);
- const apiUserPosts = await GetUserProjects();
-
- const apiUserComments: CommentProps[] = await GetUserComments();
-
- setUserPosts(apiUserPosts);
- setUserComments(apiUserComments);
-
- } catch (error) {
- console.error("Falha ao buscar dados do perfil:", error);
- }
-
- };
- Eif(currentUser)
- fetchProfileData();
- }, [currentUser]);
-
- // Lógica para fechar o menu ao clicar fora
- useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- Eif (menuRef.current && !menuRef.current.contains(event.target as Node)) {
- setIsMenuOpen(false);
- }
- }
- if (isMenuOpen) {
- document.addEventListener('mousedown', handleClickOutside);
- }
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, [isMenuOpen]);
-
- const handleLogout = () => {
- logout(); // Limpa o estado e o localStorage
- navigate('/login'); // Redireciona para a tela de login
- };
-
- const handleDeleteComment = async (commentId: string) => {
- await DeleteComment(commentId);
-
- // Remove o comentário deletado do estado local para sumir da tela instantaneamente
- setUserComments((prevComments) =>
- prevComments.filter(comment => comment.commentID !== commentId)
- );
- };
-
- const handleEditProfile = () => {
- setIsMenuOpen(false);
- navigate('/editProfile');
- };
-
- const handleDeleteProfile = async () => {
-
- Iif (isDeleting) return;
- setIsDeleting(true);
-
- try {
- await DeleteProfile();
- setNotification({ message: "Perfil excluído com sucesso. Até logo!", type: 'success' });
-
- setIsDeleteProfileModalOpen(false);
-
- setTimeout(() => {
- logout(); // Desloga o usuário e limpa o storage
- navigate('/login'); // Manda para login
- }, 2000);
-
- } catch (error) {
- Eif (error instanceof Error) {
- if (error.message.includes("não encontrado") || error.message.includes("not found")) {
- setNotification({ message: "Conta já encerrada. Redirecionando...", type: 'success' });
- setTimeout(() => { logout(); navigate('/login'); }, 2000);
- return;
- }
- setNotification({ message: error.message, type: 'error' });
- }
- setIsDeleteProfileModalOpen(false);
- } finally {
- setIsDeleting(false);
- setIsDeleteProfileModalOpen(false);
- }
- };
-
- // Função para renderizar o feed (posts ou comentários)
- const renderFeed = () => {
- if (view === 'posts') {
- if (userPosts.length === 0) {
- return <p style={{ color: '#fff', padding: '20px' }}>Nenhum projeto encontrado.</p>;
- }
-
- return userPosts.map((post, index) => (
- <S.PostContainer key={index}>
- <Postcard post={post} showMenu={true} />
- </S.PostContainer>
- ));
- }
-
- Eif (view === 'comments') {
- // Mapeia 'userComments'
- return userComments.map(comment => (
- <S.PostContainer key={comment.commentID || Math.random()}>
- <Postcard
- post={{
- id: comment.commentID,
- title: `Comentou em: ${comment.projectTitle || 'Projeto'}`,
- description: comment.content,
- technologies: [],
- status: '',
- startDate: comment.createdAt,
- // Passa o usuário atual como autor para o cabeçalho do card
- author: { title: currentUser?.username || 'Você' }
- } as unknown as ProjectProps}
- showMenu={true}
- deleteLabel="Comentário"
- onDelete={() => handleDeleteComment(comment.commentID!)}
- />
- </S.PostContainer>
- ));
- }
-
- return null;
- };
-
- return (
- <S.PageWrapper>
- <Sidebar />
-
- {notification && (
- <Toast
- message={notification.message}
- type={notification.type}
- onClose={() => setNotification(null)}
- />
- )}
-
- <S.ContentWrapper>
-
- <S.ProfileHeader>
- <S.ActionsWrapper ref={menuRef}>
- <S.ProfileActions>
- <S.IconButton
- title="Ver publicações"
- $active={view === 'posts'}
- onClick={() => setView('posts')}
- >
- <PostsIcon />
- </S.IconButton>
- <S.IconButton
- title="Ver comentários"
- $active={view === 'comments'}
- onClick={() => setView('comments')}
- >
- <CommentsIcon />
- </S.IconButton>
- <S.IconButton
- title="Configurações"
- onClick={() => setIsMenuOpen(prev => !prev)}
- >
- <SettingsIcon />
- </S.IconButton>
- </S.ProfileActions>
-
- {isMenuOpen && (
- <D.DropdownMenu>
- <D.MenuItem onClick={handleEditProfile}>Editar Perfil</D.MenuItem>
- <D.MenuItem onClick={handleLogout}>Sair</D.MenuItem>
- <D.Separator />
- <D.DangerMenuItem onClick={() => {
- setIsMenuOpen(false);
- setIsDeleteProfileModalOpen(true);
- }}>
- Excluir Perfil
- </D.DangerMenuItem>
- </D.DropdownMenu>
- )}
-
- </S.ActionsWrapper>
- </S.ProfileHeader>
-
- <S.ProfileInfo>
- <S.ProfileAvatar
- style={{
- backgroundImage: undefined
- }}
- />
- <S.Username>{currentUser?.username || 'Carregando...'}</S.Username>
- </S.ProfileInfo>
-
- <S.PostList>
- {renderFeed()}
- </S.PostList>
-
- </S.ContentWrapper>
-
- <Modal
- isOpen={isDeleteProfileModalOpen}
- onClose={() => !isDeleting && setIsDeleteProfileModalOpen(false)}
- title="Excluir Conta"
- >
- <div style={{ textAlign: 'center' }}>
- <p style={{ marginBottom: '24px', color: '#555' }}>
- Tem certeza que deseja excluir sua conta? <br/>
- <strong>Todos os seus projetos, comunidades e comentários serão apagados permanentemente.</strong>
- </p>
- <ModalS.ModalActions>
- <ModalS.ChoiceButton
- onClick={() => setIsDeleteProfileModalOpen(false)}
- disabled={isDeleting}>
- Cancelar
- </ModalS.ChoiceButton>
- <ModalS.ChoiceButton
- onClick={handleDeleteProfile}
- style={{ backgroundColor: '#e74c3c' }}
- disabled={isDeleting}
- >
- {isDeleting ? 'Excluindo...' : 'Excluir Conta'}
- </ModalS.ChoiceButton>
- </ModalS.ModalActions>
- </div>
- </Modal>
-
- </S.PageWrapper>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 | - - -1x -40x - - - - -1x - - - - - - -1x - - - - -3x - - - - - - - - - - - - - - - - - -1x - - - - - - - - - - - - - - - - - - - - -1x -40x - - - - - - - - - - - - - - - - - - - - - - - - - - - - -1x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -1x - - - -40x -40x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -1x - - -40x - - -40x - - - - - - - - - - - - - - - -40x - - - - - -1x - - - - - - - - - - - - - - - -1x - - - - - - - - - - - -120x - - -120x - - - -120x - - - - - - - - - - - - - - -120x - - - - - - - - - - - - - -120x - - -120x - - - - - - - - - - - - - - - - - -120x - - - - -1x - - - | import styled from 'styled-components';
-
-// Wrapper para a página inteira
-export const PageWrapper = styled.div`
- background: linear-gradient(135deg, ${props => props.theme['gray-100']} 0%, ${props => props.theme['gray-100'] || props.theme['gray-100']} 100%);
- min-height: 100vh;
-`;
-
-// Wrapper para o conteúdo principal
-export const ContentWrapper = styled.main`
- margin-left: 250px;
- box-sizing: border-box;
- padding-bottom: 60px;
-`;
-
-// Container principal com posts
-export const PostContainer = styled.main`
- width: 100%;
- max-width: 800px;
- margin: 0 auto;
-
- background-color: ${props => props.theme.background};
- border-radius: 16px;
- padding: 24px;
- box-sizing: border-box;
-
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04),
- 0 1px 2px rgba(0, 0, 0, 0.06);
-
- transition: all 0.3s ease;
-
- &:hover {
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08),
- 0 2px 4px rgba(0, 0, 0, 0.06);
- transform: translateY(-2px);
- }
-`;
-
-// Lista que agrupa os posts do perfil
-export const PostList = styled.div`
- display: flex;
- flex-direction: column;
- gap: 20px;
- padding: 32px 24px;
-
- animation: fadeIn 0.4s ease-out;
-
- @keyframes fadeIn {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-`;
-
-// Cabeçalho do perfil
-export const ProfileHeader = styled.header`
- background: linear-gradient(135deg, ${props => props.theme.button} 0%, ${props => props.theme['hover-button'] || props.theme.button} 100%);
- height: 160px;
- position: relative;
- z-index: 0;
-
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
-
- /* Padrão decorativo sutil */
- &::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-image:
- radial-gradient(circle at 20% 50%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
- radial-gradient(circle at 80% 80%, rgba(255, 255, 255, 0.1) 0%, transparent 50%);
- pointer-events: none;
- }
-
- /* Posiciona os botões de ação */
- display: flex;
- justify-content: flex-end;
- align-items: flex-end;
- padding: 20px 32px;
-`;
-
-// Container da foto + nome
-export const ProfileInfo = styled.div`
- display: flex;
- align-items: flex-end;
- gap: 20px;
-
- margin-top: -70px;
- margin-left: 32px;
- padding-bottom: 24px;
- position: relative;
- z-index: 1;
- pointer-events: none;
-
- animation: slideUp 0.5s ease-out;
-
- @keyframes slideUp {
- from {
- opacity: 0;
- transform: translateY(20px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-
- /* Reativa pointer-events apenas nos elementos internos */
- > * {
- pointer-events: auto;
- }
-`;
-
-// A foto de perfil circular
-export const ProfileAvatar = styled.div`
- width: 120px;
- height: 120px;
- border-radius: 50%;
- background: linear-gradient(135deg, ${props => props.theme.sidebar} 0%, ${props => props.theme.button} 100%);
- border: 5px solid ${props => props.theme.background};
- background-size: cover;
- background-position: center;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15),
- 0 2px 8px rgba(0, 0, 0, 0.1);
-
- position: relative;
- flex-shrink: 0;
-
- /* Efeito de brilho sutil */
- &::after {
- content: '';
- position: absolute;
- top: 10%;
- left: 10%;
- width: 40%;
- height: 40%;
- background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 0%, transparent 70%);
- border-radius: 50%;
- }
-
- transition: transform 0.3s ease, box-shadow 0.3s ease;
-
- &:hover {
- transform: scale(1.05);
- box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2),
- 0 4px 12px rgba(0, 0, 0, 0.15);
- }
-`;
-
-// O nome do usuário
-export const Username = styled.h1`
- font-size: 2.5em;
- font-weight: 700;
- color: ${props => props.theme.title};
- margin: 0 0 10px 0; /* Margem inferior para alinhar com a base do avatar */
-
- background: linear-gradient(135deg, ${props => props.theme.title} 0%, ${props => props.theme.subtitle} 100%);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
-
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
-
- position: relative;
-
- &::after {
- content: '';
- position: absolute;
- bottom: -8px;
- left: 0;
- width: 60px;
- height: 4px;
- background: linear-gradient(90deg, ${props => props.theme.button} 0%, transparent 100%);
- border-radius: 2px;
- }
-`;
-
-// Container dos 3 botões de ação (dentro do banner)
-export const ProfileActions = styled.div`
- display: flex;
- align-items: center;
- gap: 12px;
- position: relative;
- z-index: 3;
-
- /* Backdrop blur para destacar os botões */
- background: rgba(255, 255, 255, 0.1);
- backdrop-filter: blur(10px);
- padding: 8px;
- border-radius: 50px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
-`;
-
-// Botão de ícone
-export const IconButton = styled.button<{ $active?: boolean }>`
- display: flex;
- align-items: center;
- justify-content: center;
- width: 48px;
- height: 48px;
- border-radius: 50%;
- border: none;
- cursor: pointer;
- position: relative;
- overflow: hidden;
-
- background-color: ${props => props.$active
- ? props.theme['hover-button'] || props.theme.button
- : props.theme.background};
- color: ${props => props.$active
- ? '#FFFFFF'
- : props.theme.button};
-
- box-shadow: ${props => props.$active
- ? '0 4px 12px rgba(0, 0, 0, 0.15), inset 0 1px 2px rgba(255, 255, 255, 0.1)'
- : '0 2px 8px rgba(0, 0, 0, 0.08)'};
-
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-
- /* Efeito ripple */
- &::before {
- content: '';
- position: absolute;
- top: 50%;
- left: 50%;
- width: 0;
- height: 0;
- border-radius: 50%;
- background: ${props => props.$active
- ? 'rgba(255, 255, 255, 0.3)'
- : 'rgba(0, 0, 0, 0.1)'};
- transform: translate(-50%, -50%);
- transition: width 0.6s, height 0.6s;
- }
-
- &:hover::before {
- width: 100%;
- height: 100%;
- }
-
- &:hover {
- transform: translateY(-3px) scale(1.05);
- box-shadow: ${props => props.$active
- ? '0 6px 20px rgba(0, 0, 0, 0.2)'
- : '0 4px 16px rgba(0, 0, 0, 0.12)'};
- background-color: ${props => props.$active
- ? props.theme['hover-button'] || props.theme.button
- : props.theme['gray-500'] || props.theme.background};
- }
-
- &:active {
- transform: translateY(-1px) scale(1.02);
- }
-
- svg {
- width: 22px;
- height: 22px;
- position: relative;
- z-index: 1;
- transition: transform 0.2s ease;
- }
-
- &:hover svg {
- transform: ${props => props.$active ? 'rotate(5deg)' : 'scale(1.1)'};
- }
-`;
-
-// Menus de ações flutuantes
-export const ActionsWrapper = styled.div`
- position: relative;
- z-index: 5; /* Aumentado para ficar acima de tudo */
-`; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 90% | -18/20 | -75% | -6/8 | -100% | -4/4 | -94.11% | -16/17 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 | - - - - - - - - -4x -4x -4x - -4x - -2x -2x -2x -2x -2x - - - -2x - - -2x - - -4x -2x - - -1x - -1x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -2x - - - - - - - - - - - | import { useEffect, useState } from 'react';
-import { useParams } from 'react-router-dom';
-import Sidebar from '../../components/layout/Sidebar';
-import * as S from './styles';
-import { GetProjectById } from '../../API/Project';
-import type { ProjectProps } from '../../API/Project';
-import { FiCalendar, FiUser } from 'react-icons/fi';
-
-export default function ProjectPage() {
- const { projectId } = useParams<{ projectId: string }>();
- const [project, setProject] = useState<ProjectProps | null>(null);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- async function loadData() {
- Iif (!projectId) return;
- try {
- setLoading(true);
- const data = await GetProjectById(projectId);
- setProject(data);
- } catch (error) {
- console.error("Erro ao carregar projeto:", error);
- } finally {
- setLoading(false);
- }
- }
- loadData();
- }, [projectId]);
-
- if (loading) return <div>Carregando...</div>;
- if (!project) return <div>Projeto não encontrado</div>;
-
- // Formata data
- const startDate = new Date(project.startDate).toLocaleDateString('pt-BR');
-
- return (
- <S.PageWrapper>
- <Sidebar />
-
- <S.MainContent>
- <S.Banner />
-
- <S.HeaderContainer>
- {/* Ícone com a inicial do projeto */}
- <S.ProjectIcon>
- {project.title.charAt(0).toUpperCase()}
- </S.ProjectIcon>
-
- <S.HeaderInfo>
- <h1>{project.title}</h1>
- <span>
- <FiUser style={{ marginRight: 5, verticalAlign: 'middle' }}/>
- Criado por <strong>{project.authorUsername || project.authorName}</strong>
- </span>
- </S.HeaderInfo>
- </S.HeaderContainer>
-
- <S.ContentGrid>
-
- {/* Coluna Principal: Descrição */}
- <S.MainColumn>
- <h2>Sobre o Projeto</h2>
- <S.DescriptionBox>
- {project.description}
- </S.DescriptionBox>
-
- </S.MainColumn>
-
- <S.InfoSidebar>
- <h3>Status</h3>
- <S.StatusBadge status={project.status}>
- {project.status.replace('-', ' ')}
- </S.StatusBadge>
-
- <h3>Início</h3>
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: '#555' }}>
- <FiCalendar />
- <span>{startDate}</span>
- </div>
-
- <h3>Tecnologias</h3>
- <S.KeywordsContainer>
- {project.technologies?.map((tech) => (
- <S.KeywordTag key={tech}>
- {tech}
- </S.KeywordTag>
- ))}
- </S.KeywordsContainer>
- </S.InfoSidebar>
-
- </S.ContentGrid>
- </S.MainContent>
- </S.PageWrapper>
- );
-} |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| index.tsx | -
-
- |
- 87.5% | -14/16 | -75% | -3/4 | -60% | -3/5 | -87.5% | -14/16 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 | - - - - - - - - - - - -8x - - -8x -8x - - -2x -2x -2x - -1x - - -1x -1x - - - - -1x - - -1x -1x - - - - -8x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -10x - - - - - - - - - - - - - - - - - - - - - | import { useState } from 'react';
-import { FormPageContainer, FormWrapper, FormTitle, StyledForm, StyledInput, SubmitButton, RedirectLink } from '../../components/domain/Form/styles';
-import { useForm, Controller} from 'react-hook-form';
-import { IMaskInput } from 'react-imask';
-import Toast from '../../components/common/Toast';
-import { useNavigate } from 'react-router-dom';
-import {Register as RegisterAPI} from '../../API/Auth'
-import type { RegisterProps } from '../../API/Auth';
-import type { NotificationState } from '../../components/common/Toast';
-
-export default function Register() {
- // Estado para a notificação - usado no Toast
- const [notification, setNotification] = useState<NotificationState | null>(null);
-
-
- const { register, handleSubmit, formState: {isSubmitting}, control } = useForm<RegisterProps>();
- const navigate = useNavigate();
-
- async function onSubmit(data: RegisterProps) {
- console.log(data);
- try{
- await RegisterAPI(data);
-
- console.log('Usuário registrado com sucesso:');
-
- // Define estado para mostrar notificação de sucesso
- setNotification({ message: 'Usuário registrado com sucesso!', type: 'success' });
- setTimeout(() => {
- navigate('/feed'); // Navega para a página de feed
- }, 1000);
-
- } catch (error) {
- console.error('Erro ao registrar usuário:', error);
-
- // Define estado para mostrar notificação de erro
- Eif (error instanceof Error){
- setNotification({ message: error.message, type: 'error' });
- }
- }
- }
-
- return (
- <FormPageContainer>
-
- {notification && (
- <Toast
- message={notification.message}
- type={notification.type}
- onClose={() => setNotification(null)}
- />
- )}
-
- <FormWrapper>
- <FormTitle>Criar conta</FormTitle>
-
- <StyledForm onSubmit={handleSubmit(onSubmit)}>
- <StyledInput
- type="text"
- placeholder="Nome completo"
- required
- {...register('nomeCompleto')}
- />
- <StyledInput
- type="text"
- placeholder="Nome de usuário (user)"
- required
- {...register('username')}
- />
- <StyledInput
- type="email"
- placeholder="Email"
- required
- {...register('email')}
- />
- <StyledInput
- type="password"
- placeholder="Senha"
- required
- {...register('senha')}
- />
- <StyledInput
- type="tel"
- placeholder="Telefone"
- required
- {...register('telefone')}
- />
- <Controller
- name="dataNascimento"
- control={control}
- rules={{ required: true }}
- render={({ field }) => (
- <StyledInput
- {...field}
- as={IMaskInput}
- mask="00/00/0000"
- placeholder="Data de nascimento"
- required
- />
- )}
- />
-
- <SubmitButton disabled = {isSubmitting} type="submit">Cadastrar</SubmitButton>
- </StyledForm>
-
- <RedirectLink>
- Já tem uma conta? <a href="/login">Faça login</a>
- </RedirectLink>
- </FormWrapper>
- </FormPageContainer>
- );
-}
-
- |