diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c3a65b --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +.vercel diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..306c9cd --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 120 +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4f9adb3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "workbench.colorCustomizations": { + "activityBar.background": "#2F2F07", + "titleBar.activeBackground": "#41420A", + "titleBar.activeForeground": "#FBFBE7" + } +} \ No newline at end of file diff --git a/README.md b/README.md index fb46be5..60f6837 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,48 @@ -# LaLiga-FrontEnd +# Para mi yo futuro o a quien le pueda servir +### Este template está creado para empezar con unos mínimos que puede ahorrarme tiempo a la hora de iniciar un proyecto personal. +`**No te enfoques en el estilo actual, intento hacerlo de lo mas neutral posible **` -La Liga Front End Repo \ No newline at end of file +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Para empezar, aunque no hace falta ni decirlo, pero bueno por si acaso. + +First, run the development server: `(y a darle sin miedo)` + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +# ¿Que me voy a encontrar? +Cuando abras el proyecto, tendras un layout, con su navbar y su footer con un children para que puedas meter todo lo que quieras. +```js +
+ +
{children}
+
+``` +Entre otras cosas, que me parecen interesantes para tener ya preparadas, como es un `cambio de lenguaje`, tiene instalado **i18next**, configurado con `Inglés y español`, con la libertad para añadir los idiomas necesarios, las carpetas de locales, donde ya se irán ***`¡Ojo!`*** creando automáticamente los archivos json de las traducciones. + +===> Si dudas y no la conoces [i18next](https://www.i18next.com/) <== +### Navbar responsivo +Con su boton hamburguesa, y sus iconos, para cerrar y desplegar menu. + +### Componentes de ejemplos +Para llegar y desembarcar. Modificarlo al gusto y listo. + +# Librerías que he instalado. +`Libertad para borrar la que no se quiera ` +- **i18next** => Ya dicho anteriormente, para las traducciones de idioma. +- **react-iconst** => Iconos. Ya hay alguno en el navbar. +- **react-loading** => Para animaciones, está ya en un componente que tiene de todo, y una de las opciones usa esta librería. +- **sass** => Ni falta hace que decirlo, pero ahí esta para darle alegría a mi estilo con tailwindcss. +- **TailwindCSS** => Otro tanto, la clave para volar con los estilos. +- **Typescript** => Bendiciones para tener controlado todo. + +Y poco más. Con esto empezar será más fácil. \ No newline at end of file diff --git a/next-i18next.config.js b/next-i18next.config.js new file mode 100644 index 0000000..20bef52 --- /dev/null +++ b/next-i18next.config.js @@ -0,0 +1,9 @@ +/** @type {import('next-i18next').UserConfig} */ +module.exports = { + i18n: { + defaultLocale: 'es', + locales: ['es', 'en'] + }, + serializeConfig: false, + saveMissing: true +} diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..dad4478 --- /dev/null +++ b/next.config.js @@ -0,0 +1,15 @@ +/** @type {import('next').NextConfig} */ +const { i18n } = require('./next-i18next.config') + +const nextConfig = { + reactStrictMode: false, + i18n: { + defaultLocale: 'es', + locales: ['es', 'en'] + }, + images: { + domains: ['lh3.googleusercontent.com', 'avatars.githubusercontent.com'] + } +} + +module.exports = nextConfig diff --git a/package.json b/package.json new file mode 100644 index 0000000..eed2c4f --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "next-tailwind-boilerplate", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + + }, + "dependencies": { + "@auth/prisma-adapter": "^1.0.12", + "@headlessui/react": "^1.7.18", + "@hookform/resolvers": "^3.3.3", + "@prisma/client": "^5.8.1", + "@types/node": "20.4.1", + "@types/react": "18.2.14", + "@types/react-dom": "18.2.6", + "autoprefixer": "10.4.14", + "eslint": "8.44.0", + "eslint-config-next": "13.4.9", + "i18next": "^23.2.10", + "next": "13.4.9", + "next-auth": "^4.24.5", + "next-i18next": "^14.0.0", + "postcss": "8.4.25", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-hook-form": "^7.49.2", + "react-i18next": "^13.0.2", + "react-icons": "^4.11.0", + "react-loading": "^2.0.3", + "sass": "^1.63.6", + "swr": "^2.2.4", + "tailwindcss": "3.3.2", + "typescript": "5.1.6", + "yup": "^1.3.3" + }, + "devDependencies": { + "prisma": "^5.8.1" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/prisma/comment.ts b/prisma/comment.ts new file mode 100644 index 0000000..cddc2f4 --- /dev/null +++ b/prisma/comment.ts @@ -0,0 +1,42 @@ +import { PrismaClient } from '@prisma/client' +const prisma = new PrismaClient() + +export const getAllComments = async (postSlug?: string) => { + try { + const comments = await prisma.comment.findMany({ + orderBy: [{ createdAt: 'desc' }], + where: { + ...(postSlug && { postSlug: postSlug }) + }, + include: { user: true } + }) + return comments + } catch (error) { + console.error('Error fetching comments:', error) + throw new Error('Error fetching comments') + } +} + +export const createComment = async (body: any, userEmail: any, session: any) => { + if (!session) { + throw new Error('Not Authenticated') + } + + try { + const comment = await prisma.comment.create({ + data: { description: body.description, postSlug: body.postSlug, userEmail: userEmail } + }) + return comment + } catch (error) { + console.error('Error creating comment:', error) + throw new Error('Error creating comment') + } +} + +export const deleteComment = async (id: string) => { + await prisma.comment.delete({ + where: { + id: id + } + }) +} diff --git a/prisma/favorite.ts b/prisma/favorite.ts new file mode 100644 index 0000000..a9ed72c --- /dev/null +++ b/prisma/favorite.ts @@ -0,0 +1,59 @@ +import { PrismaClient } from '@prisma/client' +const prisma = new PrismaClient() + +export const addFavorite = async (postId: any, userEmail: any, session: any) => { + try { + if (!session) { + throw new Error('Not Authenticated') + } + const existingFavorite = await prisma.favorite.findUnique({ + where: { postId_userEmail: { postId, userEmail } } + }) + if (existingFavorite) { + throw new Error('El usuario ya marcó este post como favorito.') + } + await prisma.favorite.create({ + data: { + postId, + userEmail + } + }) + const updatedFavorites = await prisma.post + .findUnique({ + where: { id: postId } + }) + .Favorite() + + return updatedFavorites + } catch (error) { + console.error('Error al añadir a favoritos:', error) + throw new Error('Error al añadir a favoritos') + } +} + +export const deleteFavorite = async (postId: any, userEmail: string, session: any) => { + try { + // Verificar si el usuario ha marcado el post como favorito + const existingFavorite = await prisma.favorite.findUnique({ + where: { postId_userEmail: { postId, userEmail } } + }) + if (!existingFavorite) { + throw new Error('El usuario no ha marcado este post como favorito.') + } + // Eliminar de favoritos + await prisma.favorite.delete({ + where: { postId_userEmail: { postId, userEmail } } + }) + + const updatedFavorites = await prisma.post + .findUnique({ + where: { id: postId } + }) + .Favorite() + + return updatedFavorites + } catch (error) { + console.error('Error al quitar de favoritos:', error) + throw new Error('Error al quitar de favoritos') + } +} diff --git a/prisma/like.ts b/prisma/like.ts new file mode 100644 index 0000000..28c701a --- /dev/null +++ b/prisma/like.ts @@ -0,0 +1,59 @@ +import { PrismaClient } from '@prisma/client' +const prisma = new PrismaClient() + +export const addLike = async (postId: any, userEmail: any, session: any) => { + try { + if (!session) { + throw new Error('Not Authenticated') + } + const existingLike = await prisma.like.findUnique({ + where: { postId_userEmail: { postId, userEmail } } + }) + if (existingLike) { + throw new Error('El usuario ya dio like a este post.') + } + await prisma.like.create({ + data: { + postId, + userEmail + } + }) + const updatedLikes = await prisma.post + .findUnique({ + where: { id: postId } + }) + .Like() + + return updatedLikes + } catch (error) { + console.error('Error al añadir el like:', error) + throw new Error('Error al añadir el like') + } +} + +export const deleteLike = async (postId: any, userEmail: string, session: any) => { + try { + // Verificar si el usuario ha dado like al post + const existingLike = await prisma.like.findUnique({ + where: { postId_userEmail: { postId, userEmail } } + }) + if (!existingLike) { + throw new Error('El usuario no ha dado like a este post.') + } + // Eliminar el like + await prisma.like.delete({ + where: { postId_userEmail: { postId, userEmail } } + }) + + const updatedLikes = await prisma.post + .findUnique({ + where: { id: postId } + }) + .Like() + + return updatedLikes + } catch (error) { + console.error('Error al quitar el like:', error) + throw new Error('Error al quitar el like') + } +} diff --git a/prisma/post.ts b/prisma/post.ts new file mode 100644 index 0000000..449bc04 --- /dev/null +++ b/prisma/post.ts @@ -0,0 +1,68 @@ +import { PrismaClient } from '@prisma/client' +const prisma = new PrismaClient() + +export const deletePost = async (slug: string) => { + try { + const post = await prisma.post.findUnique({ + where: { + slug: slug + }, + include: { + comments: true + } + }) + if (!post) { + throw new Error('Post not found') + } + await prisma.like.deleteMany({ + where: { + postId: post.id + } + }) + await prisma.comment.deleteMany({ + where: { + postSlug: slug + } + }) + await prisma.post.delete({ + where: { + slug: slug + } + }) + } catch (error) { + console.error(error, 'Error deleting post') + throw new Error('Error deleting post') + } +} + +export const editPost = async (slug: string, newData: any) => { + try { + // Buscar el post que se va a editar + const existingPost = await prisma.post.findUnique({ + where: { + slug: slug + } + }) + // Verificar si el post existe + if (!existingPost) { + throw new Error('Post not found') + } + // Actualizar el post con los nuevos datos + const updatedPost = await prisma.post.update({ + where: { + slug: slug + }, + data: { + ...newData + // Tags: { + // set: newData.Tags.map((tag: any) => ({ id: tag.id })) + // } + } + }) + + return updatedPost + } catch (error) { + console.error(error, 'Error editing post') + throw new Error('Error editing post') + } +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..2d1e268 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,156 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mongodb" + url = env("MONGODB_URI") +} + +// datasource db { +// provider = "mongodb" +// url = env("DATABASE_URL") +// } + +model Account { + id String @id @default(cuid()) @map("_id") + userId String + type String + provider String + providerAccountId String + refresh_token String? + access_token String? + expires_at Int? + token_type String? + scope String? + id_token String? + session_state String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) +} + +model Session { + id String @id @default(cuid()) @map("_id") + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model User { + id String @id @default(cuid()) @map("_id") + name String? + email String @unique + emailVerified DateTime? + image String? + isAdmin Boolean @unique @default(false) + accounts Account[] + sessions Session[] + Post Post[] + Like Like[] + Comment Comment[] + Favorite Favorite[] +} + +model VerificationToken { + identifier String @id @map("_id") + token String @unique + expires DateTime + + @@unique([identifier, token]) +} + +model Category { + id String @id @default(cuid()) @map("_id") + slug String @unique + title String + img String? + color String? + Posts Post[] + Comment Comment[] +} + +// model Tag { +// id String @id @default(cuid()) @map("_id") +// name String @unique +// slug String @unique +// color String? +// // Posts PostTag[] +// Post Post? @relation(fields: [postId], references: [id]) +// postId String? +// } + +// model PostTag { +// id String @id @default(cuid()) @map("_id") +// postId String +// tagId String +// post Post @relation(fields: [postId], references: [id]) +// tag Tag @relation(fields: [tagId], references: [id]) + +// @@unique([postId, tagId]) +// } + +model Post { + id String @id @default(cuid()) @map("_id") + slug String @unique + title String + description String + img String? + views Int @default(0) + catSlug String? + Category Category? @relation(fields: [catSlug], references: [slug]) + userEmail String + user User @relation(fields: [userEmail], references: [email], onDelete: Cascade) + Like Like[] + comments Comment[] + url String? + twitterShareCount Int @default(0) + whatsappShareCount Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + Favorite Favorite[] + // PostTag PostTag[] + // Tags Tag[] +} + +model Like { + id String @id @default(cuid()) @map("_id") + postId String + post Post @relation(fields: [postId], references: [id]) + userEmail String + user User @relation(fields: [userEmail], references: [email], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([postId, userEmail]) +} + +model Favorite { + id String @id @default(cuid()) @map("_id") + postId String + post Post @relation(fields: [postId], references: [id]) + userEmail String + user User @relation(fields: [userEmail], references: [email], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([postId, userEmail]) +} + +model Comment { + id String @id @default(cuid()) @map("_id") + createdAt DateTime @default(now()) + description String + userEmail String + user User @relation(fields: [userEmail], references: [email], onDelete: Cascade) + postSlug String + post Post @relation(fields: [postSlug], references: [slug]) + + Category Category? @relation(fields: [categoryId], references: [id]) + categoryId String? +} diff --git a/prisma/stats.ts b/prisma/stats.ts new file mode 100644 index 0000000..c7fdc41 --- /dev/null +++ b/prisma/stats.ts @@ -0,0 +1,25 @@ +import { PrismaClient } from '@prisma/client' +const prisma = new PrismaClient() + +export const getAllStats = async () => { + try { + const totalPosts = await prisma.post.count() + const totalUsers = await prisma.user.count() + const totalViews = await prisma.post.aggregate({ _sum: { views: true } }) + const totalShares = await prisma.post.aggregate({ + _sum: { twitterShareCount: true, whatsappShareCount: true } + }) + + return { + totalPosts, + totalUsers, + totalViews: totalViews._sum || 0, + totalShares: totalShares._sum || 0 + } + } catch (error) { + console.error('Error fetching statistics:', error) + throw new Error('Error fetching statistics') + } finally { + await prisma.$disconnect() + } +} diff --git a/prisma/tag.ts b/prisma/tag.ts new file mode 100644 index 0000000..eb69423 --- /dev/null +++ b/prisma/tag.ts @@ -0,0 +1,82 @@ +// import { PrismaClient } from '@prisma/client' +// const prisma = new PrismaClient() + +// //CREAR NUEVO TAG + +// export const createTag = async (tagData: { name: string; slug: string; color?: string }) => { +// try { +// const normalizedTagSlug = tagData.slug.toLowerCase() +// const existingTagMinus = await prisma.tag.findUnique({ +// where: { slug: normalizedTagSlug } +// }) + +// if (existingTagMinus) { +// console.error(`Error al crear el Tag: El SLUG "${tagData.slug}" ya está en uso.`) +// throw new Error(`Error al crear el Tag: El SLUG "${tagData.slug}" ya está en uso.`) +// } + +// const newTag = await prisma.tag.create({ +// data: { ...tagData, slug: normalizedTagSlug } +// }) + +// return newTag +// } catch (error) { +// console.error('Error al crear el Tag:', error) +// throw new Error('Error al crear el Tag') +// } +// } + +// // OBTENER TODOS LOS TAGS + +// export const getAllTags = async () => { +// try { +// const tags = await prisma.tag.findMany() +// return tags +// } catch (error) { +// console.error('Error al obtener todos los Tags:', error) +// throw new Error('Error al obtener todos los Tags') +// } +// } + +// // OBTENER UN TAG POR SU ID + +// export const getTagById = async (tagId: string) => { +// try { +// const tag = await prisma.tag.findUnique({ +// where: { id: tagId } +// }) +// return tag +// } catch (error) { +// console.error('Error al obtener el Tag por ID:', error) +// throw new Error('Error al obtener el Tag por ID') +// } +// } + +// //ACTUALIZAR UN TAG + +// export const updateTag = async (tagData: { id: string; name?: string; slug?: string; color?: string }) => { +// try { +// const updatedTag = await prisma.tag.update({ +// where: { id: tagData.id }, +// data: { name: tagData.name, slug: tagData.slug, color: tagData.color } +// }) +// return updatedTag +// } catch (error) { +// console.error('Error al actualizar el Tag:', error) +// throw new Error('Error al actualizar el Tag') +// } +// } + +// // ELIMINAR UN TAG + +// export const deleteTag = async (tagId: string) => { +// try { +// const deletedTag = await prisma.tag.delete({ +// where: { id: tagId } +// }) +// return deletedTag +// } catch (error) { +// console.error('Error al eliminar el Tag:', error) +// throw new Error('Error al eliminar el Tag') +// } +// } diff --git a/prisma/user.ts b/prisma/user.ts new file mode 100644 index 0000000..b504f7f --- /dev/null +++ b/prisma/user.ts @@ -0,0 +1,93 @@ +import { PrismaClient } from '@prisma/client' +const prisma = new PrismaClient() + +export const getAllUsers = async () => { + try { + const users = await prisma.user.findMany() + return users + } catch (error) { + console.error('Error fetching users:', error) + throw new Error('Error fetching users') + } +} +export const getUserByEmail = async (email: string) => { + try { + const user = await prisma.user.findUnique({ + where: { + email: email + }, + include: { + Like: true, + Favorite: true + } + }) + return user + } catch (error) { + console.error('Error fetching user by ID:', error) + throw new Error('Error fetching user by ID') + } +} + +export const deleteUserByEmail = async (email: string) => { + try { + const userDeleted = await prisma.user.delete({ + where: { + email: email + }, + include: { + Like: true, + Comment: true, + sessions: true, + accounts: true + } + }) + + return userDeleted + } catch (error) { + console.error(`Error deleting user with email ${email}:`, error) + throw new Error(`Unable to delete user with email ${email}`) + } +} + +export const updateUserByEmail = async (email: string, newData: any) => { + try { + const { name } = newData + if (name) { + const existingUserWithSameName = await prisma.user.findFirst({ + where: { + name: name, + email: { not: email } + } + }) + + if (existingUserWithSameName) { + return { success: false, status: 409, error: `Name ${name} is already in use by another user` } + } + } + // Verificar si el usuario existe + const existingUser = await prisma.user.findUnique({ + where: { + email: email + } + }) + + if (!existingUser) { + return { status: 404, error: `User with email ${email} not found` } + } + + // Actualizar el usuario con los nuevos datos + const updatedUser = await prisma.user.update({ + where: { + email: email + }, + data: newData + }) + + return updatedUser + } catch (error) { + console.error(`Error updating user with email ${email}:`, error) + return { status: 500, error: `Unable to update user with email ${email}` } + } finally { + await prisma.$disconnect() + } +} diff --git a/public/images/IconWhatsapp.svg b/public/images/IconWhatsapp.svg new file mode 100644 index 0000000..bcb34c0 --- /dev/null +++ b/public/images/IconWhatsapp.svg @@ -0,0 +1,59 @@ + + + + + + + + \ No newline at end of file diff --git a/public/images/React.png b/public/images/React.png new file mode 100644 index 0000000..e9ef2ad Binary files /dev/null and b/public/images/React.png differ diff --git a/public/images/alert.svg b/public/images/alert.svg new file mode 100644 index 0000000..8e00850 --- /dev/null +++ b/public/images/alert.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/public/images/avatar-hombre.svg b/public/images/avatar-hombre.svg new file mode 100644 index 0000000..f5a8db1 --- /dev/null +++ b/public/images/avatar-hombre.svg @@ -0,0 +1,2 @@ + +Artboards_Diversity_Avatars_by_Netguru \ No newline at end of file diff --git a/public/images/avatar-mujer.svg b/public/images/avatar-mujer.svg new file mode 100644 index 0000000..30f4afe --- /dev/null +++ b/public/images/avatar-mujer.svg @@ -0,0 +1,2 @@ + +Artboards_Diversity_Avatars_by_Netguru \ No newline at end of file diff --git a/public/images/back.svg b/public/images/back.svg new file mode 100644 index 0000000..f429c02 --- /dev/null +++ b/public/images/back.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/backRoute.svg b/public/images/backRoute.svg new file mode 100644 index 0000000..c5a0f19 --- /dev/null +++ b/public/images/backRoute.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/public/images/css.png b/public/images/css.png new file mode 100644 index 0000000..90bdbde Binary files /dev/null and b/public/images/css.png differ diff --git a/public/images/css.svg b/public/images/css.svg new file mode 100644 index 0000000..79c47a9 --- /dev/null +++ b/public/images/css.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/cursos.svg b/public/images/cursos.svg new file mode 100644 index 0000000..28d9e11 --- /dev/null +++ b/public/images/cursos.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/devWorldResources-removebg.png b/public/images/devWorldResources-removebg.png new file mode 100644 index 0000000..4c5e191 Binary files /dev/null and b/public/images/devWorldResources-removebg.png differ diff --git a/public/images/devWorldResources-removebg.svg b/public/images/devWorldResources-removebg.svg new file mode 100644 index 0000000..1580460 --- /dev/null +++ b/public/images/devWorldResources-removebg.svg @@ -0,0 +1,1492 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/devWorldResources.jpeg b/public/images/devWorldResources.jpeg new file mode 100644 index 0000000..ef17cd6 Binary files /dev/null and b/public/images/devWorldResources.jpeg differ diff --git a/public/images/error.svg b/public/images/error.svg new file mode 100644 index 0000000..609e669 --- /dev/null +++ b/public/images/error.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/public/images/front.svg b/public/images/front.svg new file mode 100644 index 0000000..30302a4 --- /dev/null +++ b/public/images/front.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/github.svg b/public/images/github.svg new file mode 100644 index 0000000..2dfec51 --- /dev/null +++ b/public/images/github.svg @@ -0,0 +1,19 @@ + + + + + github [#142] + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/herramientas.svg b/public/images/herramientas.svg new file mode 100644 index 0000000..2183e72 --- /dev/null +++ b/public/images/herramientas.svg @@ -0,0 +1,32 @@ + + + \ No newline at end of file diff --git a/public/images/iconDevWorld.jpeg b/public/images/iconDevWorld.jpeg new file mode 100644 index 0000000..c3a6162 Binary files /dev/null and b/public/images/iconDevWorld.jpeg differ diff --git a/public/images/javascript.svg b/public/images/javascript.svg new file mode 100644 index 0000000..58b9768 --- /dev/null +++ b/public/images/javascript.svg @@ -0,0 +1,19 @@ + + + + + javascript [#155] + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/javascript1.svg b/public/images/javascript1.svg new file mode 100644 index 0000000..1f203d3 --- /dev/null +++ b/public/images/javascript1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/library.png b/public/images/library.png new file mode 100644 index 0000000..20b2320 Binary files /dev/null and b/public/images/library.png differ diff --git a/public/images/library.svg b/public/images/library.svg new file mode 100644 index 0000000..dc01194 --- /dev/null +++ b/public/images/library.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/linkedin.svg b/public/images/linkedin.svg new file mode 100644 index 0000000..0b3291e --- /dev/null +++ b/public/images/linkedin.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/public/images/manuelcebreiro.jpg b/public/images/manuelcebreiro.jpg new file mode 100644 index 0000000..fc233bd Binary files /dev/null and b/public/images/manuelcebreiro.jpg differ diff --git a/public/images/react.svg b/public/images/react.svg new file mode 100644 index 0000000..9a25a8c --- /dev/null +++ b/public/images/react.svg @@ -0,0 +1,21 @@ + + + + + frameworks-and-libraries/react + Created with Sketch. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/share.svg b/public/images/share.svg new file mode 100644 index 0000000..e9dea33 --- /dev/null +++ b/public/images/share.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/public/images/shared1.svg b/public/images/shared1.svg new file mode 100644 index 0000000..480a68a --- /dev/null +++ b/public/images/shared1.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/public/images/tutorials.png b/public/images/tutorials.png new file mode 100644 index 0000000..bb04b03 Binary files /dev/null and b/public/images/tutorials.png differ diff --git a/public/images/tutorials.svg b/public/images/tutorials.svg new file mode 100644 index 0000000..b708827 --- /dev/null +++ b/public/images/tutorials.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/typescript.png b/public/images/typescript.png new file mode 100644 index 0000000..a855ee8 Binary files /dev/null and b/public/images/typescript.png differ diff --git a/public/images/typescript.svg b/public/images/typescript.svg new file mode 100644 index 0000000..d1a483e --- /dev/null +++ b/public/images/typescript.svg @@ -0,0 +1,29 @@ + + + + + build-tools/typescript + Created with Sketch. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/userIcon.svg b/public/images/userIcon.svg new file mode 100644 index 0000000..802a58c --- /dev/null +++ b/public/images/userIcon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/public/images/videotutos.svg b/public/images/videotutos.svg new file mode 100644 index 0000000..2f5750e --- /dev/null +++ b/public/images/videotutos.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/xSocial.svg b/public/images/xSocial.svg new file mode 100644 index 0000000..b144d78 --- /dev/null +++ b/public/images/xSocial.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/locales/en/common.json b/public/locales/en/common.json new file mode 100644 index 0000000..19775b3 --- /dev/null +++ b/public/locales/en/common.json @@ -0,0 +1,7 @@ +{ + "TodoTest": "Doing Tests", + "HomePage": "Home", + "contact": "Contact", + "about": "About", + "login": "Login" +} \ No newline at end of file diff --git a/public/locales/es/common.json b/public/locales/es/common.json new file mode 100644 index 0000000..36bb374 --- /dev/null +++ b/public/locales/es/common.json @@ -0,0 +1,7 @@ +{ + "TodoTest": "Haciendo pruebas", + "HomePage": "Página principal", + "contact": "Contacto", + "about": "Acerca de la página", + "login": "Identificarse" +} \ No newline at end of file diff --git a/public/locales/es/common.missing.json b/public/locales/es/common.missing.json new file mode 100644 index 0000000..812be4c --- /dev/null +++ b/public/locales/es/common.missing.json @@ -0,0 +1,6 @@ +{ + "HomePage": "HomePage", + "contact": "contact", + "about": "about", + "login": "login" +} \ No newline at end of file diff --git a/src/api/auth/[...nextauth]/route.js b/src/api/auth/[...nextauth]/route.js new file mode 100644 index 0000000..285256e --- /dev/null +++ b/src/api/auth/[...nextauth]/route.js @@ -0,0 +1,4 @@ +// import NextAuth from 'next-auth' +// import { authOptions } from '../../../utils/auth' + +// export default NextAuth(authOptions) diff --git a/src/api/hello.ts b/src/api/hello.ts new file mode 100644 index 0000000..f8bcc7e --- /dev/null +++ b/src/api/hello.ts @@ -0,0 +1,13 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next' + +type Data = { + name: string +} + +export default function handler( + req: NextApiRequest, + res: NextApiResponse +) { + res.status(200).json({ name: 'John Doe' }) +} diff --git a/src/components/atoms/AutoCompleteInput.tsx b/src/components/atoms/AutoCompleteInput.tsx new file mode 100644 index 0000000..e425352 --- /dev/null +++ b/src/components/atoms/AutoCompleteInput.tsx @@ -0,0 +1,75 @@ +import { useRouter } from 'next/router' +import { useState } from 'react' +import { FaSearch } from 'react-icons/fa' +import { Post } from '../../../type' + +const AutoCompleteInput = ({ + textSearched, + setTextSearched, + posts +}: { + textSearched: string + setTextSearched: (text: string) => void + posts: Post[] +}) => { + const [showPosts, setShowPosts] = useState(false) + const router = useRouter() + const filteredPosts = posts.filter((post) => { + const searchText = textSearched.toLowerCase() + const postTitle = post.title.toLowerCase() + const searchWords = searchText.split(' ') + + return searchWords.every((word) => postTitle.includes(word)) + }) + const handleInputChange = (e: React.ChangeEvent) => { + setTextSearched(e.target.value) + setShowPosts(!!e.target.value) + } + const highlightMatches = (text: string) => { + const searchText = textSearched.toLowerCase() + const regex = new RegExp(`(${searchText})`, 'gi') + return text.replace(regex, '$1') + } + const handlePostClick = (slug: string) => { + router.push(`/post/${slug}`) + setShowPosts(false) + } + const noResults = showPosts && filteredPosts.length === 0 + + return ( +
+
+ + + + +
+ {noResults && ( +
+ No hay coincidencias. +
+ )} + {showPosts && ( +
+ {filteredPosts.map((post) => ( +
handlePostClick(post.slug)} + > +

+

+ ))} +
+ )} +
+ ) +} + +export default AutoCompleteInput diff --git a/src/components/atoms/Button.tsx b/src/components/atoms/Button.tsx new file mode 100644 index 0000000..5f49988 --- /dev/null +++ b/src/components/atoms/Button.tsx @@ -0,0 +1,26 @@ +import ReactLoading from 'react-loading' + +type ButtonProps = { + children: React.ReactNode + className?: string + buttonClassName?: string + icon?: React.ReactNode + loading?: boolean + [others: string]: any +} + +export const Button = ({ children, className, icon, loading, buttonClassName, ...others }: ButtonProps) => { + return ( +
+ {loading && ( +
+ +
+ )} + +
+ ) +} diff --git a/src/components/atoms/DisclosureIndividual.tsx b/src/components/atoms/DisclosureIndividual.tsx new file mode 100644 index 0000000..a66db01 --- /dev/null +++ b/src/components/atoms/DisclosureIndividual.tsx @@ -0,0 +1,34 @@ +import { Disclosure } from '@headlessui/react' +import { IoIosArrowForward } from 'react-icons/io' + +export default function DisclosureIndividual({ + classNameArrow, + className, + text, + children +}: { + classNameArrow?: string + className?: string + text?: string + children: React.ReactNode +}) { + return ( +
+
+ + {({ open }) => ( + <> + + {text} + + + {children} + + )} + +
+
+ ) +} diff --git a/src/components/atoms/DropDownShare.tsx b/src/components/atoms/DropDownShare.tsx new file mode 100644 index 0000000..854419c --- /dev/null +++ b/src/components/atoms/DropDownShare.tsx @@ -0,0 +1,131 @@ +import { Menu, Transition } from '@headlessui/react' +import Image from 'next/image' +import { Fragment, useEffect, useState } from 'react' +import ShareWhatsapp from '../../../public/images/IconWhatsapp.svg' +import Share from '../../../public/images/shared1.svg' +import ShareTwitter from '../../../public/images/xSocial.svg' + +export const DropDownShare = ({ + slug, + id, + counTwitter, + countWhatsapp +}: { + slug?: string + id: string + counTwitter?: number + countWhatsapp?: number +}) => { + const [textShare, setTextShare] = useState('') + useEffect(() => { + if (slug) { + setTextShare( + 'Tienes que ver este recurso, ' + + location.href + + 'post/' + + slug + + ' lo he encontrado aquí, pásate hay más ' + + location.origin + ) + } else { + setTextShare( + 'Tienes que ver este recurso, ' + location.href + ' lo he encontrado aquí, pásate hay más ' + location.origin + ) + } + }, []) + + { + /*
+ +
*/ + } + const sharePost = async (postId, platform) => { + try { + const response = await fetch('/api/share', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ postId, platform }) + }) + + if (response.ok) { + const data = await response.json() + // Actualiza la interfaz de usuario según sea necesario + } else { + console.error('Error sharing post:', response.statusText) + } + } catch (error) { + console.error('Error sharing post:', error.message) + } + } + + return ( + + <> + + {/* */} + {/*
*/} + {'user'} + {/*
*/} +
+ + + + + {({ active }) => ( + { + sharePost(id, 'twitter') + }} + > +
+ {'twitter'} + Twitter +
+ {counTwitter} +
+ )} +
+ + {({ active }) => ( + { + sharePost(id, 'whatsapp') + }} + > +
+ {'whatsapp'} + Whatsapp +
+ {countWhatsapp} +
+ )} +
+
+
+
+ +
+ ) +} diff --git a/src/components/atoms/Modal.tsx b/src/components/atoms/Modal.tsx new file mode 100644 index 0000000..f718064 --- /dev/null +++ b/src/components/atoms/Modal.tsx @@ -0,0 +1,70 @@ +import { Dialog } from '@headlessui/react' +import Image from 'next/image' +import { useRouter } from 'next/router' +import AlertIcon from '../../../public/images/alert.svg' + +const Modal = ({ + setIsOpen, + isOpen, + icon, + tittle, + description, + textTrue, + textFalse = 'Cancelar', + functionTrue, + functionFalse +}: { + isOpen: boolean + setIsOpen: (value: boolean) => void + icon: boolean + tittle: string + description: string + textTrue: string + textFalse?: string + functionTrue: () => void + functionFalse: () => void +}) => { + const router = useRouter() + return ( + setIsOpen(false)} + className="fixed inset-0 flex items-center justify-center z-50" + > + + {icon && ( +
+ alert +
+ )} +

{tittle}

+
+

{description}

+
+ + +
+
+
+
+ ) +} + +export default Modal diff --git a/src/components/atoms/ModalCreateTag.tsx b/src/components/atoms/ModalCreateTag.tsx new file mode 100644 index 0000000..3871f14 --- /dev/null +++ b/src/components/atoms/ModalCreateTag.tsx @@ -0,0 +1,138 @@ +import { Dialog } from '@headlessui/react' +import { useSession } from 'next-auth/react' +import Image from 'next/image' +import { useRouter } from 'next/router' +import { SubmitHandler, useForm } from 'react-hook-form' +import AlertIcon from '../../../public/images/alert.svg' + +interface FormData { + name: string + slug: string + color: string +} + +const ModalCreateTag = ({ + setIsOpen, + isOpen, + icon, + tittle, + description, + textTrue, + textFalse = 'Cancelar', + functionTrue, + functionFalse +}: { + isOpen: boolean + setIsOpen: (value: boolean) => void + icon?: boolean + tittle: string + description?: string + textTrue: string + textFalse?: string + functionTrue: () => void + functionFalse: () => void +}) => { + const router = useRouter() + const { data: session } = useSession() + + const { + register, + handleSubmit, + watch, + getValues, + formState: { errors } + } = useForm() + + const onSubmit: SubmitHandler = async (dataForm) => { + const { name, slug, color } = dataForm + + // try { + // const response = await fetch('/api/tag', { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json' + // }, + + // body: JSON.stringify({ + // name, + // slug: slugify(name), + // color, + // userEmail: session?.user?.email + // }) + // }) + // // if (!response.ok) { + // // throw new Error('You have already used this title') + // // } + // if (!response.ok) { + // const errorData = await response.json() + // } + // const data = await response.json() + // setIsOpen(false) + // } catch (error) { + // console.error(error, 'Error fetching data') + // const errorMessage = (error as { message?: string })?.message || 'Error creating post' + // } + } + return ( + setIsOpen(false)} + className="fixed inset-0 flex items-center justify-center z-50" + > + + {icon && ( +
+ alert +
+ )} +

{tittle}

+
+
+

{description}

+
+ +
+
+ + +
+
+ + +
+
+
+
+
+ ) +} + +export default ModalCreateTag diff --git a/src/components/atoms/StatIndividual.tsx b/src/components/atoms/StatIndividual.tsx new file mode 100644 index 0000000..b550777 --- /dev/null +++ b/src/components/atoms/StatIndividual.tsx @@ -0,0 +1,10 @@ +const StatIndividual = ({ stat, tittle }: { stat: number; tittle: string }) => { + return ( +
+
{stat}
+

{tittle}

+
+ ) +} + +export default StatIndividual diff --git a/src/components/atoms/Tab.tsx b/src/components/atoms/Tab.tsx new file mode 100644 index 0000000..8be515b --- /dev/null +++ b/src/components/atoms/Tab.tsx @@ -0,0 +1,39 @@ +import { languageRedirect } from '@/helpers/changeLanguage' +import { useRouter } from 'next/router' +import { useState } from 'react' + +export const Tab = ({ className }: { className?: string }) => { + const router = useRouter() + const currentLanguage = router.locale || 'es' + const [selectedTab, setSelectedTab] = useState(currentLanguage) + const handleTabClick = (tab: string) => { + languageRedirect(router, tab) + setSelectedTab(tab) + } + return ( +
+
+ handleTabClick('es')} + > + Es + + handleTabClick('en')} + > + En + +
+
+ ) +} diff --git a/src/components/atoms/Tag.tsx b/src/components/atoms/Tag.tsx new file mode 100644 index 0000000..2512abd --- /dev/null +++ b/src/components/atoms/Tag.tsx @@ -0,0 +1,21 @@ +type Tag = { + color: string + name: string + className?: string + [others: string]: any +} + +const Tag = ({ color, name, className, others }: Tag) => { + return ( +
+
+

{name}

+
+
+ ) +} + +export default Tag diff --git a/src/components/layouts/Layout.tsx b/src/components/layouts/Layout.tsx new file mode 100644 index 0000000..9c9c067 --- /dev/null +++ b/src/components/layouts/Layout.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import { Footer } from '../organism/Footer' +import { Navbar } from '../organism/Navbar' + +type LayoutProps = { + children: React.ReactNode +} +export const Layout = ({ children }: LayoutProps) => { + return ( +
+ +
{children}
+
+
+ ) +} diff --git a/src/components/molecules/Card.tsx b/src/components/molecules/Card.tsx new file mode 100644 index 0000000..3de641b --- /dev/null +++ b/src/components/molecules/Card.tsx @@ -0,0 +1,301 @@ +import Image from 'next/image' +import Link from 'next/link' +import { FaRegComment } from 'react-icons/fa' +// import { FaLink } from 'react-icons/fa6' +// import { MdDelete, MdEdit } from 'react-icons/md' +import { useSession } from 'next-auth/react' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' +import { CgLoadbarSound } from 'react-icons/cg' +import { FaRegStar, FaStar } from 'react-icons/fa' +import { FcLike } from 'react-icons/fc' +import { FiHeart } from 'react-icons/fi' +import { Post } from '../../../type' +import { DropDownShare } from '../atoms/DropDownShare' +import Modal from '../atoms/Modal' + +interface CardProps { + post: Post +} + +export function Card({ post }: CardProps) { + // const { postData, updatePostData } = usePostContext() + const { + description, + title, + Category, + slug, + url, + Like, + id, + comments, + views, + twitterShareCount, + whatsappShareCount, + Favorite + // Tags + } = post + const { data: session } = useSession() + // const likeOfUser = Like?.some((user) => user.userEmail === session?.user?.email) + const [isLike, setIsLike] = useState() + const [isFavorite, setIsFavorite] = useState() + const [likesCount, setLikesCount] = useState(Like?.length || 0) + const [favoriteCount, setFavoriteCount] = useState(Favorite?.length || 0) + let [isOpen, setIsOpen] = useState(false) + const router = useRouter() + + useEffect(() => { + if (session && Like?.some((like) => like?.userEmail === session?.user?.email)) { + setIsLike(true) + } else { + setIsLike(false) + } + }, [Like, session]) + + useEffect(() => { + if (session && Favorite?.some((fav) => fav?.userEmail === session?.user?.email)) { + setIsFavorite(true) + } else { + setIsFavorite(false) + } + }, [Favorite, session]) + + const handleAddLike = async () => { + if (!session) { + setIsOpen(true) + return + } + const response = await fetch(`/api/like`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: session.user.email, + postId: id, + session: session + }) + }) + const data = await response.json() + if (response.ok) { + data && setIsLike(true) + setLikesCount((prevCount) => prevCount + 1) + } else { + console.error('Error al dar like:', data.error) + } + } + const handleDeleteLike = async () => { + if (!session) { + setIsOpen(true) + return + } + const response = await fetch('/api/like', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: session.user.email, + postId: id, + session: session + }) + }) + + const data = await response.json() + + if (response.ok) { + setIsLike(false) + setLikesCount((prevCount) => Math.max(0, prevCount - 1)) + } else { + console.error('Error al quitar like:', data.error) + } + } + const handleAddFavorite = async () => { + if (!session) { + setIsOpen(true) + return + } + const response = await fetch(`/api/favorite`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: session.user.email, + postId: id, + session: session + }) + }) + const data = await response.json() + if (response.ok) { + data && setIsFavorite(true) + setFavoriteCount((prevCount) => prevCount + 1) + } else { + console.error('Error al dar like:', data.error) + } + } + const handleDeleteFavorite = async () => { + if (!session) { + setIsOpen(true) + return + } + const response = await fetch('/api/favorite', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: session.user.email, + postId: id, + session: session + }) + }) + + const data = await response.json() + + if (response.ok) { + setIsFavorite(false) + setFavoriteCount((prevCount) => Math.max(0, prevCount - 1)) + } else { + console.error('Error al quitar like:', data.error) + } + } + + return ( +
+
+
+ img post +
+
+
+
+ +
+

{title}

+
+ +
+

+ {description?.length > 80 ? `${description.substring(0, 80)}...` : description} +

{' '} + {description?.length > 80 && ( + + ver más + + )} +
+
+
+ {/*
+ {Tags && + Tags?.slice(0, 4).map((tag) => ( +
+
+

{tag?.name}

+
+
+ ))} +
*/} +
+ {/*
+ + + +
*/} +
+
{ + isLike ? handleDeleteLike() : handleAddLike() + }} + > + {isLike ? ( + + ) : ( + + )} + + {/* */} +
+ + {likesCount} + +
+
+
{ + isFavorite ? handleDeleteFavorite() : handleAddFavorite() + }} + > + {isFavorite ? ( + + ) : ( + + )} + + {favoriteCount} + +
+
+
+ +
+ +
+ + + {comments?.length} + {' '} +
+
+
+ +
+ + {twitterShareCount + whatsappShareCount} + {' '} +
+
+ + + {views} + {' '} +
+
+
+
+ router.push('/login')} + functionFalse={() => router.back()} + /> +
+ ) +} diff --git a/src/components/molecules/CardList.tsx b/src/components/molecules/CardList.tsx new file mode 100644 index 0000000..ed524f9 --- /dev/null +++ b/src/components/molecules/CardList.tsx @@ -0,0 +1,48 @@ +import { usePostContext } from '@/context/PostContext' +import { Card } from './Card' +import Pagination from './Pagination' + +export function CardList({ page, cat, count }: any) { + // const [dataCards, setDataCards] = useState([]) + // const [countN, setCount] = useState(count) + const { postData, updatePostData } = usePostContext() + + // const getData = async (page: any, cat: any) => { + // console.log('funciona') + + // try { + // const response = await fetch(`http://localhost:3000/api/posts?page=${page || 1}&cat=${cat || ''}`, { + // cache: 'no-store' + // }) + // console.log(response) + + // if (!response.ok) { + // throw new Error('Failed') + // } + // const result = await response.json() + // updatePostData(result.posts) + // // setCount(result.count) + // } catch (error) { + // console.error(error) + // } + // } + // useEffect(() => { + // getData(page, cat) + // }, [page, cat]) + + // NO LO TENGO MUY CLARO PERO CREO QUE ESTO NO ME HACE FALTA, TENGO QUE REVISARLO CON CALMA + + const POST_PER_PAGE = 4 + const hasPrev = POST_PER_PAGE * (page - 1) > 0 + const hasNext = POST_PER_PAGE * (page - 1) + POST_PER_PAGE < count + return ( + <> +
+ {Array.isArray(postData) && postData?.map((item) => )} +
+
+ +
+ + ) +} diff --git a/src/components/molecules/CardPerfil.tsx b/src/components/molecules/CardPerfil.tsx new file mode 100644 index 0000000..3aae3b7 --- /dev/null +++ b/src/components/molecules/CardPerfil.tsx @@ -0,0 +1,279 @@ +import Link from 'next/link' +import { FaRegComment } from 'react-icons/fa' +// import { FaLink } from 'react-icons/fa6' +// import { MdDelete, MdEdit } from 'react-icons/md' +import { useSession } from 'next-auth/react' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' +import { CgLoadbarSound } from 'react-icons/cg' +import { FaRegStar, FaStar } from 'react-icons/fa' +import { FcLike } from 'react-icons/fc' +import { FiHeart } from 'react-icons/fi' +import { Post } from '../../../type' +import { DropDownShare } from '../atoms/DropDownShare' +import Modal from '../atoms/Modal' + +interface CardPerfilProps { + post: Post +} + +export function CardPerfil({ post }: CardPerfilProps) { + // const { postData, updatePostData } = usePostContext() + const { + description, + title, + Category, + slug, + url, + Like, + id, + comments, + views, + twitterShareCount, + whatsappShareCount, + Favorite + } = post + const { data: session } = useSession() + // const likeOfUser = Like?.some((user) => user.userEmail === session?.user?.email) + const [isLike, setIsLike] = useState() + const [isFavorite, setIsFavorite] = useState() + const [likesCount, setLikesCount] = useState(Like?.length || 0) + const [favoriteCount, setFavoriteCount] = useState(Favorite?.length || 0) + let [isOpen, setIsOpen] = useState(false) + const router = useRouter() + + useEffect(() => { + if (session && Like?.some((like) => like?.userEmail === session?.user?.email)) { + setIsLike(true) + } else { + setIsLike(false) + } + }, [Like, session]) + + useEffect(() => { + if (session && Favorite?.some((fav) => fav?.userEmail === session?.user?.email)) { + setIsFavorite(true) + } else { + setIsFavorite(false) + } + }, [Favorite, session]) + + const handleAddLike = async () => { + if (!session) { + setIsOpen(true) + return + } + const response = await fetch(`/api/like`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: session.user.email, + postId: id, + session: session + }) + }) + const data = await response.json() + if (response.ok) { + data && setIsLike(true) + setLikesCount((prevCount) => prevCount + 1) + } else { + console.error('Error al dar like:', data.error) + } + } + const handleDeleteLike = async () => { + if (!session) { + setIsOpen(true) + return + } + const response = await fetch('/api/like', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: session.user.email, + postId: id, + session: session + }) + }) + + const data = await response.json() + + if (response.ok) { + setIsLike(false) + setLikesCount((prevCount) => Math.max(0, prevCount - 1)) + } else { + console.error('Error al quitar like:', data.error) + } + } + const handleAddFavorite = async () => { + if (!session) { + setIsOpen(true) + return + } + const response = await fetch(`/api/favorite`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: session.user.email, + postId: id, + session: session + }) + }) + const data = await response.json() + if (response.ok) { + data && setIsFavorite(true) + setFavoriteCount((prevCount) => prevCount + 1) + } else { + console.error('Error al dar like:', data.error) + } + } + const handleDeleteFavorite = async () => { + if (!session) { + setIsOpen(true) + return + } + const response = await fetch('/api/favorite', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: session.user.email, + postId: id, + session: session + }) + }) + + const data = await response.json() + + if (response.ok) { + setIsFavorite(false) + setFavoriteCount((prevCount) => Math.max(0, prevCount - 1)) + } else { + console.error('Error al quitar like:', data.error) + } + } + return ( +
+ {/*
+
+ img post +
+
*/} +
+
+ +
+

{title}

+
+ +
+
+ +
+ {/*
+ + + +
*/} +
+
{ + isLike ? handleDeleteLike() : handleAddLike() + }} + > + {isLike ? ( + + ) : ( + + )} + + {/* */} +
+ + {likesCount} + +
+
+
{ + isFavorite ? handleDeleteFavorite() : handleAddFavorite() + }} + > + {isFavorite ? ( + + ) : ( + + )} + + {favoriteCount} + +
+
+
+ +
+ +
+ + + {comments?.length} + {' '} +
+
+
+ +
+ + {twitterShareCount + whatsappShareCount} + {' '} +
+
+ + + {views} + {' '} +
+
+
+
+ router.push('/login')} + functionFalse={() => router.back()} + /> +
+ ) +} diff --git a/src/components/molecules/Coment.tsx b/src/components/molecules/Coment.tsx new file mode 100644 index 0000000..4853183 --- /dev/null +++ b/src/components/molecules/Coment.tsx @@ -0,0 +1,107 @@ +'use client' + +import { useSession } from 'next-auth/react' +import Image from 'next/image' +import { useRouter } from 'next/router' +import { useState } from 'react' +import { MdDelete } from 'react-icons/md' +import useSWR from 'swr' +import { Comment } from '../../../type' +import Modal from '../atoms/Modal' + +interface CommentProps { + postSlug?: string | string[] | undefined + // comments?: Comment[] | [] | undefined + // setComments: React.Dispatch> +} +export default function Comment({ + postSlug +}: // comments, setComments +CommentProps) { + const router = useRouter() + const session = useSession() + const [showModalComment, setShowModalComment] = useState(false) + const fetcher = async (url: string) => { + const res = await fetch(url) + const data = await res.json() + if (!res.ok) { + throw new Error('Failed to fetch comments') + } + return data + } + // const [comments, setComments] = useState([]) + // const [loading, setLoading] = useState(true) + const { data, mutate, isLoading } = useSWR(`/api/comments/?postSlug=${postSlug}`, fetcher) + const formatDate = (dateString: string) => { + const options: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'long', + day: 'numeric' + } + return new Date(dateString).toLocaleDateString('en-US', options) + } + const deleteComment = async (commentId: string) => { + try { + const response = await fetch(`/api/comments`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: commentId }) + }) + if (!response.ok) { + throw new Error('Failed to delete comment') + } + mutate() + } catch (error) {} + } + return ( +
+
+ {isLoading + ? '...loading' + : data?.map((comment: Comment) => ( +
+
+
+
+ {comment?.user?.image && ( +
+ avatar Mujer +
+ )} +
+

{comment?.user?.name}

+

{formatDate(comment?.createdAt)}

+
+
+ {comment?.user?.email === session.data?.user?.email && ( +
setShowModalComment(true)}> + +
+ )} +
+

{comment.description}

+
+ {/* MODAL PARA ELIMINAR COMENTARIO */} + deleteComment(comment?.id)} + functionFalse={() => setShowModalComment(false)} + /> +
+ ))} +
+
+ ) +} diff --git a/src/components/molecules/CommentWrite.tsx b/src/components/molecules/CommentWrite.tsx new file mode 100644 index 0000000..61583d5 --- /dev/null +++ b/src/components/molecules/CommentWrite.tsx @@ -0,0 +1,60 @@ +import { useSession } from 'next-auth/react' +import { useState } from 'react' +import { useSWRConfig } from 'swr' +import { Button } from '../atoms/Button' + +export default function CommentWrite({ postSlug }: any) { + const [description, setDesc] = useState('') + const { data: session } = useSession() + const [loading, setLoading] = useState(false) + const { mutate } = useSWRConfig() + + const handleChange = (e: React.ChangeEvent) => { + setDesc(e.target.value) + } + const handleSubmit = async () => { + try { + setLoading(true) + + const response = await fetch('/api/comments', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ description, postSlug, session }) + }) + + if (!response.ok) { + throw new Error('Failed to create comment') + } + + setDesc('') + mutate(`/api/comments/?postSlug=${postSlug}`) + } catch (error) { + console.error(error) + } finally { + setLoading(false) + } + } + return ( +
+

Comments

+
+