first commit

This commit is contained in:
Manuel Cebreiro 2024-05-21 19:47:44 +02:00
parent e09eda6658
commit b4a58c8a54
121 changed files with 10201 additions and 2 deletions

3
.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

37
.gitignore vendored Normal file
View File

@ -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

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 120
}

7
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"workbench.colorCustomizations": {
"activityBar.background": "#2F2F07",
"titleBar.activeBackground": "#41420A",
"titleBar.activeForeground": "#FBFBE7"
}
}

View File

@ -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 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
<div>
<Navbar />
<div>{children}</div>
<Footer />
</div>
```
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.

9
next-i18next.config.js Normal file
View File

@ -0,0 +1,9 @@
/** @type {import('next-i18next').UserConfig} */
module.exports = {
i18n: {
defaultLocale: 'es',
locales: ['es', 'en']
},
serializeConfig: false,
saveMissing: true
}

15
next.config.js Normal file
View File

@ -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

43
package.json Normal file
View File

@ -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"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

42
prisma/comment.ts Normal file
View File

@ -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
}
})
}

59
prisma/favorite.ts Normal file
View File

@ -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')
}
}

59
prisma/like.ts Normal file
View File

@ -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')
}
}

68
prisma/post.ts Normal file
View File

@ -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')
}
}

156
prisma/schema.prisma Normal file
View File

@ -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?
}

25
prisma/stats.ts Normal file
View File

@ -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()
}
}

82
prisma/tag.ts Normal file
View File

@ -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')
// }
// }

93
prisma/user.ts Normal file
View File

@ -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()
}
}

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg version="1.1" id="Icons" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 32 32" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#3A559F;}
.st2{fill:#F4F4F4;}
.st3{fill:#FF0084;}
.st4{fill:#0063DB;}
.st5{fill:#00ACED;}
.st6{fill:#FFEC06;}
.st7{fill:#FF0000;}
.st8{fill:#25D366;}
.st9{fill:#0088FF;}
.st10{fill:#314358;}
.st11{fill:#EE6996;}
.st12{fill:#01AEF3;}
.st13{fill:#FFFEFF;}
.st14{fill:#F06A35;}
.st15{fill:#00ADEF;}
.st16{fill:#1769FF;}
.st17{fill:#1AB7EA;}
.st18{fill:#6001D1;}
.st19{fill:#E41214;}
.st20{fill:#05CE78;}
.st21{fill:#7B519C;}
.st22{fill:#FF4500;}
.st23{fill:#00F076;}
.st24{fill:#FFC900;}
.st25{fill:#00D6FF;}
.st26{fill:#FF3A44;}
.st27{fill:#FF6A36;}
.st28{fill:#0061FE;}
.st29{fill:#F7981C;}
.st30{fill:#EE1B22;}
.st31{fill:#EF3561;}
.st32{fill:none;stroke:#FFFFFF;stroke-width:2;stroke-miterlimit:10;}
.st33{fill:#0097D3;}
.st34{fill:#01308A;}
.st35{fill:#019CDE;}
.st36{fill:#FFD049;}
.st37{fill:#16A05D;}
.st38{fill:#4486F4;}
.st39{fill:none;}
.st40{fill:#34A853;}
.st41{fill:#4285F4;}
.st42{fill:#FBBC05;}
.st43{fill:#EA4335;}
</style>
<path class="st8" d="M17,0C8.7,0,2,6.7,2,15c0,3.4,1.1,6.6,3.2,9.2l-2.1,6.4c-0.1,0.4,0,0.8,0.3,1.1C3.5,31.9,3.8,32,4,32
c0.1,0,0.3,0,0.4-0.1l6.9-3.1C13.1,29.6,15,30,17,30c8.3,0,15-6.7,15-15S25.3,0,17,0z"/>
<path class="st0" d="M25.7,20.5c-0.4,1.2-1.9,2.2-3.2,2.4C22.2,23,21.9,23,21.5,23c-0.8,0-2-0.2-4.1-1.1c-2.4-1-4.8-3.1-6.7-5.8
L10.7,16C10.1,15.1,9,13.4,9,11.6c0-2.2,1.1-3.3,1.5-3.8c0.5-0.5,1.2-0.8,2-0.8c0.2,0,0.3,0,0.5,0c0.7,0,1.2,0.2,1.7,1.2l0.4,0.8
c0.3,0.8,0.7,1.7,0.8,1.8c0.3,0.6,0.3,1.1,0,1.6c-0.1,0.3-0.3,0.5-0.5,0.7c-0.1,0.2-0.2,0.3-0.3,0.3c-0.1,0.1-0.1,0.1-0.2,0.2
c0.3,0.5,0.9,1.4,1.7,2.1c1.2,1.1,2.1,1.4,2.6,1.6l0,0c0.2-0.2,0.4-0.6,0.7-0.9l0.1-0.2c0.5-0.7,1.3-0.9,2.1-0.6
c0.4,0.2,2.6,1.2,2.6,1.2l0.2,0.1c0.3,0.2,0.7,0.3,0.9,0.7C26.2,18.5,25.9,19.8,25.7,20.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/images/React.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

7
public/images/alert.svg Normal file
View File

@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 507.425 507.425" xml:space="preserve" fill="#000000">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <path style="fill:#DF5C4E;" d="M329.312,129.112l13.6,22l150.8,242.4c22.4,36,6,65.2-36.8,65.2h-406.4c-42.4,0-59.2-29.6-36.8-65.6 l198.8-320.8c22.4-36,58.8-36,81.2,0L329.312,129.112z"/> <g> <path style="fill:#F4EFEF;" d="M253.712,343.512c-10.8,0-20-8.8-20-20v-143.2c0-10.8,9.2-20,20-20s20,8.8,20,20v143.2 C273.712,334.312,264.512,343.512,253.712,343.512z"/> <path style="fill:#F4EFEF;" d="M253.312,407.112c-5.2,0-10.4-2-14-6c-3.6-3.6-6-8.8-6-14s2-10.4,6-14c3.6-3.6,8.8-6,14-6 s10.4,2,14,6c3.6,3.6,6,8.8,6,14s-2,10.4-6,14C263.712,404.712,258.512,407.112,253.312,407.112z"/> </g> <path d="M456.912,465.512h-406.4c-22,0-38.4-7.6-46-21.6s-5.6-32.8,6-51.2l198.8-321.6c11.6-18.8,27.2-29.2,44.4-29.2l0,0 c16.8,0,32.4,10,43.6,28.4l35.2,56.4l0,0l13.6,22l150.8,243.6c11.6,18.4,13.6,37.2,6,51.2 C495.312,457.912,478.912,465.512,456.912,465.512z M253.312,49.912L253.312,49.912c-14,0-27.2,8.8-37.6,25.2l-198.8,321.6 c-10,16-12,31.6-5.6,43.2s20.4,17.6,39.2,17.6h406.4c18.8,0,32.8-6.4,39.2-17.6c6.4-11.2,4.4-27.2-5.6-43.2l-150.8-243.6l-13.6-22 l-35.2-56.4C280.512,58.712,267.312,49.912,253.312,49.912z"/> <path d="M249.712,347.512c-13.2,0-24-10.8-24-24v-143.2c0-13.2,10.8-24,24-24s24,10.8,24,24v143.2 C273.712,336.712,262.912,347.512,249.712,347.512z M249.712,164.312c-8.8,0-16,7.2-16,16v143.2c0,8.8,7.2,16,16,16s16-7.2,16-16 v-143.2C265.712,171.512,258.512,164.312,249.712,164.312z"/> <path d="M249.712,411.112L249.712,411.112c-6.4,0-12.4-2.4-16.8-6.8c-4.4-4.4-6.8-10.8-6.8-16.8c0-6.4,2.4-12.4,6.8-16.8 c4.4-4.4,10.8-7.2,16.8-7.2c6.4,0,12.4,2.4,16.8,7.2c4.4,4.4,7.2,10.4,7.2,16.8s-2.4,12.4-7.2,16.8 C262.112,408.312,256.112,411.112,249.712,411.112z M249.712,371.112c-4,0-8.4,1.6-11.2,4.8c-2.8,2.8-4.8,7.2-4.8,11.2 c0,4.4,1.6,8.4,4.8,11.2c2.8,2.8,7.2,4.8,11.2,4.8s8.4-1.6,11.2-4.8c2.8-2.8,4.8-7.2,4.8-11.2s-1.6-8.4-4.8-11.2 C258.112,372.712,253.712,371.112,249.712,371.112z"/> </g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 366.34 366.34" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:#00214e;}.cls-2{fill:#f2a196;}.cls-3{fill:#e88870;}.cls-4{fill:#845161;}.cls-5,.cls-6{fill:none;stroke-miterlimit:10;}.cls-5{stroke:#00214e;}.cls-6{stroke:#f2a196;}</style></defs><title>Artboards_Diversity_Avatars_by_Netguru</title><path class="cls-1" d="M256.93,135.87c-2.42,7.2-2.81,17-7.06,23.38-9.35,14-27.49,16.4-42.25,21.44a11.74,11.74,0,0,0-2.82.91l-5.22,1.65q-7.53,2.37-15,4.8c-6.92,2.25-16.43,3.63-21.41,9.31a109.81,109.81,0,0,0-8,10.33c-.48.71-1.07,1.49-1.92,1.56-.54,0-1-.71-1.45-1-8.88-4.78-15.41-11.34-20.88-19.66a126,126,0,0,1-19-86.17c.06-.44.12-.87.19-1.31a51.6,51.6,0,0,1,12.78-26.06A86.79,86.79,0,0,1,148.39,57a148.21,148.21,0,0,1,16.18-7.35A110,110,0,0,1,202.25,42,94.36,94.36,0,0,1,238,49.07c15.62,6.25,27.73,17.92,28.74,35.58C267.74,102.06,262.46,119.41,256.93,135.87Z"/><circle class="cls-2" cx="134.83" cy="162.86" r="17"/><circle class="cls-3" cx="140.68" cy="161.86" r="16"/><path class="cls-2" d="M296.41,286a184.56,184.56,0,0,1-226.48-1l48.66-22.81a46.83,46.83,0,0,0,6.65-3.82l1.11-.78.78-.6a46.35,46.35,0,0,0,12.78-15.09c4-7.55,5.32-15.89,5.38-24.39,0-2.87-.06-5.74-.15-8.61s-.19-5.7-.22-8.56q-.06-4.76-.1-9.51l1.84.95.14.07,5.2,2.69,2.41.41,27.88,4.74,11.05,1.88L213.41,205l.94,32,.39,13.3.07,2.24v.33l12.1,4.92.75.31Z"/><path class="cls-3" d="M145.14,202.68,178,233.5a51.66,51.66,0,0,0,36.77,12.79l-.39-13.3-.94-32-20.07-3.42c-2.74,1.24-5.48,2.48-8.22,3.56-8.2,3.23-17.47,2-25.42-1.36a36.93,36.93,0,0,1-14.87-12Z"/><path class="cls-4" d="M296.41,286a184.56,184.56,0,0,1-226.48-1l48.66-22.81a46.83,46.83,0,0,0,6.65-3.82l1.11-.78c24.36,16.61,56.82,26.66,85,14a37.81,37.81,0,0,0,16.31-13.51Z"/><path class="cls-2" d="M254.46,171.31a106,106,0,0,1-3.51,21.6c-4.31,16.92-11.45,33.65-15,39.91-2.66,4.67-6.37,5.57-12.24,7a51.47,51.47,0,0,1-42.29-8.94L145,204c-.51-5-1.37-10.25-2.33-15.6-2.78-15.54-6.38-32-4.82-46.54a17,17,0,0,1,5.64-10.69c8.38-7.94,24-11.54,33.74-14,14.62-3.76,29.64-6,44.05-10.43,10.61-3.3,22.06-8.32,28.42-17.88,0,0,1.1,10.81,1.35,13.28.53,5.09.52,9.51,1.07,14.53a52,52,0,0,1-1,16.45c-.83,3.73-1.81,7.45-2.51,11.21-.67,3.6.28,3.66,2.13,6.21C254.54,155.74,254.69,165.14,254.46,171.31Z"/><path class="cls-1" d="M187.56,143c6.1,0,6.1,9.38,0,9.43h-.27c-6.1,0-6.1-9.38,0-9.43h.27Z"/><path class="cls-1" d="M237.69,141.6c5.66.05,5.66,8.7,0,8.75h-.25c-5.67,0-5.67-8.7,0-8.75h.25Z"/><path class="cls-5" d="M219.4,147c-.05.2,4.79,8.56,7.31,17.59a58,58,0,0,1,1.62,14.75H214.82"/><path class="cls-1" d="M251,192.91c-4.31,16.92-11.45,33.65-15,39.91-2.66,4.67-6.37,5.57-12.24,7a51.47,51.47,0,0,1-42.29-8.94L145,204c-.6-5.89-2.75-11.53-4-17.33-.54-2.62-1-5.36-.36-8a6.84,6.84,0,0,0,1.71,3.73c4.72,6.42,10.24,13.67,18.1,16.31,8.2,2.75,17.54,3.18,25.45-.7,6.89-3.38,13.64-7.79,20.83-10.57a35.05,35.05,0,0,1,15.11-2.61c7.34.5,10.48,3.81,15.85,8.18,1.76,1.44,3.73,2.94,6,2.95C246.29,196,248.79,194.56,251,192.91Z"/><path class="cls-6" d="M204.37,198.32a29.8,29.8,0,0,0,20.89-1.21"/><path class="cls-5" d="M172.49,135a80.35,80.35,0,0,1,28.13-.8"/><path class="cls-5" d="M231.54,135.13A55.7,55.7,0,0,1,249,133.92"/><path class="cls-5" d="M203.49,174.34a3.4,3.4,0,0,0,2.11,6.38"/><polygon class="cls-1" points="141.67 188.44 142.67 188.38 143.33 189.16 145.48 202.98 145 203.16 144.28 202.17 141.67 188.44"/></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 366.34 366.34" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style>.cls-1{fill:#00214e;}.cls-2{fill:#f2a196;}.cls-3{fill:#e88870;}.cls-4{fill:none;stroke:#00214e;stroke-miterlimit:10;}.cls-5{fill:#f2d4cf;}.cls-6{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" x1="109.87" y1="140.89" x2="166.58" y2="65.41" gradientUnits="userSpaceOnUse"><stop offset="0.29" stop-color="#00214e"/><stop offset="0.51" stop-color="#6878b1"/><stop offset="0.79" stop-color="#00214e"/></linearGradient></defs><title>Artboards_Diversity_Avatars_by_Netguru</title><path class="cls-1" d="M258.38,149.67c-.41-33.19-6.33-69.59-36.31-89.15C195,42.86,158.32,43.72,131.45,61a77.55,77.55,0,0,0-10.22,7.8,105.09,105.09,0,0,0-26.66,39,130.84,130.84,0,0,0-9,54.2c1.67,37.14-2.36,74.39-2.36,111.68,0,1.29,0,2.77,1,3.59a4.27,4.27,0,0,0,3,.59c15.62-.87,29.45-3.57,44.53-6.08,18.8-3.12,39.07-2.23,58.1-3.42,11.33-.66,23.24-2.49,34.57-2,3.61.16,35.22,4.85,35.36,6.8C256.75,231.8,258.89,191,258.38,149.67ZM148,145.49l-14.09-10a130.31,130.31,0,0,0,31.94-16,70.63,70.63,0,0,0,21.84-23l5.71,3.61Z"/><path class="cls-2" d="M296.41,280.37a184.56,184.56,0,0,1-226.48-1l48.66-22.81a47.68,47.68,0,0,0,4.35-2.34l1.12-.7c.4-.25.79-.51,1.18-.78a46.54,46.54,0,0,0,14.67-16.47c4-7.55,5.32-15.89,5.38-24.39,0-4.67-.19-9.34-.31-14q0-1.57-.06-3.15-.06-4.75-.1-9.51l.07,0,1.91,1,5.2,2.69,30.29,5.15,31.12,5.3.71,24,.2,6.88,0,1.07.47,15.87,11.47,4.67,9,3.64Z"/><path class="cls-3" d="M214.12,223.37a12.12,12.12,0,0,1-7.34,2.11C192,223.89,163.14,212.3,145,190.85q0-1.57-.06-3.15l0-2.47,1.91,1,5.2,2.69,30.29,5.15,31.12,5.3Z"/><circle class="cls-2" cx="119.82" cy="153.19" r="17"/><circle class="cls-3" cx="125.82" cy="151.19" r="17"/><path class="cls-2" d="M235.36,127.45c11.74,40.68-13.2,89.87-28.54,89.87-21,0-72-16.78-83.73-57.46S127,78.94,158,70,223.62,86.76,235.36,127.45Z"/><path class="cls-4" d="M177.31,176.87a29.74,29.74,0,0,0,18.54,9.69"/><path class="cls-4" d="M197.93,131.73c-.05.2,4.79,8.56,7.31,17.59a58,58,0,0,1,1.62,14.76H193.35"/><path class="cls-4" d="M210.34,123.22a31.18,31.18,0,0,1,22.85-2.16"/><path class="cls-4" d="M151.19,127.2a36.75,36.75,0,0,1,31.23-1"/><path class="cls-1" d="M168.73,136.06c6.1.05,6.1,9.38,0,9.42h-.27c-6.1,0-6.1-9.37,0-9.42h.27Z"/><path class="cls-1" d="M218.86,134.67c5.66,0,5.66,8.7,0,8.74h-.25c-5.67,0-5.67-8.7,0-8.74h.25Z"/><path class="cls-1" d="M198.93,59.32a113.91,113.91,0,0,1-2.27,14,81,81,0,0,1-9,23.18,70.63,70.63,0,0,1-21.84,23,130.31,130.31,0,0,1-31.94,16,202.94,202.94,0,0,1-27.46,7.23c.31-7.63,0-17.07.94-25.93.7-6.36,2.1-12.43,4.92-17.32a79.54,79.54,0,0,1,32.19-30.3l.12-.06C159.38,61.51,182.29,54,198.93,59.32Z"/><path class="cls-5" d="M296.41,280.37a184.56,184.56,0,0,1-226.48-1l48.66-22.81a46.83,46.83,0,0,0,6.65-3.82c.64-.44,1.28-.9,1.89-1.38a46.35,46.35,0,0,0,12.78-15.09,44.69,44.69,0,0,0,4.64-14.48,28.66,28.66,0,0,0,2.22,1.94A95.14,95.14,0,0,0,166.59,235a99,99,0,0,0,10.46,3.69,93.52,93.52,0,0,0,33,3.49c1.54-.12,3.09-.27,4.63-.38l.15,5.08v.33l12.1,4.92Z"/><path class="cls-6" d="M187.64,96.54a70.63,70.63,0,0,1-21.84,23,130.31,130.31,0,0,1-31.94,16l-26.52-18.7-12.77-9a105.09,105.09,0,0,1,26.66-39A77.55,77.55,0,0,1,131.45,61l13,8.22Z"/></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

51
public/images/back.svg Normal file
View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<circle style="fill:#FAD24D;" cx="256" cy="256" r="256"/>
<ellipse style="fill:#EDB937;" cx="256" cy="421.64" rx="182.28" ry="14.369"/>
<path style="fill:#666666;" d="M65.982,67.222h380.036c12.486,0,22.7,10.217,22.7,22.703V324.03H43.282V89.925
C43.282,77.439,53.496,67.222,65.982,67.222z"/>
<path style="fill:#15BDB2;" d="M58.695,308.614h394.607V89.922c0-3.979-3.308-7.287-7.287-7.287H65.979
c-3.976,0-7.287,3.308-7.287,7.287v218.693L58.695,308.614L58.695,308.614z"/>
<g>
<path style="fill:#FFFFFF;" d="M468.718,324.03v24.826c0,12.489-11.261,22.703-25.026,22.703H68.305
c-13.765,0-25.026-10.214-25.026-22.703V324.03h425.436H468.718z"/>
<path style="fill:#FFFFFF;" d="M144.153,234.3H131.29l-2.201-14.717h-15.643l-2.201,14.717H99.54l12.979-81.119h18.657
l12.979,81.119H144.153z M115.064,208.573h12.284l-6.14-41.024L115.064,208.573z"/>
<path style="fill:#FFFFFF;" d="M168.837,153.181c6.413,0,11.182,1.7,14.314,5.099c3.129,3.399,4.693,8.384,4.693,14.951v10.544
c0,6.568-1.563,11.549-4.693,14.949c-3.129,3.399-7.901,5.099-14.314,5.099h-6.025v30.476h-12.748V153.18h18.773V153.181z
M162.812,164.769v27.465h6.025c2.009,0,3.554-0.54,4.635-1.624c1.081-1.081,1.621-3.09,1.621-6.025v-12.168
c0-2.935-0.54-4.944-1.621-6.025c-1.083-1.081-2.628-1.621-4.635-1.621h-6.025V164.769z"/>
<path style="fill:#FFFFFF;" d="M193.75,153.181h12.748V234.3H193.75V153.181z"/>
<path style="fill:#FFFFFF;" d="M367.541,206.507l4.024-15.339l-9.107-3.575c0.307-3.438,0.288-6.901-0.055-10.34l9.073-3.67
l-4.184-15.295l-9.663,1.458c-1.437-3.101-3.179-6.091-5.228-8.932l6.02-7.709l-11.274-11.155l-7.646,6.101
c-2.861-2.02-5.872-3.733-8.983-5.136l1.358-9.679l-15.339-4.024l-3.575,9.107c-3.438-0.307-6.903-0.291-10.34,0.055l-3.667-9.073
l-15.297,4.184l1.458,9.663l0.005-0.002c-3.098,1.435-6.091,3.179-8.932,5.228l-7.709-6.022l-11.155,11.274l6.101,7.646
c-2.02,2.861-3.733,5.87-5.136,8.983l-9.679-1.358l-4.024,15.339l9.107,3.575c-0.307,3.438-0.288,6.901,0.055,10.34l-9.073,3.667
l4.184,15.297l9.66-1.458c1.437,3.098,3.179,6.091,5.228,8.932l-6.02,7.709L269,233.453l7.646-6.101
c2.858,2.02,5.87,3.733,8.983,5.136l-1.358,9.676l15.339,4.026l3.575-9.107c3.438,0.307,6.901,0.288,10.34-0.055l3.67,9.073
l15.295-4.184l-1.458-9.66c3.101-1.437,6.091-3.179,8.932-5.228l7.709,6.02l11.155-11.271l-6.101-7.646
c2.02-2.858,3.733-5.87,5.136-8.983L367.541,206.507z M325.84,200.282c-9.713,9.813-25.538,9.894-35.35,0.184
c-9.813-9.713-9.895-25.537-0.184-35.35c9.713-9.813,25.538-9.894,35.35-0.184C335.469,174.645,335.55,190.47,325.84,200.282z"/>
</g>
<path style="fill:#ECF0F1;" d="M308.071,150.421c-17.826,0-32.279,14.45-32.279,32.279c0,17.826,14.451,32.279,32.279,32.279
c17.826,0,32.279-14.451,32.279-32.279C340.35,164.874,325.9,150.421,308.071,150.421z M325.84,200.284
c-9.713,9.813-25.538,9.894-35.35,0.184c-9.813-9.712-9.895-25.537-0.184-35.35c9.713-9.813,25.538-9.894,35.35-0.184
C335.469,174.647,335.55,190.472,325.84,200.284z"/>
<path style="fill:#FFFFFF;" d="M417.06,242.49c0.098-1.815,0.006-3.649-0.285-5.477l5.244-2.987l-5.957-12.386l-5.65,2.028
c-1.236-1.38-2.605-2.605-4.076-3.67l1.598-5.821l-12.971-4.546l-2.563,5.429c-1.815-0.098-3.649-0.006-5.477,0.285l-2.986-5.241
l-12.386,5.959l2.028,5.647c-1.377,1.236-2.605,2.605-3.67,4.076l-5.821-1.598l-4.546,12.971l5.429,2.565
c-0.097,1.812-0.008,3.649,0.283,5.474l-5.244,2.987l5.957,12.386l5.65-2.028c1.238,1.377,2.607,2.605,4.076,3.67l-1.595,5.821
l12.971,4.546l2.563-5.432c1.812,0.1,3.649,0.008,5.477-0.283l2.985,5.244l12.386-5.957l-2.028-5.647
c1.38-1.239,2.605-2.608,3.67-4.079l5.821,1.598l4.546-12.971L417.06,242.49z M400.732,250.72c-5.309,5.367-13.963,5.408-19.329,0.1
c-5.363-5.31-5.408-13.963-0.1-19.329c5.309-5.365,13.963-5.408,19.329-0.1C405.997,236.702,406.041,245.353,400.732,250.72z"/>
<path style="fill:#ECF0F1;" d="M391.016,221.639c-10.752,0-19.468,8.716-19.468,19.468s8.716,19.468,19.468,19.468
c10.752,0,19.468-8.716,19.468-19.468C410.484,230.355,401.768,221.639,391.016,221.639z M400.73,250.72
c-5.309,5.365-13.963,5.408-19.326,0.1c-5.366-5.309-5.411-13.963-0.103-19.326v-0.003c5.312-5.367,13.963-5.411,19.329-0.1
C405.995,236.699,406.039,245.353,400.73,250.72z"/>
<circle style="fill:#B6B6B8;" cx="256" cy="346.8" r="7.814"/>
<path style="fill:#C2C2C4;" d="M305.065,407.271l36.095,11.564H170.836l29.123-11.564v-35.712h105.104v35.712H305.065z"/>
<path style="fill:#B6B6B8;" d="M305.065,407.271l-105.104-35.712h105.104V407.271z"/>
<path style="fill:#ECF0F1;" d="M199.959,407.271h105.104l36.095,11.564v4.981h-85.161h-85.163v-4.981l29.123-11.564H199.959z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="212px" height="212px" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="2.304"/>
<g id="SVGRepo_iconCarrier"> <rect width="48" height="48" fill="white" fill-opacity="0.01"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M44 40.8361C39.1069 34.8632 34.7617 31.4739 30.9644 30.6682C27.1671 29.8625 23.5517 29.7408 20.1182 30.303V41L4 23.5453L20.1182 7V17.167C26.4667 17.2172 31.8638 19.4948 36.3095 24C40.7553 28.5052 43.3187 34.1172 44 40.8361Z" fill="#a0a1a2" stroke="#000000" stroke-width="4" stroke-linejoin="round"/> </g>
</svg>

After

Width:  |  Height:  |  Size: 907 B

BIN
public/images/css.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

1
public/images/css.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" height="2500" width="2183" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 124 141.53"><path d="M10.383 126.892L0 0l124 .255-10.979 126.637-50.553 14.638z" fill="#1b73ba"/><path d="M62.468 129.275V12.085l51.064.17-9.106 104.85z" fill="#1c88c7"/><path d="M100.851 27.064H22.298l2.128 15.318h37.276l-36.68 15.745 2.127 14.808h54.043l-1.958 20.68-18.298 3.575-16.595-4.255-1.277-11.745H27.83l2.042 24.426 32.681 9.106 31.32-9.957 4-47.745H64.765l36.085-14.978z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 495 B

77
public/images/cursos.svg Normal file
View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<path style="fill:#E4EAF8;" d="M486.881,324.409H161.937c-9.446,0-17.102-7.656-17.102-17.102V50.772
c0-9.446,7.656-17.102,17.102-17.102h324.944c9.446,0,17.102,7.656,17.102,17.102v256.534
C503.983,316.753,496.326,324.409,486.881,324.409z"/>
<rect x="409.921" y="67.875" style="fill:#7DF5A5;" width="42.756" height="222.33"/>
<rect x="332.96" y="136.284" style="fill:#FFDC64;" width="42.756" height="153.921"/>
<rect x="256" y="204.693" style="fill:#FF5050;" width="42.756" height="85.511"/>
<path style="fill:#D29B6E;" d="M210.119,363.267L182.2,353.96c-6.984-2.328-11.694-8.863-11.694-16.225v-21.877H119.2v21.878
c0,7.361-4.71,13.897-11.694,16.225l-27.918,9.307c-6.984,2.328-11.694,8.863-11.694,16.225v98.837h153.921v-98.838
C221.813,372.13,217.103,365.595,210.119,363.267z"/>
<path style="fill:#5B5D6E;" d="M210.119,363.267L182.2,353.96c-1.677-0.559-3.205-1.386-4.586-2.384
c-7.844,9.446-19.527,15.589-32.762,15.589c-13.235,0-24.918-6.143-32.762-15.589c-1.381,0.997-2.908,1.826-4.586,2.384
l-27.918,9.307c-6.984,2.328-11.694,8.863-11.694,16.225v98.838h153.921v-98.838C221.813,372.13,217.103,365.595,210.119,363.267z"
/>
<ellipse style="fill:#694B4B;" cx="144.856" cy="243.173" rx="68.409" ry="64.134"/>
<path style="fill:#5A4146;" d="M161.382,181.003c-5.303-1.236-10.819-1.964-16.529-1.964c-37.781,0-68.409,28.714-68.409,64.134
c0,30.414,22.607,55.826,52.91,62.418c0-11.005,0-27.379,0-51.729C129.354,216.819,147.19,193.639,161.382,181.003z"/>
<path style="fill:#F0C087;" d="M187.973,311.4l-27.823,13.912c-9.63,4.814-20.964,4.814-30.594,0L101.733,311.4
c-5.115-2.557-8.612-7.501-9.322-13.175l-5.194-41.55c-0.612-4.898,3.011-9.269,7.935-9.591
c15.938-1.043,49.178-5.044,69.669-20.388c3.456-2.588,8.28-2.132,11.233,1.018l23.773,25.354c1.739,1.855,2.562,4.387,2.247,6.909
l-4.781,38.247C196.586,303.898,193.087,308.843,187.973,311.4z"/>
<path style="fill:#E6AF78;" d="M199.829,253.068l-23.774-25.355c-2.974-3.171-7.815-3.569-11.298-0.968
c-10.381,7.751-24.006,12.583-37.006,15.618v0.001c-12.713,2.967-24.825,4.214-32.68,4.726c-4.954,0.323-8.469,4.661-7.853,9.586
l5.193,41.548c0.71,5.675,4.207,10.619,9.323,13.177l27.823,13.912c4.155,2.077,8.639,3.078,13.155,3.364l-10.446-20.892
c-2.968-5.936-4.514-12.481-4.514-19.118v-13.239c0-4.003,2.779-7.391,6.673-8.321c10.865-2.595,22.152-6.299,32.494-11.632
l14.197,15.142c4.765,5.083,11.825,9.027,18.599,8.257l2.362-18.895C202.392,257.455,201.568,254.923,199.829,253.068z"/>
<path style="fill:#5B5D6E;" d="M196.16,170.489H93.546l-6.545,26.18c-1.118,4.474,1.512,9.029,5.946,10.296l47.207,13.487
c3.071,0.878,6.326,0.878,9.397,0l47.207-13.487c4.435-1.267,7.065-5.822,5.946-10.296L196.16,170.489z"/>
<path style="fill:#464655;" d="M93.546,170.489L87,196.669c-1.118,4.474,1.512,9.029,5.946,10.296l47.208,13.488
c1.536,0.438,3.117,0.658,4.699,0.658v-50.623H93.546z"/>
<path style="fill:#707487;" d="M138.103,137.222l-76.489,21.652c-3.006,0.851-3.006,5.276,0,6.127l76.489,21.652
c4.419,1.251,9.08,1.251,13.5,0l76.489-21.652c3.006-0.851,3.006-5.276,0-6.127l-76.489-21.652
C147.183,135.972,142.522,135.972,138.103,137.222z"/>
<path style="fill:#D5DCED;" d="M187.591,478.33h-85.511c-4.722,0-8.551-3.829-8.551-8.551v-51.307c0-4.722,3.829-8.551,8.551-8.551
h85.511c4.722,0,8.551,3.829,8.551,8.551v51.307C196.142,474.501,192.313,478.33,187.591,478.33z"/>
<path d="M486.881,25.653H161.937c-13.851,0-25.119,11.268-25.119,25.119v78.507c-0.311,0.078-0.624,0.142-0.934,0.23l-76.489,21.65
c-4.838,1.369-8.088,5.7-8.088,10.778c0,5.077,3.251,9.408,8.087,10.778l25.379,7.184l-4.595,13.787
c-1.134,3.403-1.098,7.094,0.021,10.47c-7.739,11.356-11.808,24.387-11.808,38.007c0,14.823,4.845,28.938,14.019,40.973
l2.011,16.084c1.044,8.352,6.162,15.588,13.691,19.351l13.035,6.517v12.648c0,3.917-2.496,7.381-6.212,8.618l-27.92,9.307
c-10.273,3.425-17.175,13.001-17.175,23.83v90.823H8.017c-4.427,0-8.017,3.589-8.017,8.017s3.589,8.017,8.017,8.017h273.637
c4.427,0,8.017-3.589,8.017-8.017s-3.589-8.017-8.017-8.017h-51.859v-90.822c0-10.829-6.903-20.406-17.176-23.83l-27.918-9.307
c-3.715-1.239-6.212-4.702-6.212-8.618v-5.31h308.394c13.851,0,25.119-11.268,25.119-25.119V50.772
C512,36.922,500.732,25.653,486.881,25.653z M140.251,144.936C140.252,144.936,140.252,144.936,140.251,144.936
c2.989-0.845,6.146-0.845,9.133,0l60.062,17.002l-60.061,17.002c-2.988,0.846-6.146,0.846-9.133,0l-60.062-17.002L140.251,144.936z
M144.817,359.148c-7.962,0-15.622-2.746-21.748-7.662c2.613-3.992,4.111-8.732,4.111-13.75v-4.676
c5.572,2.56,11.602,3.857,17.637,3.857s12.065-1.297,17.637-3.857v4.676c0,5.017,1.496,9.757,4.11,13.749
C160.442,356.403,152.796,359.148,144.817,359.148z M84.496,239.589c0.434-8.676,3.059-17.005,7.706-24.49l50.413,14.404
c0.032,0.01,0.066,0.012,0.098,0.021c-25.228,9.725-56.273,9.91-56.686,9.91C85.509,239.434,84.997,239.492,84.496,239.589z
M197.433,215.1c5.104,8.223,7.776,17.463,7.776,27.062c0,1.583-0.081,3.156-0.228,4.719l-25.099-26.767L197.433,215.1z
M194.246,198.755c0.013,0.038,0.024,0.125,0.012,0.23c-0.154,0.151-0.295,0.311-0.434,0.471l-40.99,11.711v-16.575
c0.306-0.077,0.614-0.14,0.917-0.226l35.666-10.096L194.246,198.755z M135.885,194.367c0.304,0.086,0.61,0.149,0.915,0.226v16.575
l-40.967-11.704c-0.148-0.171-0.298-0.339-0.462-0.498c-0.006-0.095,0.005-0.173,0.017-0.208l4.828-14.485L135.885,194.367z
M100.33,297.23l-5.261-42.096c16.635-1.028,52.262-5.215,74.813-22.248l24.236,25.848l-4.811,38.496
c-0.378,3.022-2.229,5.637-4.953,7l0,0l-27.823,13.912c-7.335,3.666-16.09,3.665-23.424,0l-27.823-13.912
C102.559,302.868,100.708,300.251,100.33,297.23z M75.874,379.491c0-3.918,2.496-7.381,6.212-8.62l27.92-9.307
c0.056-0.018,0.108-0.043,0.162-0.062c7.402,6.91,16.685,11.446,26.636,13.038l0.011,27.362h-34.736
c-9.136,0-16.568,7.432-16.568,16.568v51.307c0,0.181,0.021,0.356,0.027,0.534h-9.665v-90.821H75.874z M102.079,470.313
c-0.295,0-0.534-0.239-0.534-0.534v-51.307c0-0.295,0.239-0.534,0.534-0.534h42.756c0.001,0,0.002,0,0.003,0h42.753
c0.295,0,0.534,0.239,0.534,0.534v51.307c0,0.295-0.239,0.534-0.534,0.534H102.079z M213.761,379.491v90.822h-9.63
c0.005-0.178,0.027-0.354,0.027-0.534v-51.307c0-9.136-7.432-16.568-16.568-16.568h-34.742l-0.011-27.364
c9.961-1.591,19.239-6.114,26.641-13.032c0.051,0.017,0.099,0.04,0.151,0.057l27.918,9.307
C211.265,372.111,213.761,375.575,213.761,379.491z M495.967,307.307c0,5.01-4.076,9.086-9.086,9.086H195.086
c5.558-4.047,9.26-10.219,10.129-17.174l0.125-0.997h264.439c4.427,0,8.017-3.589,8.017-8.017c0-4.427-3.589-8.017-8.017-8.017
h-9.086V67.875c0-4.427-3.589-8.017-8.017-8.017h-42.756c-4.427,0-8.017,3.589-8.017,8.017v214.313h-18.171V136.284
c0-4.427-3.589-8.017-8.017-8.017H332.96c-4.427,0-8.017,3.589-8.017,8.017v145.904h-18.171v-77.495
c0-4.427-3.589-8.017-8.017-8.017H256c-4.427,0-8.017,3.589-8.017,8.017v77.495H207.93c8.708-11.832,13.313-25.63,13.313-40.026
c0-13.62-4.068-26.651-11.808-38.007c1.119-3.376,1.155-7.066,0.021-10.47l-4.596-13.787l17.451-4.94v38.286
c0,4.427,3.589,8.017,8.017,8.017s8.017-3.589,8.017-8.017v-51.307c0-0.305-0.02-0.605-0.053-0.901
c-0.359-4.683-3.503-8.589-8.052-9.877l-76.489-21.652c-0.298-0.084-0.6-0.145-0.899-0.221V50.772c0-5.01,4.076-9.086,9.086-9.086
H486.88c5.01,0,9.086,4.076,9.086,9.086v256.534H495.967z M290.739,282.188h-26.722V212.71h26.722V282.188z M367.699,282.188
h-26.722V144.301h26.722V282.188z M444.66,282.188h-26.722V75.891h26.722V282.188z"/>
<path d="M144.878,436.109h-0.086c-4.427,0-7.974,3.589-7.974,8.017c0,4.427,3.632,8.017,8.059,8.017s8.017-3.589,8.017-8.017
C152.895,439.698,149.305,436.109,144.878,436.109z"/>
</svg>

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

7
public/images/error.svg Normal file
View File

@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg fill="#878787" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve" stroke="#878787">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <path d="M256,0C114.6,0,0,114.6,0,256s114.6,256,256,256s256-114.6,256-256S397.4,0,256,0z M64,256c0-106.1,86-192,192-192 c42.1,0,81,13.7,112.6,36.7L100.7,368.6C77.7,337,64,298.1,64,256z M256,448c-42.1,0-81-13.7-112.6-36.7l267.9-267.9 c23,31.7,36.7,70.5,36.7,112.6C448,362.1,362,448,256,448z"/> </g>
</svg>

After

Width:  |  Height:  |  Size: 898 B

35
public/images/front.svg Normal file
View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<rect x="11.77" y="11.77" style="fill:#ECF0F1;" width="488.46" height="488.46"/>
<rect x="11.77" y="11.77" style="fill:#9E9E9E;" width="488.46" height="76.506"/>
<rect x="423.724" y="11.77" style="fill:#B71C1C;" width="76.506" height="76.506"/>
<rect x="284.86" y="296.054" style="fill:#FFC107;" width="159.073" height="141.241"/>
<rect x="141.241" y="138.923" style="fill:#2196F3;" width="229.517" height="52.966"/>
<g>
<path style="fill:#231F20;" d="M500.23,0H11.77C5.269,0,0,5.269,0,11.77v488.46C0,506.731,5.269,512,11.77,512h488.46
c6.501,0,11.77-5.269,11.77-11.77V11.77C512,5.269,506.731,0,500.23,0z M488.46,23.54v52.966h-52.966V23.54H488.46z M23.54,23.54
h388.414v52.966H23.54V23.54z M23.54,488.46V100.046h464.92V488.46H23.54z"/>
<path style="fill:#231F20;" d="M443.93,237.208H68.07c-6.501,0-11.77,5.269-11.77,11.77c0,6.501,5.269,11.77,11.77,11.77h375.859
c6.501,0,11.77-5.269,11.77-11.77C455.699,242.477,450.431,237.208,443.93,237.208z"/>
<path style="fill:#231F20;" d="M233.073,284.288H110.566c-6.501,0-11.77,5.269-11.77,11.77s5.269,11.77,11.77,11.77h122.508
c6.501,0,11.77-5.269,11.77-11.77S239.574,284.288,233.073,284.288z"/>
<path style="fill:#231F20;" d="M68.07,307.829h2.183c6.501,0,11.77-5.269,11.77-11.77s-5.269-11.77-11.77-11.77H68.07
c-6.501,0-11.77,5.269-11.77,11.77S61.571,307.829,68.07,307.829z"/>
<path style="fill:#231F20;" d="M233.073,331.369H68.07c-6.501,0-11.77,5.269-11.77,11.77s5.269,11.77,11.77,11.77h165.003
c6.501,0,11.77-5.269,11.77-11.77S239.574,331.369,233.073,331.369z"/>
<path style="fill:#231F20;" d="M233.073,378.449h-1.275c-6.501,0-11.77,5.269-11.77,11.77s5.269,11.77,11.77,11.77h1.275
c6.501,0,11.77-5.269,11.77-11.77S239.574,378.449,233.073,378.449z"/>
<path style="fill:#231F20;" d="M68.07,401.989h126.357c6.501,0,11.77-5.269,11.77-11.77s-5.269-11.77-11.77-11.77H68.07
c-6.501,0-11.77,5.269-11.77,11.77S61.571,401.989,68.07,401.989z"/>
<path style="fill:#231F20;" d="M233.073,425.53H68.07c-6.501,0-11.77,5.269-11.77,11.77s5.269,11.77,11.77,11.77h165.003
c6.501,0,11.77-5.269,11.77-11.77S239.574,425.53,233.073,425.53z"/>
<path style="fill:#231F20;" d="M443.93,284.288H284.861c-6.501,0-11.77,5.269-11.77,11.77V437.3c0,6.501,5.27,11.77,11.77,11.77
H443.93c6.501,0,11.77-5.269,11.77-11.77V296.058C455.7,289.558,450.431,284.288,443.93,284.288z M432.16,425.53H296.632V307.829
H432.16V425.53z"/>
<path style="fill:#231F20;" d="M141.241,203.663h229.517c6.501,0,11.77-5.27,11.77-11.77v-52.966c0-6.501-5.269-11.77-11.77-11.77
H141.241c-6.501,0-11.77,5.269-11.77,11.77v52.966C129.471,198.394,134.741,203.663,141.241,203.663z M153.011,150.697h205.977
v29.425H153.011V150.697z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

19
public/images/github.svg Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>github [#142]</title>
<desc>Created with Sketch.</desc>
<defs>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Dribbble-Light-Preview" transform="translate(-140.000000, -7559.000000)" fill="#000000">
<g id="icons" transform="translate(56.000000, 160.000000)">
<path d="M94,7399 C99.523,7399 104,7403.59 104,7409.253 C104,7413.782 101.138,7417.624 97.167,7418.981 C96.66,7419.082 96.48,7418.762 96.48,7418.489 C96.48,7418.151 96.492,7417.047 96.492,7415.675 C96.492,7414.719 96.172,7414.095 95.813,7413.777 C98.04,7413.523 100.38,7412.656 100.38,7408.718 C100.38,7407.598 99.992,7406.684 99.35,7405.966 C99.454,7405.707 99.797,7404.664 99.252,7403.252 C99.252,7403.252 98.414,7402.977 96.505,7404.303 C95.706,7404.076 94.85,7403.962 94,7403.958 C93.15,7403.962 92.295,7404.076 91.497,7404.303 C89.586,7402.977 88.746,7403.252 88.746,7403.252 C88.203,7404.664 88.546,7405.707 88.649,7405.966 C88.01,7406.684 87.619,7407.598 87.619,7408.718 C87.619,7412.646 89.954,7413.526 92.175,7413.785 C91.889,7414.041 91.63,7414.493 91.54,7415.156 C90.97,7415.418 89.522,7415.871 88.63,7414.304 C88.63,7414.304 88.101,7413.319 87.097,7413.247 C87.097,7413.247 86.122,7413.234 87.029,7413.87 C87.029,7413.87 87.684,7414.185 88.139,7415.37 C88.139,7415.37 88.726,7417.2 91.508,7416.58 C91.513,7417.437 91.522,7418.245 91.522,7418.489 C91.522,7418.76 91.338,7419.077 90.839,7418.982 C86.865,7417.627 84,7413.783 84,7409.253 C84,7403.59 88.478,7399 94,7399" id="github-[#142]">
</path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--noto" preserveAspectRatio="xMidYMid meet">
<path d="M12.75 118.55c6.65 6.65 14.73 4.34 19.4-.98c4.77-5.44 7.31-8.6 7.31-8.6c5.95-8.76 30.53-38.04 51.51-59.11c2.92 1.2 6.02 1.83 9.13 1.83c5.54 0 11.09-1.91 15.53-5.74a23.83 23.83 0 0 0 8.3-18.36c-.01-.49-.27-.95-.7-1.19c-.43-.24-.95-.24-1.38 0l-14.45 8.34c-6.98-2.41-11.11-9.59-9.69-16.77l14.4-8.31c.43-.25.69-.7.69-1.2s-.26-.95-.69-1.2c-9.35-5.47-21.23-3.93-28.89 3.73a23.915 23.915 0 0 0-6.97 17.61a23.66 23.66 0 0 0 2.92 10.72C55.99 62.43 27.71 87.42 20.35 93c0 0-4.43 3.21-8.66 7.56c-4.17 4.29-5.72 11.21 1.06 17.99zm4.87-10.23c0-2.94 2.38-5.33 5.33-5.33s5.33 2.38 5.33 5.33c0 2.94-2.38 5.33-5.33 5.33s-5.33-2.39-5.33-5.33z" fill="#82aec0">
</path>
<path d="M76 42.47c1.04-1.03 2.1-2.07 3.18-3.15a23.66 23.66 0 0 1-2.92-10.72c-.08-2.6.27-5.17 1.01-7.62c.66-1.08 1.7-2.24 2.36-1.54c-.27 8.51 2.2 17.38 8.12 23.5c2.94 3.04 6.76 5.23 10.86 6.26c2.02.51 4.12.74 6.21.62c1.23-.07 4.67-1.34 5.36-.4v.04a23.902 23.902 0 0 1-10.08 2.22c-3.11 0-6.22-.63-9.13-1.83c-6.79 6.82-12.41 12.96-17.13 18.45c2.36-4.46 9.31-12.68 11.17-15.82c.42-.71 1-2.53-.32-4.17c-1.87-2.3-5.7-4.44-8.69-5.84z" fill="#2f7889">
</path>
<path d="M47.68 77.75c.54-.56.03-1.5-.73-1.33c-1.42.31-3.47 1.12-5.91 3.1c-4.78 3.88-17.4 14.36-18.96 16.43s4.7-.03 6.9-.42c1.81-.33 14.42-13.33 18.7-17.78z" fill="#b9e4ea">
</path>
<path d="M91.43 10.93c-2.67 1.91-5.07 4.18-7.57 6.31c-.64.55-1.59 1.1-2.25.58c-.72-.57-.27-1.72.27-2.46c4.73-6.5 13.56-11.15 22.1-9.3c-4.5 1.45-8.55 2.01-12.55 4.87z" fill="#b9e4ea">
</path>
<path d="M112.66 33.24c-1.05.63-2.51 1.47-3.34 1.71c-1.01.28-3.87-.83-4.7-1.49c3.42-2.23 7.23-4.72 10.65-6.95c.39-.25.78-.51 1.23-.65c.53-.17 1.11-.15 1.67-.13c.78.04 4.59-.09 5.04.58c.45.68-1.13 1.34-1.65 1.65c-2.97 1.77-5.94 3.52-8.9 5.28z" fill="#2f7889">
</path>
<path d="M71.8 70.17l-11.19-12.9c-4.05 3.81-8.06 7.52-11.89 11.03l3.26-.71c1.43-.3 2.91.14 3.95 1.17l5.22 5.85c1.1 1.1 1.28 3.01.86 4.51l-.58 2.77c3.3-3.81 6.79-7.76 10.37-11.72z" fill="#2f7889">
</path>
<g>
<path d="M121.39 116.71l-6.7 5.79c-2.58 2.23-6.5 1.93-8.71-.65L26.57 26l10.21-7.68l85.12 89.77a6.01 6.01 0 0 1-.51 8.62z" fill="#a06841">
</path>
<path d="M33.83 34.76l18.9 22.82c2.95-4.44 4.45-9.76 5.66-16.48L40.14 21.85a16.546 16.546 0 0 0-3.73 4.31c-1.55 2.61-2.36 5.57-2.58 8.6z" fill="#7d5133">
</path>
<path d="M71.29 4.94c-17.34-.2-23.76 1.34-33.42 9.69c-2.9 2.5-5.79 5-8.69 7.51c-3.15 2.72-7.34 5.1-6.68 9.8c.24 1.72.77 3.46.45 5.16c-.31 1.61-2.18 2.41-3.51 1.49c-1.25-.86-2.63-1.92-4.17-2.1c-1.44-.16-2.96.29-4.05 1.26L4.5 43.72s-.96 3.91 6.56 12.42s12.36 7.9 12.36 7.9l6.32-5.56c1.06-.93 1.61-2.3 1.58-3.71c-.03-1.65-.99-2.93-1.57-4.41c-.11-.28-.74-1.28.36-2.19c.98-.85 3-.56 4.15-.25c1.15.31 2.25.8 3.41 1.1c2.26.59 3.32-.46 4.89-1.81c1.39-1.2 9.76-8.43 12.55-10.85c5.57-4.82-2.92-13.26-2.92-13.26c-4-4.53 20.27-15.92 20.27-15.92c1.78-.62 1.24-2.22-1.17-2.24z" fill="#82aec0">
</path>
<path d="M37.68 49.03c.47.12.88.16 1.26.15v-.19c-.1-1.08-.69-2.06-1.29-2.97A64.622 64.622 0 0 0 23.9 30.96c-.44-.35-.9-.7-1.38-1c-.1.61-.12 1.27-.02 1.98c.24 1.72.77 3.46.45 5.16c-.34 1.76-2.18 2.25-3.59 1.59c3.67 2.37 6.81 5.53 9.1 9.25c.31.5.62 1.03.98 1.51c.05-.49.27-.96.68-1.27c.98-.85 3-.56 4.15-.25c1.16.3 2.25.79 3.41 1.1z" fill="#2f7889">
</path>
<path d="M17 51.15c5.27 5.51 8.23 11.22 6.61 12.77c-1.61 1.54-7.19-1.67-12.46-7.17S2.89 45.27 4.5 43.72c1.61-1.54 7.23 1.92 12.5 7.43z" fill="#2f7889">
</path>
<path d="M37.51 22.68c4.19-1.78 7.92-5.6 12.8-9.81c1.39-1.2 3.16-2.34 4.97-3.2c.68-.32.43-1.34-.32-1.33c-2.51.04-4.75.8-6.95 1.76c-3.08 1.34-5.8 3.37-8.42 5.47c-1.8 1.44-6.02 5-8.68 7.25c-.5.42-.11 1.08.54 1.08c1.87.02 2.92.12 6.06-1.22z" fill="#b9e4ea">
</path>
<path d="M11.12 40.16c-1.77 1.99.49 2.53 4.46 5.81c2.8 2.32 5.78.17 5.81-2c.02-1.95-.47-3-3.3-4.78s-5.37-.83-6.97.97z" fill="#b9e4ea">
</path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>javascript [#155]</title>
<desc>Created with Sketch.</desc>
<defs>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Dribbble-Light-Preview" transform="translate(-420.000000, -7479.000000)" fill="#000000">
<g id="icons" transform="translate(56.000000, 160.000000)">
<path d="M379.328,7337.432 C377.583,7337.432 376.455,7336.6 375.905,7335.512 L375.905,7335.512 L377.435,7334.626 C377.838,7335.284 378.361,7335.767 379.288,7335.767 C380.066,7335.767 380.563,7335.378 380.563,7334.841 C380.563,7334.033 379.485,7333.717 378.724,7333.391 C377.368,7332.814 376.468,7332.089 376.468,7330.558 C376.468,7329.149 377.542,7328.075 379.221,7328.075 C380.415,7328.075 381.275,7328.491 381.892,7329.578 L380.429,7330.518 C380.107,7329.941 379.758,7329.713 379.221,7329.713 C378.67,7329.713 378.321,7330.062 378.321,7330.518 C378.321,7331.082 378.67,7331.31 379.476,7331.659 C381.165,7332.383 382.443,7332.952 382.443,7334.814 C382.443,7336.506 381.114,7337.432 379.328,7337.432 L379.328,7337.432 Z M375,7334.599 C375,7336.546 373.801,7337.575 372.136,7337.575 C370.632,7337.575 369.731,7337 369.288,7336 L369.273,7336 L369.266,7336 L369.262,7336 L370.791,7334.931 C371.086,7335.454 371.352,7335.825 371.996,7335.825 C372.614,7335.825 373,7335.512 373,7334.573 L373,7328 L375,7328 L375,7334.599 Z M364,7339 L384,7339 L384,7319 L364,7319 L364,7339 Z" id="javascript-[#155]">
</path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="2500" height="2500" viewBox="0 0 1052 1052"><path fill="#f0db4f" d="M0 0h1052v1052H0z"/><path d="M965.9 801.1c-7.7-48-39-88.3-131.7-125.9-32.2-14.8-68.1-25.399-78.8-49.8-3.8-14.2-4.3-22.2-1.9-30.8 6.9-27.9 40.2-36.6 66.6-28.6 17 5.7 33.1 18.801 42.8 39.7 45.4-29.399 45.3-29.2 77-49.399-11.6-18-17.8-26.301-25.4-34-27.3-30.5-64.5-46.2-124-45-10.3 1.3-20.699 2.699-31 4-29.699 7.5-58 23.1-74.6 44-49.8 56.5-35.6 155.399 25 196.1 59.7 44.8 147.4 55 158.6 96.9 10.9 51.3-37.699 67.899-86 62-35.6-7.4-55.399-25.5-76.8-58.4-39.399 22.8-39.399 22.8-79.899 46.1 9.6 21 19.699 30.5 35.8 48.7 76.2 77.3 266.899 73.5 301.1-43.5 1.399-4.001 10.6-30.801 3.199-72.101zm-394-317.6h-98.4c0 85-.399 169.4-.399 254.4 0 54.1 2.8 103.7-6 118.9-14.4 29.899-51.7 26.2-68.7 20.399-17.3-8.5-26.1-20.6-36.3-37.699-2.8-4.9-4.9-8.7-5.601-9-26.699 16.3-53.3 32.699-80 49 13.301 27.3 32.9 51 58 66.399 37.5 22.5 87.9 29.4 140.601 17.3 34.3-10 63.899-30.699 79.399-62.199 22.4-41.3 17.6-91.3 17.4-146.6.5-90.2 0-180.4 0-270.9z" fill="#323330"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
public/images/library.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

23
public/images/library.svg Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512.004 512.004" xml:space="preserve">
<rect x="39.151" style="fill:#00DDC0;" width="113.52" height="512.004"/>
<rect x="95.911" style="fill:#00AC93;" width="56.76" height="512.004"/>
<rect x="152.671" style="fill:#FFAD1D;" width="113.52" height="512.004"/>
<rect x="209.442" style="fill:#FF8900;" width="56.76" height="512.004"/>
<rect x="39.151" y="437.501" style="fill:#006659;" width="113.52" height="74.493"/>
<rect x="95.911" y="437.501" style="fill:#005349;" width="56.76" height="74.493"/>
<rect x="152.671" y="437.501" style="fill:#FF4F18;" width="113.52" height="74.493"/>
<rect x="209.442" y="437.501" style="fill:#FF3400;" width="56.76" height="74.493"/>
<rect x="315.22" y="9.326" transform="matrix(-0.9806 0.1962 -0.1962 -0.9806 783.7764 437.2287)" style="fill:#00A5FF;" width="110.026" height="496.216"/>
<rect x="369.697" y="3.927" transform="matrix(-0.9806 0.1962 -0.1962 -0.9806 836.1343 421.2428)" style="fill:#0082D2;" width="55.013" height="496.216"/>
<polygon style="fill:#006DF3;" points="472.845,489.894 364.963,511.484 350.333,438.373 458.261,417.012 "/>
<polygon style="fill:#005FD1;" points="472.845,489.894 418.905,500.689 404.297,427.692 458.261,417.012 "/>
<rect x="73.978" y="51.2" style="fill:#FFFFFF;" width="44.522" height="33.391"/>
<rect x="96.236" y="51.2" style="fill:#E1E1E4;" width="22.261" height="33.391"/>
<rect x="187.509" y="51.2" style="fill:#FFFFFF;" width="44.522" height="33.391"/>
<rect x="209.442" y="51.2" style="fill:#E1E1E4;" width="22.594" height="33.391"/>
<rect x="318.886" y="59.425" transform="matrix(-0.1896 -0.9819 0.9819 -0.1896 319.6778 425.8492)" style="fill:#FFFFFF;" width="33.392" height="43.145"/>
<polygon style="fill:#E1E1E4;" points="338.215,97.479 331.662,64.735 353.595,60.502 359.925,93.287 "/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="24" cy="24" r="20" fill="#0077B5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.7747 14.2839C18.7747 15.529 17.8267 16.5366 16.3442 16.5366C14.9194 16.5366 13.9713 15.529 14.0007 14.2839C13.9713 12.9783 14.9193 12 16.3726 12C17.8267 12 18.7463 12.9783 18.7747 14.2839ZM14.1199 32.8191V18.3162H18.6271V32.8181H14.1199V32.8191Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.2393 22.9446C22.2393 21.1357 22.1797 19.5935 22.1201 18.3182H26.0351L26.2432 20.305H26.3322C26.9254 19.3854 28.4079 17.9927 30.8101 17.9927C33.7752 17.9927 35.9995 19.9502 35.9995 24.219V32.821H31.4922V24.7838C31.4922 22.9144 30.8404 21.6399 29.2093 21.6399C27.9633 21.6399 27.2224 22.4999 26.9263 23.3297C26.8071 23.6268 26.7484 24.0412 26.7484 24.4574V32.821H22.2411V22.9446H22.2393Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

21
public/images/react.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.1 KiB

8
public/images/share.svg Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 12C9 13.3807 7.88071 14.5 6.5 14.5C5.11929 14.5 4 13.3807 4 12C4 10.6193 5.11929 9.5 6.5 9.5C7.88071 9.5 9 10.6193 9 12Z" stroke="#1C274C" stroke-width="1.5"/>
<path d="M14 6.5L9 10" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
<path d="M14 17.5L9 14" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
<path d="M19 18.5C19 19.8807 17.8807 21 16.5 21C15.1193 21 14 19.8807 14 18.5C14 17.1193 15.1193 16 16.5 16C17.8807 16 19 17.1193 19 18.5Z" stroke="#1C274C" stroke-width="1.5"/>
<path d="M19 5.5C19 6.88071 17.8807 8 16.5 8C15.1193 8 14 6.88071 14 5.5C14 4.11929 15.1193 3 16.5 3C17.8807 3 19 4.11929 19 5.5Z" stroke="#1C274C" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 922 B

View File

@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" d="M13.803 5.33333C13.803 3.49238 15.3022 2 17.1515 2C19.0008 2 20.5 3.49238 20.5 5.33333C20.5 7.17428 19.0008 8.66667 17.1515 8.66667C16.2177 8.66667 15.3738 8.28596 14.7671 7.67347L10.1317 10.8295C10.1745 11.0425 10.197 11.2625 10.197 11.4872C10.197 11.9322 10.109 12.3576 9.94959 12.7464L15.0323 16.0858C15.6092 15.6161 16.3473 15.3333 17.1515 15.3333C19.0008 15.3333 20.5 16.8257 20.5 18.6667C20.5 20.5076 19.0008 22 17.1515 22C15.3022 22 13.803 20.5076 13.803 18.6667C13.803 18.1845 13.9062 17.7255 14.0917 17.3111L9.05007 13.9987C8.46196 14.5098 7.6916 14.8205 6.84848 14.8205C4.99917 14.8205 3.5 13.3281 3.5 11.4872C3.5 9.64623 4.99917 8.15385 6.84848 8.15385C7.9119 8.15385 8.85853 8.64725 9.47145 9.41518L13.9639 6.35642C13.8594 6.03359 13.803 5.6896 13.803 5.33333Z" fill="#000000"/> </g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/images/tutorials.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

106
public/images/tutorials.svg Normal file
View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<circle style="fill:#8AD5DD;" cx="256" cy="256" r="256"/>
<rect id="SVGCleanerId_0" x="115" y="68" style="fill:#FFFFFF;" width="282" height="376"/>
<rect id="SVGCleanerId_1" x="146.336" y="339.8" style="fill:#2D2D2D;" width="97.696" height="8"/>
<rect id="SVGCleanerId_2" x="146.336" y="371.12" style="fill:#E0E0E0;" width="97.696" height="8"/>
<rect id="SVGCleanerId_3" x="146.336" y="386.8" style="fill:#E0E0E0;" width="97.696" height="8"/>
<rect id="SVGCleanerId_4" x="146.336" y="402.48" style="fill:#E0E0E0;" width="97.696" height="8"/>
<path id="SVGCleanerId_5" style="fill:#DB2B42;" d="M355.872,316.384H156.128c-6.464,0-11.752-5.288-11.752-11.752v-144.92
c0-6.464,5.288-11.752,11.752-11.752H355.88c6.464,0,11.752,5.288,11.752,11.752v144.92
C367.624,311.096,362.336,316.384,355.872,316.384z"/>
<g>
<rect id="SVGCleanerId_0_1_" x="115" y="68" style="fill:#FFFFFF;" width="282" height="376"/>
</g>
<g>
<rect id="SVGCleanerId_1_1_" x="146.336" y="339.8" style="fill:#2D2D2D;" width="97.696" height="8"/>
</g>
<g>
<rect id="SVGCleanerId_2_1_" x="146.336" y="371.12" style="fill:#E0E0E0;" width="97.696" height="8"/>
</g>
<g>
<rect id="SVGCleanerId_3_1_" x="146.336" y="386.8" style="fill:#E0E0E0;" width="97.696" height="8"/>
</g>
<g>
<rect id="SVGCleanerId_4_1_" x="146.336" y="402.48" style="fill:#E0E0E0;" width="97.696" height="8"/>
</g>
<g>
<path id="SVGCleanerId_5_1_" style="fill:#DB2B42;" d="M355.872,316.384H156.128c-6.464,0-11.752-5.288-11.752-11.752v-144.92
c0-6.464,5.288-11.752,11.752-11.752H355.88c6.464,0,11.752,5.288,11.752,11.752v144.92
C367.624,311.096,362.336,316.384,355.872,316.384z"/>
</g>
<polygon style="fill:#FFFFFF;" points="232.496,191.472 303,232.176 232.496,272.88 "/>
<g>
<rect x="269.928" y="339.04" style="fill:#E0E0E0;" width="97.696" height="19.016"/>
<rect x="269.928" y="365.624" style="fill:#E0E0E0;" width="97.696" height="19.016"/>
<rect x="269.928" y="392.24" style="fill:#E0E0E0;" width="97.696" height="19.016"/>
</g>
<g>
<rect x="269.928" y="392.24" style="fill:#2D2D2D;" width="20.304" height="19.016"/>
<rect x="269.928" y="365.624" style="fill:#2D2D2D;" width="20.304" height="19.016"/>
<rect x="269.928" y="339.04" style="fill:#2D2D2D;" width="20.304" height="19.016"/>
</g>
<g>
<path style="fill:#E0E0E0;" d="M365.664,100.072v19.616h-135.24v-19.616H365.664 M367.624,98.112h-139.16v23.536h139.16
L367.624,98.112L367.624,98.112z"/>
<rect x="341.312" y="98.112" style="fill:#E0E0E0;" width="26.312" height="23.536"/>
</g>
<path style="fill:#DB2B42;" d="M207.656,121.648H173.2c-1.312,0-2.384-1.072-2.384-2.384v-18.768c0-1.312,1.072-2.384,2.384-2.384
h34.456c1.312,0,2.384,1.072,2.384,2.384v18.768C210.04,120.576,208.96,121.648,207.656,121.648z"/>
<g>
<path style="fill:#FFFFFF;" d="M178.008,107.264h-2.784v-1.488h7.384v1.488h-2.816v8.28h-1.784L178.008,107.264L178.008,107.264z"
/>
<path style="fill:#FFFFFF;" d="M189.28,113.424c0,0.84,0.032,1.544,0.056,2.12h-1.568l-0.088-1.08h-0.024
c-0.304,0.504-1,1.232-2.264,1.232c-1.288,0-2.464-0.768-2.464-3.072v-4.144h1.784v3.84c0,1.176,0.384,1.936,1.32,1.936
c0.712,0,1.176-0.504,1.36-0.96c0.064-0.16,0.104-0.344,0.104-0.552v-4.264h1.784V113.424z"/>
<path style="fill:#FFFFFF;" d="M191.144,115.544c0.032-0.48,0.064-1.264,0.064-1.984v-8.312h1.784v4.2h0.032
c0.432-0.672,1.2-1.128,2.256-1.128c1.728,0,2.96,1.432,2.944,3.584c0,2.536-1.608,3.8-3.208,3.8c-0.912,0-1.728-0.352-2.232-1.216
h-0.032l-0.088,1.056H191.144z M192.992,112.632c0,0.152,0.016,0.288,0.04,0.424c0.192,0.712,0.808,1.256,1.584,1.256
c1.12,0,1.8-0.904,1.8-2.32c0-1.256-0.592-2.264-1.784-2.264c-0.72,0-1.376,0.528-1.584,1.304
c-0.024,0.128-0.056,0.288-0.056,0.464V112.632z"/>
<path style="fill:#FFFFFF;" d="M200.952,112.512c0.04,1.28,1.04,1.832,2.176,1.832c0.824,0,1.416-0.12,1.96-0.32l0.264,1.232
c-0.616,0.248-1.456,0.44-2.472,0.44c-2.288,0-3.64-1.408-3.64-3.576c0-1.952,1.192-3.8,3.448-3.8c2.296,0,3.048,1.888,3.048,3.44
c0,0.336-0.032,0.592-0.056,0.752C205.68,112.512,200.952,112.512,200.952,112.512z M204.056,111.264
c0.016-0.648-0.272-1.728-1.464-1.728c-1.104,0-1.568,1.008-1.64,1.728H204.056z"/>
</g>
<g>
<path style="fill:#2D2D2D;" d="M147.464,115.544v-4.072l-3.088-5.696h2.032l1.176,2.504c0.336,0.728,0.584,1.28,0.848,1.944h0.024
c0.248-0.624,0.52-1.232,0.856-1.944l1.176-2.504h2.016l-3.248,5.656v4.112H147.464z"/>
<path style="fill:#2D2D2D;" d="M158.864,111.944c0,2.6-1.824,3.76-3.624,3.76c-2,0-3.536-1.376-3.536-3.632
c0-2.328,1.52-3.744,3.656-3.744C157.464,108.32,158.864,109.8,158.864,111.944z M153.544,112.024c0,1.36,0.664,2.392,1.752,2.392
c1.016,0,1.728-1,1.728-2.424c0-1.104-0.496-2.368-1.712-2.368C154.056,109.624,153.544,110.84,153.544,112.024z"/>
<path style="fill:#2D2D2D;" d="M166.656,113.424c0,0.84,0.032,1.544,0.056,2.12h-1.568l-0.088-1.08h-0.024
c-0.304,0.504-1,1.232-2.264,1.232c-1.288,0-2.464-0.768-2.464-3.072v-4.144h1.784v3.84c0,1.176,0.384,1.936,1.32,1.936
c0.712,0,1.176-0.504,1.36-0.96c0.064-0.16,0.104-0.344,0.104-0.552v-4.264h1.784V113.424z"/>
<path style="fill:#2D2D2D;" d="M353.936,113.776c-3.096,0-5.616-2.52-5.616-5.616s2.52-5.616,5.616-5.616s5.616,2.52,5.616,5.616
C359.552,111.256,357.032,113.776,353.936,113.776z M353.936,103.816c-2.392,0-4.344,1.952-4.344,4.344s1.952,4.344,4.344,4.344
s4.344-1.952,4.344-4.344S356.328,103.816,353.936,103.816z"/>
<rect x="356.02" y="113.813" transform="matrix(-0.6044 -0.7967 0.7967 -0.6044 484.7665 469.5985)" style="fill:#2D2D2D;" width="5.896" height="1.272"/>
<path style="fill:#2D2D2D;" d="M240.088,113.168c0.48,0.296,1.184,0.544,1.92,0.544c1.096,0,1.736-0.576,1.736-1.416
c0-0.768-0.44-1.216-1.56-1.648c-1.352-0.48-2.192-1.184-2.192-2.352c0-1.296,1.072-2.248,2.68-2.248
c0.848,0,1.464,0.192,1.832,0.4l-0.296,0.872c-0.272-0.152-0.824-0.392-1.576-0.392c-1.136,0-1.56,0.672-1.56,1.24
c0,0.776,0.504,1.152,1.648,1.6c1.4,0.544,2.12,1.216,2.12,2.432c0,1.28-0.952,2.392-2.904,2.392c-0.8,0-1.68-0.24-2.12-0.528
L240.088,113.168z"/>
<path style="fill:#2D2D2D;" d="M246.984,111.688c0.024,1.472,0.96,2.064,2.048,2.064c0.776,0,1.24-0.128,1.648-0.304l0.184,0.776
c-0.384,0.176-1.032,0.368-1.984,0.368c-1.832,0-2.928-1.208-2.928-3s1.056-3.216,2.792-3.216c1.952,0,2.464,1.704,2.464,2.808
c0,0.216-0.024,0.392-0.04,0.496h-4.184V111.688z M250.16,110.92c0.016-0.688-0.288-1.76-1.504-1.76
c-1.096,0-1.576,1.008-1.664,1.76H250.16z"/>
<path style="fill:#2D2D2D;" d="M255.88,114.472l-0.088-0.752h-0.032c-0.336,0.472-0.968,0.888-1.824,0.888
c-1.208,0-1.824-0.848-1.824-1.704c0-1.448,1.28-2.232,3.584-2.216v-0.128c0-0.488-0.136-1.384-1.36-1.384
c-0.552,0-1.128,0.176-1.552,0.448l-0.248-0.72c0.488-0.312,1.208-0.528,1.96-0.528c1.824,0,2.264,1.24,2.264,2.432v2.232
c0,0.52,0.024,1.024,0.104,1.424h-0.984V114.472z M255.72,111.44c-1.184-0.024-2.528,0.184-2.528,1.336
c0,0.704,0.472,1.032,1.024,1.032c0.776,0,1.272-0.488,1.44-1c0.04-0.112,0.064-0.24,0.064-0.344V111.44z"/>
<path style="fill:#2D2D2D;" d="M258.552,110.376c0-0.704-0.016-1.304-0.056-1.864h0.944l0.04,1.176h0.04
c0.272-0.8,0.928-1.304,1.656-1.304c0.12,0,0.2,0.008,0.304,0.032v1.024c-0.112-0.024-0.224-0.032-0.368-0.032
c-0.768,0-1.312,0.576-1.456,1.392c-0.024,0.144-0.056,0.312-0.056,0.496v3.176h-1.072v-4.096H258.552z"/>
<path style="fill:#2D2D2D;" d="M266.72,114.256c-0.288,0.152-0.912,0.344-1.72,0.344c-1.792,0-2.968-1.216-2.968-3.04
c0-1.84,1.264-3.168,3.208-3.168c0.64,0,1.208,0.168,1.504,0.304l-0.248,0.84c-0.264-0.152-0.664-0.28-1.264-0.28
c-1.368,0-2.104,1.008-2.104,2.248c0,1.384,0.888,2.232,2.064,2.232c0.616,0,1.024-0.168,1.328-0.296L266.72,114.256z"/>
<path style="fill:#2D2D2D;" d="M268.016,105.736h1.08v3.712h0.024c0.176-0.304,0.448-0.576,0.776-0.76
c0.312-0.184,0.704-0.304,1.104-0.304c0.808,0,2.08,0.488,2.08,2.544v3.544H272v-3.424c0-0.96-0.352-1.768-1.376-1.768
c-0.704,0-1.264,0.488-1.456,1.08c-0.064,0.152-0.08,0.304-0.08,0.52v3.6h-1.08v-8.744H268.016z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 73 73" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>build-tools/typescript</title>
<desc>Created with Sketch.</desc>
<defs>
<rect id="path-1" x="4" y="4" width="69" height="69" rx="14">
</rect>
</defs>
<g id="build-tools/typescript" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="container" transform="translate(-2.000000, -2.000000)">
<rect id="mask" stroke="#003355" stroke-width="2" fill="#FFFFFF" fill-rule="nonzero" x="3" y="3" width="71" height="71" rx="14">
</rect>
<mask id="mask-2" fill="white">
<use xlink:href="#path-1">
</use>
</mask>
<rect stroke="#003355" stroke-width="2" x="3" y="3" width="71" height="71" rx="14">
</rect>
<g id="logo" mask="url(#mask-2)" fill="#007ACC" fill-rule="nonzero">
<g id="Group" transform="translate(36.500000, 36.500000) scale(-1, 1) rotate(-180.000000) translate(-36.500000, -36.500000) ">
<path d="M0,36.5 L0,0 L36.5,0 L73,0 L73,36.5 L73,73 L36.5,73 L0,73 L0,36.5 Z M58.8287302,39.4084127 C60.6826984,38.9449206 62.0963492,38.1222222 63.394127,36.7780952 C64.0661905,36.0596825 65.0626984,34.7503175 65.1438095,34.4374603 C65.1669841,34.3447619 61.9920635,32.2126984 60.0685714,31.0192063 C59.9990476,30.9728571 59.7209524,31.274127 59.4080952,31.737619 C58.4695238,33.1049206 57.4846032,33.695873 55.978254,33.8001587 C53.7650794,33.9507937 52.3398413,32.7920635 52.3514286,30.8569841 C52.3514286,30.2892063 52.4325397,29.9531746 52.6642857,29.4896825 C53.1509524,28.4815873 54.0547619,27.8790476 56.8936508,26.6507937 C62.1195238,24.4028571 64.355873,22.9196825 65.7463492,20.8107937 C67.2990476,18.4585714 67.6466667,14.7042857 66.5922222,11.911746 C65.4334921,8.87587302 62.5598413,6.81333333 58.515873,6.12968254 C57.2644444,5.90952381 54.2980952,5.94428571 52.9539683,6.18761905 C50.022381,6.70904762 47.2414286,8.15746032 45.5265079,10.0577778 C44.8544444,10.7993651 43.5450794,12.7344444 43.6261905,12.8734921 C43.6609524,12.9198413 43.9622222,13.1052381 44.298254,13.3022222 C44.6226984,13.487619 45.8509524,14.1944444 47.0096825,14.8665079 L49.1069841,16.0831746 L49.5473016,15.4342857 C50.1614286,14.4957143 51.5055556,13.2095238 52.3166667,12.7807937 C54.6457143,11.5525397 57.8438095,11.7263492 59.4196825,13.14 C60.091746,13.754127 60.3698413,14.3914286 60.3698413,15.33 C60.3698413,16.175873 60.2655556,16.5466667 59.8252381,17.1839683 C59.2574603,17.9950794 58.0987302,18.6787302 54.8079365,20.1039683 C51.0420635,21.7261905 49.4198413,22.7342857 47.9366667,24.3333333 C47.0792063,25.2603175 46.2680952,26.7434921 45.9320635,27.9833333 C45.6539683,29.0146032 45.5844444,31.5985714 45.8046032,32.6414286 C46.5809524,36.2798413 49.3271429,38.8174603 53.29,39.5706349 C54.5761905,39.8139683 57.5657143,39.7212698 58.8287302,39.4084127 Z M41.6911111,36.3609524 L41.7142857,33.3714286 L36.9634921,33.3714286 L32.2126984,33.3714286 L32.2126984,19.8722222 L32.2126984,6.37301587 L28.852381,6.37301587 L25.4920635,6.37301587 L25.4920635,19.8722222 L25.4920635,33.3714286 L20.7412698,33.3714286 L15.9904762,33.3714286 L15.9904762,36.3030159 C15.9904762,37.9252381 16.0252381,39.2809524 16.0715873,39.3157143 C16.1063492,39.3620635 21.8884127,39.3852381 28.8987302,39.3736508 L41.6563492,39.3388889 L41.6911111,36.3609524 Z" id="Shape">
</path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 5.5C5 4.11929 6.11929 3 7.5 3C8.88071 3 10 4.11929 10 5.5C10 6.88071 8.88071 8 7.5 8C6.11929 8 5 6.88071 5 5.5Z" fill="#000000"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 0C3.35786 0 0 3.35786 0 7.5C0 11.6421 3.35786 15 7.5 15C11.6421 15 15 11.6421 15 7.5C15 3.35786 11.6421 0 7.5 0ZM1 7.5C1 3.91015 3.91015 1 7.5 1C11.0899 1 14 3.91015 14 7.5C14 9.34956 13.2275 11.0187 11.9875 12.2024C11.8365 10.4086 10.3328 9 8.5 9H6.5C4.66724 9 3.16345 10.4086 3.01247 12.2024C1.77251 11.0187 1 9.34956 1 7.5Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 768 B

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M27 4H5C3.34315 4 2 5.34315 2 7V25C2 26.6569 3.34315 28 5 28H27C28.6569 28 30 26.6569 30 25V7C30 5.34315 28.6569 4 27 4Z" fill="#B71C1C"/>
<path d="M25 24H7C6.73478 24 6.48043 23.8946 6.29289 23.7071C6.10536 23.5196 6 23.2652 6 23C6 22.7348 6.10536 22.4804 6.29289 22.2929C6.48043 22.1054 6.73478 22 7 22H25C25.2652 22 25.5196 22.1054 25.7071 22.2929C25.8946 22.4804 26 22.7348 26 23C26 23.2652 25.8946 23.5196 25.7071 23.7071C25.5196 23.8946 25.2652 24 25 24Z" fill="#EEEEEE"/>
<path d="M19 25C18.7348 25 18.4804 24.8946 18.2929 24.7071C18.1054 24.5196 18 24.2652 18 24V22C18 21.7348 18.1054 21.4804 18.2929 21.2929C18.4804 21.1054 18.7348 21 19 21C19.2652 21 19.5196 21.1054 19.7071 21.2929C19.8946 21.4804 20 21.7348 20 22V24C20 24.2652 19.8946 24.5196 19.7071 24.7071C19.5196 24.8946 19.2652 25 19 25Z" fill="#EEEEEE"/>
<path d="M20.45 12.67L13.45 9.16996C13.2978 9.09325 13.1285 9.05673 12.9581 9.06386C12.7878 9.071 12.6222 9.12155 12.4769 9.21072C12.3316 9.2999 12.2115 9.42473 12.1281 9.57336C12.0446 9.722 12.0005 9.8895 12 10.06V17.94C12.0013 18.1182 12.0502 18.2928 12.1416 18.4457C12.233 18.5987 12.3637 18.7244 12.52 18.81C12.6648 18.897 12.831 18.942 13 18.94C13.1872 18.9406 13.3709 18.8886 13.53 18.79L20.53 14.41C20.6816 14.3156 20.8051 14.1823 20.8877 14.024C20.9704 13.8658 21.0091 13.6883 21 13.51C20.9905 13.3339 20.9347 13.1635 20.8381 13.0159C20.7415 12.8684 20.6076 12.7491 20.45 12.67Z" fill="#EEEEEE"/>
<path d="M5 4C4.20435 4 3.44129 4.31607 2.87868 4.87868C2.31607 5.44129 2 6.20435 2 7V25C2 25.7956 2.31607 26.5587 2.87868 27.1213C3.44129 27.6839 4.20435 28 5 28H16V4H5Z" fill="#E53935"/>
<path d="M7 22C6.73478 22 6.48043 22.1054 6.29289 22.2929C6.10536 22.4804 6 22.7348 6 23C6 23.2652 6.10536 23.5196 6.29289 23.7071C6.48043 23.8946 6.73478 24 7 24H16V22H7Z" fill="#FAFAFA"/>
<path d="M13.45 9.16996C13.2978 9.09325 13.1285 9.05673 12.9581 9.06386C12.7878 9.071 12.6222 9.12155 12.4769 9.21072C12.3316 9.2999 12.2115 9.42473 12.1281 9.57336C12.0446 9.722 12.0005 9.8895 12 10.06V17.94C12.0013 18.1182 12.0502 18.2928 12.1416 18.4457C12.233 18.5987 12.3637 18.7244 12.52 18.81C12.6648 18.897 12.831 18.942 13 18.94C13.1872 18.9406 13.3709 18.8886 13.53 18.79L16 17.24V10.44L13.45 9.16996Z" fill="#FFEBEE"/>
<path d="M27 4H5C4.20435 4 3.44129 4.31607 2.87868 4.87868C2.31607 5.44129 2 6.20435 2 7V25C2 25.7956 2.31607 26.5587 2.87868 27.1213C3.44129 27.6839 4.20435 28 5 28H27C27.7956 28 28.5587 27.6839 29.1213 27.1213C29.6839 26.5587 30 25.7956 30 25V7C30 6.20435 29.6839 5.44129 29.1213 4.87868C28.5587 4.31607 27.7956 4 27 4ZM28 25C28 25.2652 27.8946 25.5196 27.7071 25.7071C27.5196 25.8946 27.2652 26 27 26H5C4.73478 26 4.48043 25.8946 4.29289 25.7071C4.10536 25.5196 4 25.2652 4 25V7C4 6.73478 4.10536 6.48043 4.29289 6.29289C4.48043 6.10536 4.73478 6 5 6H27C27.2652 6 27.5196 6.10536 27.7071 6.29289C27.8946 6.48043 28 6.73478 28 7V25Z" fill="#263238"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 512 512"><path d="M256 0c141.385 0 256 114.615 256 256S397.385 512 256 512 0 397.385 0 256 114.615 0 256 0z"/><path fill="#fff" fill-rule="nonzero" d="M318.64 157.549h33.401l-72.973 83.407 85.85 113.495h-67.222l-52.647-68.836-60.242 68.836h-33.423l78.052-89.212-82.354-107.69h68.924l47.59 62.917 55.044-62.917zm-11.724 176.908h18.51L205.95 176.493h-19.86l120.826 157.964z"/></svg>

After

Width:  |  Height:  |  Size: 580 B

View File

@ -0,0 +1,7 @@
{
"TodoTest": "Doing Tests",
"HomePage": "Home",
"contact": "Contact",
"about": "About",
"login": "Login"
}

View File

@ -0,0 +1,7 @@
{
"TodoTest": "Haciendo pruebas",
"HomePage": "Página principal",
"contact": "Contacto",
"about": "Acerca de la página",
"login": "Identificarse"
}

View File

@ -0,0 +1,6 @@
{
"HomePage": "HomePage",
"contact": "contact",
"about": "about",
"login": "login"
}

View File

@ -0,0 +1,4 @@
// import NextAuth from 'next-auth'
// import { authOptions } from '../../../utils/auth'
// export default NextAuth(authOptions)

13
src/api/hello.ts Normal file
View File

@ -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<Data>
) {
res.status(200).json({ name: 'John Doe' })
}

View File

@ -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<HTMLInputElement>) => {
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, '<strong>$1</strong>')
}
const handlePostClick = (slug: string) => {
router.push(`/post/${slug}`)
setShowPosts(false)
}
const noResults = showPosts && filteredPosts.length === 0
return (
<div className="relative w-full max-w-lg" data-te-input-wrapper-init id="basic">
<div className="relative ">
<input
type="text"
className="peer block min-h-[auto] w-full py-2 px-4 rounded-lg border border-black "
placeholder="Busca por títulos..."
value={textSearched}
onChange={handleInputChange}
/>
<span className="absolute md:flex hidden inset-y-0 right-4 pl-3 items-center pointer-events-none">
<FaSearch className="h-5 w-5 text-gray-400" />
</span>
</div>
{noResults && (
<div className="absolute top-full left-0 w-full bg-white shadow-lg border border-gray-200 rounded-lg z-10 p-4">
No hay coincidencias.
</div>
)}
{showPosts && (
<div className="absolute top-full left-0 w-full bg-white shadow-lg border border-gray-200 rounded-lg z-10 max-h-48 overflow-auto">
{filteredPosts.map((post) => (
<div
key={post.id}
className="py-2 px-4 hover:bg-gray-100 cursor-pointer"
onClick={() => handlePostClick(post.slug)}
>
<p dangerouslySetInnerHTML={{ __html: highlightMatches(post.title) }} />
</div>
))}
</div>
)}
</div>
)
}
export default AutoCompleteInput

View File

@ -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 (
<div className={`${className} rounded-md relative overflow-hidden w-fit select-none `}>
{loading && (
<div className="flex justify-center items-center bg-white opacity-90 absolute inset-0">
<ReactLoading type="spin" width={30} height={30} color="black" />
</div>
)}
<button className={`flex items-center ${buttonClassName}`} {...others}>
{children}
{icon && <span className="ml-2">{icon}</span>}
</button>
</div>
)
}

View File

@ -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 (
<div className="w-full">
<div className="w-full rounded-2xl bg-white p-2">
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button
className={` ${className} flex w-full justify-between rounded-lg px-4 py-2 text-left text-sm font-medium focus:outline-none focus-visible:ring `}
>
<span>{text}</span>
<IoIosArrowForward className={`${open ? 'rotate-90 transform' : ''} h-5 w-5 ${classNameArrow} `} />
</Disclosure.Button>
<Disclosure.Panel className=" pb-2 pt-2 text-sm text-gray-500">{children}</Disclosure.Panel>
</>
)}
</Disclosure>
</div>
</div>
)
}

View File

@ -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
)
}
}, [])
{
/* <div className="flex items-center justify-center bg-white rounded-full w-8 h-8 hover:bg-opacity-80 hover:cursor-pointer">
<FaRegComment className="w-5 h-5" />
</div> */
}
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 (
<Menu as="div" className="relative ">
<>
<Menu.Button className="flex items-center w-5 h-5 p-0">
{/* <Menu.Button className="flex items-center bg-white rounded-full border w-8 h-8 p-0"> */}
{/* <div className="w-8 h-8 flex items-center justify-center cursor-pointer"> */}
<Image quality={100} src={Share} alt={'user'} width={20} height={20} />
{/* </div> */}
</Menu.Button>
<Transition as={Fragment}>
<Transition.Child
className="absolute top-[18px] left-[17px] w-full z-10"
enter="transition ease-out duration-500"
enterFrom="opacity-20 -translate-y-10"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="bg-white flex flex-col absolute right-0 mt-2 w-40 p-1 rounded-lg z-10 border border-gray-200 ">
<Menu.Item>
{({ active }) => (
<a
className={`flex justify-between p-2 rounded-lg cursor-pointer items-center hover:no-underline hover:bg-slate-200 ${
active && 'bg-secondary-200'
}`}
href={`https://twitter.com/intent/tweet?text=${textShare}`}
target="_blank"
rel="noopener noreferrer"
onClick={() => {
sharePost(id, 'twitter')
}}
>
<div className="flex">
<Image quality={100} src={ShareTwitter} alt={'twitter'} width={26} height={26} />
<span className="pl-1">Twitter</span>
</div>
<span className="text-xs">{counTwitter}</span>
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a
className={`flex justify-between p-2 rounded-lg cursor-pointer items-center hover:no-underline hover:bg-slate-200 ${
active && 'bg-secondary-200'
}`}
href={`https://api.whatsapp.com/send?text=${textShare}`}
target="_blank"
rel="noopener noreferrer"
onClick={() => {
sharePost(id, 'whatsapp')
}}
>
<div className="flex">
<Image quality={100} src={ShareWhatsapp} alt={'whatsapp'} width={26} height={26} />
<span className="pl-1">Whatsapp</span>
</div>
<span className="text-xs">{countWhatsapp}</span>
</a>
)}
</Menu.Item>
</Menu.Items>
</Transition.Child>
</Transition>
</>
</Menu>
)
}

View File

@ -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 (
<Dialog
open={isOpen}
onClose={() => setIsOpen(false)}
className="fixed inset-0 flex items-center justify-center z-50"
>
<Dialog.Panel className="bg-white w-80 h-64 border border-black rounded-lg p-6">
{icon && (
<div className="flex justify-center">
<Image src={AlertIcon} width={50} height={50} alt="alert" />
</div>
)}
<h3 className="font-semibold text-center">{tittle}</h3>
<div className="flex flex-col justify-center items-center gap-4 mt-5">
<p className="text-center">{description}</p>
<div className="flex gap-2">
<button
className="w-28 bg-red-200 border border-red-200 hover:bg-red-400 hover:border-black"
onClick={() => {
// router.back()
setIsOpen(false)
functionFalse()
}}
>
{textFalse}
</button>
<button
className="w-28 bg-blue-300 border border-blue-200 hover:bg-blue-400 hover:border-black"
onClick={() => {
setIsOpen(false)
functionTrue()
}}
>
{textTrue}
</button>
</div>
</div>
</Dialog.Panel>
</Dialog>
)
}
export default Modal

View File

@ -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<FormData>()
const onSubmit: SubmitHandler<FormData> = 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 (
<Dialog
open={isOpen}
onClose={() => setIsOpen(false)}
className="fixed inset-0 flex items-center justify-center z-50"
>
<Dialog.Panel className="bg-white w-80 h-64 border border-black rounded-lg p-6">
{icon && (
<div className="flex justify-center">
<Image src={AlertIcon} width={50} height={50} alt="alert" />
</div>
)}
<h3 className="font-semibold text-center">{tittle}</h3>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col justify-center items-center gap-4 mt-5">
<p className="text-center">{description}</p>
<div className="w-full">
<input
type="text"
className="px-4 py-2 rounded-md w-full border border-gray-400"
placeholder="Nombre"
{...register('name', { required: 'El campo del nombre es obligatorio' })}
/>
</div>
<div className="flex w-full justify-center items-center gap-2">
<label className="text-gray-600">Color</label>
<input
type="color"
className="w-7 h-8 p-1 rounded-md"
{...register('color', { required: 'El campo del color es obligatorio' })}
/>
</div>
<div className="flex gap-2">
<button
className="w-28 bg-red-200 border border-red-200 hover:bg-red-400 hover:border-black"
onClick={() => {
// router.back()
setIsOpen(false)
functionFalse()
}}
>
{textFalse}
</button>
<button
type="submit"
className="w-28 bg-blue-300 border border-blue-200 hover:bg-blue-400 hover:border-black"
onClick={() => {
setIsOpen(false)
functionTrue()
handleSubmit(onSubmit)
}}
>
{textTrue}
</button>
</div>
</div>
</form>
</Dialog.Panel>
</Dialog>
)
}
export default ModalCreateTag

View File

@ -0,0 +1,10 @@
const StatIndividual = ({ stat, tittle }: { stat: number; tittle: string }) => {
return (
<div className="text-center md:border-r">
<h6 className="text-4xl font-bold lg:text-5xl xl:text-6xl">{stat}</h6>
<p className="text-sm font-medium tracking-widest text-gray-800 uppercase lg:text-base">{tittle}</p>
</div>
)
}
export default StatIndividual

View File

@ -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 (
<div className={`${className}`}>
<div className="hidden text-sm font-medium text-center text-gray-500 divide-x divide-gray-200 rounded-lg shadow sm:flex dark:divide-gray-700 dark:text-gray-400">
<span
className={`inline-block w-full p-1 px-2 rounded-l-lg cursor-pointer ${
selectedTab === 'es'
? 'bg-blue-100 hover:bg-blue-100 dark:bg-gray-800 dark:hover:bg-gray-700'
: 'bg-white hover:text-gray-700 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700'
}`}
onClick={() => handleTabClick('es')}
>
Es
</span>
<span
className={`inline-block w-full p-1 px-2 rounded-r-lg cursor-pointer ${
selectedTab === 'en'
? 'bg-blue-100 hover:bg-blue-100 dark:bg-gray-800 dark:hover:bg-gray-700'
: 'bg-white hover:text-gray-700 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700'
}`}
onClick={() => handleTabClick('en')}
>
En
</span>
</div>
</div>
)
}

View File

@ -0,0 +1,21 @@
type Tag = {
color: string
name: string
className?: string
[others: string]: any
}
const Tag = ({ color, name, className, others }: Tag) => {
return (
<div
className={`flex justify-between items-center px-2 py-1 w-fit h-6 rounded-xl border-[1px] border-black border-solid gap-2 `}
style={{ backgroundColor: `${color}80` }}
>
<div className={`${className}`} {...others}>
<p className="text-xs font-bold ">{name}</p>
</div>
</div>
)
}
export default Tag

View File

@ -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 (
<div>
<Navbar />
<div className="h-full">{children}</div>
<Footer />
</div>
)
}

View File

@ -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<boolean>()
const [isFavorite, setIsFavorite] = useState<boolean>()
const [likesCount, setLikesCount] = useState<number>(Like?.length || 0)
const [favoriteCount, setFavoriteCount] = useState<number>(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 (
<div
className="flex lg:flex-row flex-col rounded-xl border-black border-4 gap-4 p-6 mb-5 justify-between items-center lg:min-h-full min-h-[450px]"
style={{ backgroundColor: `${Category?.color}80` }}
>
<div className="flex justify-center items-start">
<div
className="relative h-[120px] w-[120px] border-white border-2 m-2 p-4 rounded-xl"
style={{ backgroundColor: `${Category?.color}` }}
>
<Image
src={`/images/${Category?.img}.svg`}
height={100}
width={100}
alt="img post"
className="w-full h-full object-cover"
></Image>
</div>
</div>
<div className="flex flex-col w-full justify-between m-2">
<div className="mb-6">
<Link href={`/post/${slug}`}>
<div className="flex text-center lg:text-left items-center">
<h2 className="flextext-black mb-4 hover:opacity-50 lg:text-3xl text-xl font-extrabold ">{title}</h2>
</div>
</Link>
<div className="lg:flex lg:flex-row flex-col gap-2">
<p className="text-black">
{description?.length > 80 ? `${description.substring(0, 80)}...` : description}
</p>{' '}
{description?.length > 80 && (
<a
className="font-medium flex justify-center text-[#0066cc] hover:text-[#726edf] visited:text-[#800080] whitespace-pre"
href={`/post/${slug}`}
>
ver más
</a>
)}
</div>
</div>
<div className="flex lg:flex-row flex-col md:justify-between justify-center items-center gap-2">
{/* <div className="flex flex-wrap gap-1">
{Tags &&
Tags?.slice(0, 4).map((tag) => (
<div
key={tag?.id}
className={`flex justify-between items-center px-2 py-1 w-fit h-6 rounded-xl border-[1px] border-black border-solid gap-2 `}
style={{ backgroundColor: `${tag?.color}80` }}
>
<div>
<p className="text-xs font-bold ">{tag?.name}</p>
</div>
</div>
))}
</div> */}
<div className="flex md:justify-end justify-center gap-2">
{/* <div className="flex items-center justify-center bg-white rounded-full w-8 h-8 hover:opacity-50 hover:cursor-pointer">
<a href={url} target="_blank" rel="noopener noreferrer">
<FaLink className="w-5 h-5" />
</a>
</div> */}
<div className="flex items-center">
<div
className="flex items-center justify-center"
onClick={() => {
isLike ? handleDeleteLike() : handleAddLike()
}}
>
{isLike ? (
<FcLike className="w-5 h-5 cursor-pointer hover:scale-110" />
) : (
<FiHeart className="w-5 h-5 cursor-pointer hover:opacity-50 hover:scale-110" />
)}
{/* <AiOutlineLike className="w-5 h-5" /> */}
</div>
<span className="text-xs text-gray-600 w-4 h-4 flex justify-center items-center font-bold">
{likesCount}
</span>
</div>
<div className="flex items-center">
<div
className="flex items-center justify-center"
onClick={() => {
isFavorite ? handleDeleteFavorite() : handleAddFavorite()
}}
>
{isFavorite ? (
<FaStar className="w-5 h-5 cursor-pointer hover:scale-110 text-yellow-500" />
) : (
<FaRegStar className="w-5 h-5 cursor-pointer hover:scale-110" />
)}
<span className="text-xs text-gray-600 w-4 h-4 flex justify-center items-center font-bold">
{favoriteCount}
</span>
</div>
</div>
<div className="flex items-center">
<Link href={`/post/${slug}`}>
<div className="flex items-center justify-center w-5 h-5 hover:bg-opacity-80 hover:cursor-pointer hover:opacity-50 hover:scale-110">
<FaRegComment className="w-5 h-5" />
</div>
</Link>
<span className="text-xs text-gray-600 w-4 h-4 flex justify-center items-center font-bold ">
{comments?.length}
</span>{' '}
</div>
<div className="flex items-center ">
<div className="">
<DropDownShare slug={slug} id={id} counTwitter={twitterShareCount} countWhatsapp={twitterShareCount} />
</div>
<span className="text-xs text-gray-600 w-4 h-4 flex justify-center items-center font-bold">
{twitterShareCount + whatsappShareCount}
</span>{' '}
</div>
<div className="flex items-center">
<CgLoadbarSound className="w-6 h-6" />
<span className="text-xs text-gray-600 w-4 h-4 flex justify-center items-center font-bold">
{views}
</span>{' '}
</div>
</div>
</div>
</div>
<Modal
setIsOpen={setIsOpen}
isOpen={isOpen}
icon={true}
tittle="No estas logeado"
description="No puedes comentar sin estar registrado antes."
textTrue="Login"
textFalse="Cancelar"
functionTrue={() => router.push('/login')}
functionFalse={() => router.back()}
/>
</div>
)
}

View File

@ -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<Post[]>([])
// const [countN, setCount] = useState<number>(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 (
<>
<div className="flex flex-col ">
{Array.isArray(postData) && postData?.map((item) => <Card {...item} post={item} key={item.id} />)}
</div>
<div className="py-4">
<Pagination page={page} hasPrev={hasPrev} hasNext={hasNext} />
</div>
</>
)
}

View File

@ -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<boolean>()
const [isFavorite, setIsFavorite] = useState<boolean>()
const [likesCount, setLikesCount] = useState<number>(Like?.length || 0)
const [favoriteCount, setFavoriteCount] = useState<number>(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 (
<div
className="flex flex-row rounded-xl border-black border-4 lg:gap-4 lg:p-3 p-1 mb-5 justify-center items-center "
style={{ backgroundColor: `${Category?.color}80` }}
>
{/* <div className="lg:block hidden justify-center items-start">
<div
className="relative h-[40px] w-[40px] border-white border-2 m-2 p-1 rounded-xl"
style={{ backgroundColor: `${Category?.color}` }}
>
<Image
src={`/images/${Category?.img}.svg`}
height={100}
width={100}
alt="img post"
className="w-full h-full object-cover"
></Image>
</div>
</div> */}
<div className="flex flex-col w-full justify-between m-2">
<div className="flex">
<Link href={`/post/${slug}`}>
<div className="flex text-center lg:text-left">
<h3 className="flex text-gray-700 mb-4 hover:opacity-50 lg:text-lg text-xs font-extrabold ">{title}</h3>
</div>
</Link>
</div>
<div className="flex md:justify-between justify-center items-center">
<div
className={`md:flex hidden justify-between items-center px-4 py-1 w-fit h-10 rounded-xl border-[1px200psx20] border-black border-solid gap-2 `}
style={{ backgroundColor: Category?.color }}
>
<div>
<p className="text-sm font-bold ">{Category?.title}</p>
</div>
</div>
<div className="flex md:justify-end justify-start gap-0 mobile:gap-2">
{/* <div className="flex items-center justify-center bg-white rounded-full w-8 h-8 hover:opacity-50 hover:cursor-pointer">
<a href={url} target="_blank" rel="noopener noreferrer">
<FaLink className="w-5 h-5" />
</a>
</div> */}
<div className="flex items-center">
<div
className="flex items-center justify-center"
onClick={() => {
isLike ? handleDeleteLike() : handleAddLike()
}}
>
{isLike ? (
<FcLike className="w-5 h-5 cursor-pointer hover:scale-110 " />
) : (
<FiHeart className="w-5 h-5 cursor-pointer hover:opacity-50 hover:scale-110" />
)}
{/* <AiOutlineLike className="w-5 h-5" /> */}
</div>
<span className="text-xs text-gray-600 w-4 h-4 flex justify-center items-center font-bold">
{likesCount}
</span>
</div>
<div className="flex items-center">
<div
className="flex items-center justify-center"
onClick={() => {
isFavorite ? handleDeleteFavorite() : handleAddFavorite()
}}
>
{isFavorite ? (
<FaStar className="w-5 h-5 cursor-pointer hover:scale-110 text-yellow-500" />
) : (
<FaRegStar className="w-5 h-5 cursor-pointer hover:scale-110" />
)}
<span className="text-xs text-gray-600 w-4 h-4 flex justify-center items-center font-bold">
{favoriteCount}
</span>
</div>
</div>
<div className="flex items-center">
<Link href={`/post/${slug}`}>
<div className="flex items-center justify-center w-5 h-5 hover:bg-opacity-80 hover:cursor-pointer hover:opacity-50 hover:scale-110">
<FaRegComment className="w-5 h-5" />
</div>
</Link>
<span className="text-xs text-gray-600 w-4 h-4 flex justify-center items-center font-bold ">
{comments?.length}
</span>{' '}
</div>
<div className="flex items-center ">
<div className="">
<DropDownShare slug={slug} id={id} counTwitter={twitterShareCount} countWhatsapp={twitterShareCount} />
</div>
<span className="text-xs text-gray-600 w-4 h-4 flex justify-center items-center font-bold">
{twitterShareCount + whatsappShareCount}
</span>{' '}
</div>
<div className="flex items-center">
<CgLoadbarSound className="w-6 h-6" />
<span className="text-xs text-gray-600 w-4 h-4 flex justify-center items-center font-bold">
{views}
</span>{' '}
</div>
</div>
</div>
</div>
<Modal
setIsOpen={setIsOpen}
isOpen={isOpen}
icon={true}
tittle="No estas logeado"
description="No puedes comentar sin estar registrado antes."
textTrue="Login"
textFalse="Cancelar"
functionTrue={() => router.push('/login')}
functionFalse={() => router.back()}
/>
</div>
)
}

View File

@ -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<React.SetStateAction<Comment[]>>
}
export default function Comment({
postSlug
}: // comments, setComments
CommentProps) {
const router = useRouter()
const session = useSession()
const [showModalComment, setShowModalComment] = useState<boolean>(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<Comment[]>([])
// const [loading, setLoading] = useState<boolean>(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 (
<div>
<div className="mb-6">
{isLoading
? '...loading'
: data?.map((comment: Comment) => (
<div key={comment.id} className="w-full flex flex-col justify-start gap-4">
<div className="mb-6">
<div className="flex justify-between items-center font-semibold gap-3">
<div className="flex">
{comment?.user?.image && (
<div className="relative h-[60px] w-[60px] border-gray-300 border-2 m-2 rounded-full overflow-hidden">
<Image
src={comment?.user?.image}
width={50}
height={50}
alt="avatar Mujer"
className="w-full h-full object-cover"
/>
</div>
)}
<div className="flex flex-col justify-center text-gray-500 gap-1">
<p>{comment?.user?.name}</p>
<p className="font-semibold">{formatDate(comment?.createdAt)}</p>
</div>
</div>
{comment?.user?.email === session.data?.user?.email && (
<div onClick={() => setShowModalComment(true)}>
<MdDelete className="w-5 h-5 hover:cursor-pointer hover:w-6 hover:h-6" />
</div>
)}
</div>
<p className="font-semibold p-2">{comment.description}</p>
</div>
{/* MODAL PARA ELIMINAR COMENTARIO */}
<Modal
setIsOpen={setShowModalComment}
isOpen={showModalComment}
icon={false}
tittle="¿Estás seguro de borrar?"
description="No puedes recuperar este comentario una vez borrado."
textTrue="Borrar"
textFalse="Cancelar"
functionTrue={() => deleteComment(comment?.id)}
functionFalse={() => setShowModalComment(false)}
/>
</div>
))}
</div>
</div>
)
}

View File

@ -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<HTMLTextAreaElement>) => {
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 (
<div className="mb-6">
<h2 className="font-bold my-5">Comments</h2>
<div className="w-full flex lg:flex-row flex-col justify-center items-center gap-4">
<textarea
className="w-full border-2 p-5 rounded-md placeholder-gray-500 bg- focus:placeholder-gray-500 resize-none "
placeholder="Write a comment..."
value={description}
onChange={handleChange}
/>
<Button
loading={loading}
className="h-fit"
buttonClassName="flex justify-center w-full text-white content-center font-bold bg-teal-600 py-2 px-6"
onClick={handleSubmit}
>
Send
</Button>
</div>
</div>
)
}

View File

@ -0,0 +1,240 @@
import { useSession } from 'next-auth/react'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { Category } from '../../../type'
import { Button } from '../atoms/Button'
interface FormData {
title: string
description: string
url: string
catSlug: string
// tag?: string[]
}
export function CreatePostForm() {
const {
register,
handleSubmit,
watch,
getValues,
formState: { errors }
} = useForm<FormData>()
// console.log(errors)
const router = useRouter()
// const session = useSession()
const { data: session } = useSession()
const [categories, setCategories] = useState<Category[]>([])
const [categorySelected, setCategorySelected] = useState<Category>()
const [error, setError] = useState<string>()
// const [showCreateTag, setShowCreateTag] = useState<boolean>(false)
useEffect(() => {
const getData = async () => {
try {
const response = await fetch('api/categories', {
cache: 'no-store'
})
if (!response.ok) {
throw new Error('Failed')
}
const result = await response.json()
setCategories(result)
} catch (error) {
console.error(error)
}
}
getData()
}, [])
const slugify = (str: string) => {
return str
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '')
}
const onSubmit: SubmitHandler<FormData> = async (dataForm) => {
const { title, description, url, catSlug } = dataForm
try {
const response = await fetch('/api/post', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title,
description,
slug: slugify(title),
catSlug: catSlug,
url,
userEmail: session?.user?.email
})
})
// if (!response.ok) {
// throw new Error('You have already used this title')
// }
if (!response.ok) {
const errorData = await response.json()
if (errorData.type === 'duplicate') throw new Error(error)
if (errorData.type === 'unAuthorized') throw new Error(error)
}
const data = await response.json()
router.push('/')
} catch (error) {
console.error(error, 'Error fetching data')
const errorMessage = (error as { message?: string })?.message || 'Error creating post'
setError(errorMessage)
}
}
useEffect(() => {
const category = getValues('catSlug')
const categorySelect = categories.find((cat) => cat.slug === category)
categorySelect && setCategorySelected(categorySelect)
}, [watch('catSlug')])
useEffect(() => {
// hago esto para comprobar si el usuario logeado (session) es admin.
// Para eso hago un get del usuario para traerme los datos de este y ver si es admin.
const fetchUser = async () => {
try {
const response = await fetch(`/api/user?email=${session?.user?.email}`)
const userData = await response.json()
!userData.isAdmin && router.push('/')
} catch (error: any) {
console.error('Error fetching user:', error.message)
}
}
fetchUser()
}, [])
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div
className="flex lg:flex-row flex-col rounded-xl border-black border-4 gap-4 p-6 mb-5"
style={{ backgroundColor: categorySelected ? `${categorySelected?.color}80` : '#c7c7c7' }}
>
<div className="flex justify-center items-start">
<div
className="relative h-[200px] w-[200px] border-white border-2 p-4 rounded-xl"
style={{ backgroundColor: categorySelected ? categorySelected?.color : '#c7c7c7' }}
>
<Image
src={categorySelected ? `/images/${categorySelected.img}.svg` : `/images/error.svg`}
width={200}
height={200}
alt="img post"
className="w-full h-full object-cover"
></Image>
</div>
</div>
<div className="w-full flex flex-col justify-between lg:gap-1 gap-5">
<div className="flex lg:flex-row flex-col gap-5">
<div>
<input
type="text"
className="px-4 py-2 rounded-md w-full"
placeholder="Titulo"
{...register('title', { required: 'El campo del título es obligatorio' })}
/>
</div>
<div className="w-full">
<select {...register('catSlug')} className="w-full h-full px-4 py-2 rounded-md text-gray-600">
<option value="">Seleccionar categoría</option>
{categories.map((category: Category) => (
<option key={category.id} value={category.slug}>
{category.title}
</option>
))}
</select>
</div>
</div>
<div className="flex flex-col gap-4">
<textarea
className="w-full border-2 p-2 rounded-md placeholder-gray-500 bg- focus:placeholder-gray-500 resize-none "
placeholder="Write a comment..."
{...register('description', { required: 'El campo de comentario es obligatorio' })}
/>
</div>
<div>
<input
type="url"
className="px-4 py-2 rounded-md w-full"
placeholder="Url"
{...register('url', {
required: 'La url es obligatoria'
// pattern: {
// value: /^(ftp|http|https):\/\/[^ "]+$/,
// message: 'Ingrese una URL válida'
// }
})}
/>
</div>
</div>
</div>
{errors && errors.title && (
<div className="bg-red-500 rounded-md flex justify-center font-bold mb-5">
<p className="text-white text-sm p-2 uppercase">{errors.title.message}</p>
</div>
)}
{errors && errors.description && (
<div className="bg-red-500 rounded-md flex justify-center font-bold mb-5">
<p className="text-white text-sm p-2 uppercase">{errors.description.message}</p>
</div>
)}
{errors && errors.url && (
<div className="bg-red-500 rounded-md flex justify-center font-bold mb-5">
<p className="text-white text-sm p-2 uppercase">{errors.url.message}</p>
</div>
)}
{error && (
<div className="bg-red-500 rounded-md flex justify-center font-bold mb-5">
<p className="text-white text-sm p-2 uppercase">{error}</p>
</div>
)}
{/* <ModalCreateTag
setIsOpen={setShowCreateTag}
isOpen={showCreateTag}
tittle="Crear un tag nuevo"
// description="No puedes comentar sin estar registrado antes."
textTrue="Crear"
textFalse="Cancelar"
functionTrue={() => console.log('nada')}
functionFalse={() => console.log('nada')}
/> */}
<div className="flex lg:justify-end justify-center gap-4">
{/* <div
className="font-medium py-2 px-4 rounded-lg bg-red-200 border border-red-200 hover:bg-red-400 hover:border-black cursor-pointer"
onClick={() => {
router.push('/editTag')
}}
>
Editar tag
</div>
<div
className="font-medium py-2 px-4 rounded-lg bg-red-200 border border-red-200 hover:bg-red-400 hover:border-black cursor-pointer"
onClick={() => {
router.push('/createTag')
}}
>
Añadir tag
</div> */}
<Button
className="h-fit"
buttonClassName="flex justify-center w-full text-white content-center font-bold bg-teal-600 py-2 px-6 hover:opacity-80"
type="submit"
>
Crear
</Button>{' '}
</div>
</form>
)
}

View File

@ -0,0 +1,291 @@
import { useSession } from 'next-auth/react'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { Category, Post } from '../../../type'
import { Button } from '../atoms/Button'
interface FormData {
title: string
description: string
url: string
catSlug: string
// Tags?: Tag[]
}
type EditProps = {
dataPost: Post
}
export function EditPost({ dataPost }: EditProps) {
const {
register,
handleSubmit,
watch,
getValues,
reset,
formState: { errors }
} = useForm<FormData>()
// console.log(errors)
const router = useRouter()
// const session = useSession()
const { data: session } = useSession()
const [categories, setCategories] = useState<Category[]>([])
// const [tags, setTags] = useState<Tag[]>()
const [categorySelected, setCategorySelected] = useState<Category>()
// const [selectedTags, setSelectedTags] = useState<Tag[]>()
const [error, setError] = useState<string>()
useEffect(() => {
const getData = async () => {
try {
const response = await fetch('/api/categories', {
cache: 'no-store'
})
if (!response.ok) {
throw new Error('Failed')
}
const result = await response.json()
setCategories(result)
} catch (error) {
console.error(error)
}
}
getData()
}, [])
// const slugify = (str: string) => {
// return str
// .toLowerCase()
// .trim()
// .replace(/[^\w\s-]/g, '')
// .replace(/[\s_-]+/g, '-')
// .replace(/^-+|-+$/g, '')
// }
const onSubmit: SubmitHandler<FormData> = async (dataForm) => {
const { title, description, url, catSlug } = dataForm
try {
const response = await fetch(`/api/post/${dataPost?.slug}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title,
description,
// slug: slugify(title),
catSlug: catSlug,
url
// Tags: selectedTags
// userEmail: session?.user?.email
})
})
// if (!response.ok) {
// throw new Error('You have already used this title')
// }
if (!response.ok) {
const errorData = await response.json()
if (errorData.type === 'duplicate') throw new Error(error)
if (errorData.type === 'unAuthorized') throw new Error(error)
}
const data = await response.json()
router.push('/')
} catch (error) {
console.error(error, 'Error fetching data')
const errorMessage = (error as { message?: string })?.message || 'Error creating post'
setError(errorMessage)
}
}
useEffect(() => {
const category = getValues('catSlug')
const categorySelect = categories.find((cat) => cat.slug === category)
categorySelect && setCategorySelected(categorySelect)
}, [watch('catSlug')])
useEffect(() => {
// hago esto para comprobar si el usuario logeado (session) es admin.
// Para eso hago un get del usuario para traerme los datos de este y ver si es admin.
const fetchUser = async () => {
try {
const response = await fetch(`/api/user?email=${session?.user?.email}`)
const userData = await response.json()
!userData.isAdmin && router.push('/')
} catch (error: any) {
console.error('Error fetching user:', error.message)
}
}
fetchUser()
}, [session])
useEffect(() => {
reset({
title: dataPost?.title,
catSlug: dataPost?.catSlug,
description: dataPost?.description,
url: dataPost?.url
// Tags: dataPost?.Tags
})
// setSelectedTags(dataPost?.Tags?.map((tag) => tag) || [])
}, [dataPost])
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex justify-center mb-4 uppercase">
<h1>Editar post</h1>
</div>
<div
className="flex lg:flex-row flex-col rounded-xl border-black border-4 gap-4 p-6 mb-5"
style={{ backgroundColor: categorySelected ? `${categorySelected?.color}80` : '#c7c7c7' }}
>
<div className="flex justify-center items-start">
<div
className="relative h-[200px] w-[200px] border-white border-2 p-4 rounded-xl"
style={{ backgroundColor: categorySelected ? categorySelected?.color : '#c7c7c7' }}
>
<Image
src={categorySelected ? `/images/${categorySelected.img}.svg` : `/images/error.svg`}
width={200}
height={200}
alt="img post"
className="w-full h-full object-cover"
></Image>
</div>
</div>
<div className="w-full flex flex-col justify-between gap-4">
<div className="flex flex-col lg:flex-row gap-5">
<div className="w-full">
<input
type="text"
className="w-full px-4 py-2 rounded-md"
placeholder="Titulo"
{...register('title', { required: 'El campo del título es obligatorio' })}
/>
</div>
<div className="w-full">
<select {...register('catSlug')} className="w-full h-full px-4 py-2 rounded-md text-gray-600">
<option value="">Seleccionar categoría</option>
{categories.map((category: Category) => (
<option key={category.id} value={category.slug}>
{category.title}
</option>
))}
</select>
</div>
{/* <div className="w-full">
<select {...register('tags')} className="w-full h-full px-4 py-2 rounded-md text-gray-600">
<option value="">Seleccionar tag</option>
{tags?.map((tag: Tag) => (
<option key={tag.id} value={tag.slug}>
{tag.name}
</option>
))}
</select>
</div> */}
{/* <div className="w-full">
<select
{...register('Tag')}
className="w-full h-full px-4 py-2 rounded-md text-gray-600"
onChange={(e) => {
const selectedTag = e.target.value
setSelectedTags((prevTags) => [...prevTags, selectedTag]) // Permite seleccionar múltiples tags
}}
>
<option value="">Seleccionar tag</option>
{tags?.map((tag: Tag) => (
<option key={tag.id} value={tag.slug}>
{tag.name}
</option>
))}
</select>
</div> */}
</div>
<div className="flex flex-wrap gap-2">
{/* {tags?.map((tag) => (
<div
key={tag?.id}
className={`flex justify-between items-center px-2 py-1 w-fit h-6 rounded-xl border-[1px] border-black border-solid gap-2 `}
style={{ backgroundColor: `${tag?.color}80` }}
>
<input
type="checkbox"
id={tag?.slug}
value={tag?.slug}
checked={selectedTags.map((tag) => tag.slug).includes(tag.slug)}
onChange={(e) => {
const tagSlug = e.target.value
const selectedTag = tags.find((tag) => tag.slug === tagSlug)
setSelectedTags((prevTags) => {
if (prevTags.some((prevTag) => prevTag.slug === tagSlug)) {
// Si el tag ya está seleccionado, quítalo de la lista
return prevTags.filter((prevTag) => prevTag.slug !== tagSlug)
} else {
// Si el tag no está seleccionado, agrégalo a la lista
return [...prevTags, selectedTag]
}
})
}}
/>
<label htmlFor={tag?.slug}>{tag?.name}</label>
</div>
))} */}
</div>
<div className="flex flex-col gap-4">
<textarea
className="w-full border-2 p-2 rounded-md placeholder-gray-500 bg- focus:placeholder-gray-500 resize-none "
placeholder="Write a comment..."
{...register('description', { required: 'El campo de comentario es obligatorio' })}
/>
</div>
<div>
<input
type="url"
className="px-4 py-2 rounded-md w-full"
placeholder="Url"
{...register('url', {
required: 'La url es obligatoria'
// pattern: {
// value: /^(ftp|http|https):\/\/[^ "]+$/,
// message: 'Ingrese una URL válida'
// }
})}
/>
</div>
</div>
</div>
{errors && errors.title && (
<div className="bg-red-500 rounded-md flex justify-center font-bold mb-5">
<p className="text-white text-sm p-2 uppercase">{errors.title.message}</p>
</div>
)}
{errors && errors.description && (
<div className="bg-red-500 rounded-md flex justify-center font-bold mb-5">
<p className="text-white text-sm p-2 uppercase">{errors.description.message}</p>
</div>
)}
{errors && errors.url && (
<div className="bg-red-500 rounded-md flex justify-center font-bold mb-5">
<p className="text-white text-sm p-2 uppercase">{errors.url.message}</p>
</div>
)}
{error && (
<div className="bg-red-500 rounded-md flex justify-center font-bold mb-5">
<p className="text-white text-sm p-2 uppercase">{error}</p>
</div>
)}
<div className="flex justify-end ">
<Button
className="h-fit"
buttonClassName="flex justify-center w-full text-white content-center font-bold bg-teal-600 py-2 px-6"
>
Editar
</Button>{' '}
</div>
</form>
)
}

View File

@ -0,0 +1,136 @@
import { useSession } from 'next-auth/react'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { User } from '../../../type'
import { Button } from '../atoms/Button'
type EditProps = {
user: User
}
export function EditUser({ user }: EditProps) {
const {
register,
handleSubmit,
watch,
getValues,
reset,
setError,
formState: { errors }
} = useForm<User>({
defaultValues: {}
})
const router = useRouter()
// const [userData, setUser] = useState<User>()
const { data: session, status } = useSession()
const [errorShow, setShowError] = useState('')
const onSubmit: SubmitHandler<User> = async (dataForm) => {
try {
const response = await fetch(`/api/user/?email=${user?.email}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: dataForm.name
})
})
if (response.status === 409) {
const errorData = await response.json()
setError('name', { type: 'manual', message: errorData.message })
return
}
if (!response.ok) {
throw new Error('Error updating user. Please try again later.')
}
const data = await response.json()
setShowError(data?.message)
// router.push('/')
} catch (error) {
console.error(error, 'Error fetching data')
}
}
useEffect(() => {
status === 'unauthenticated' && router.push('/')
}, [session, status, router])
useEffect(() => {
reset({
name: user?.name,
email: user?.email
})
}, [user])
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex justify-center mb-4 uppercase">
<h1 className="lg:text-3xl text-xl font-semibold">Editar usuario</h1>
</div>
<div className="flex lg:flex-row flex-col rounded-xl border-black border-4 gap-4 p-6 mb-5">
<div className="flex justify-center items-start">
<div className="relative h-[160px] w-[160px] border-white border-2 p-4 rounded-xl">
<Image
src={user?.image}
width={200}
height={200}
alt="img post"
className="w-full h-full object-cover rounded-lg"
></Image>
</div>
</div>
<div className="w-full flex flex-col py-4 gap-2">
<div className="flex gap-5">
<div className="flex w-full lg:flex-row flex-col items-center lg:gap-6 gap-2">
<label className="font-bold lg:w-10 w-full">Apodo:</label>
<input
type="text"
className="w-full lg:w-2/4 px-4 py-2 rounded-md border border-gray-400"
placeholder="Name"
{...register('name', { required: 'El campo del name es obligatorio' })}
/>
</div>
</div>
<div className="flex gap-5">
<div className="flex w-full lg:flex-row flex-col items-center lg:gap-6 gap-2">
<label className="font-bold lg:w-10 w-full">Email:</label>
<input
disabled
type="email"
className="w-full lg:w-2/4 px-4 py-2 rounded-md border border-gray-400"
placeholder="Email"
{...register('email', { required: 'El campo del email es obligatorio' })}
/>
</div>
</div>
</div>
</div>
{errors && errors.name && (
<div className="bg-red-500 rounded-md flex justify-center font-bold mb-5">
<p className="text-white text-sm p-2 uppercase">{errors.name.message}</p>
</div>
)}
{errors && errors.email && (
<div className="bg-red-500 rounded-md flex justify-center font-bold mb-5">
<p className="text-white text-sm p-2 uppercase">{errors.email.message}</p>
</div>
)}
{errorShow && (
<div className="bg-red-500 rounded-md flex justify-center font-bold mb-5">
<p className="text-white text-sm p-2 uppercase">{errorShow}</p>
</div>
)}
<div className="flex justify-end ">
<Button
className="h-fit"
buttonClassName="flex justify-center w-full text-white content-center font-bold bg-teal-600 py-2 px-6"
>
Guardar
</Button>{' '}
</div>
</form>
)
}

View File

@ -0,0 +1,77 @@
import { signOut, useSession } from 'next-auth/react'
import { useRouter } from 'next/router'
import { User } from '../../../type'
import { Button } from '../atoms/Button'
interface MenuNavbarProps {
// userCurrent: User | undefined
dataUserCurrent: User
}
export const MenuNavbar = ({ dataUserCurrent }: MenuNavbarProps) => {
const router = useRouter()
const { status, data: session } = useSession()
return (
<div className="md:flex gap-2 hidden md:items-center">
<Button
buttonClassName="content-center font-bold hover:opacity-50 p-2"
onClick={() => {
router.push('/')
}}
>
Home
</Button>
{dataUserCurrent && dataUserCurrent.isAdmin && (
<Button
buttonClassName="content-center font-bold hover:opacity-50 p-2"
onClick={() => {
router.push('/create')
}}
>
Create
</Button>
)}
{dataUserCurrent && (
<Button
className="btn-primary-500 font-bold hover:opacity-50 "
buttonClassName="content-center font-bold hover:opacity-50 p-2"
onClick={() => {
router.push('/perfil')
}}
>
Perfil
</Button>
)}
{status === 'authenticated' ? (
<Button
buttonClassName="btn-primary-500 font-bold hover:opacity-50 p-2"
onClick={() => {
signOut()
}}
>
Logout
</Button>
) : (
<Button
buttonClassName="btn-primary-500 font-bold hover:opacity-50 p-2"
onClick={() => {
router.push('/login')
}}
>
Login
</Button>
)}
{!dataUserCurrent && (
<Button
buttonClassName="btn-primary-500 font-bold hover:opacity-50 p-2"
onClick={() => {
router.push('/register')
}}
>
Register
</Button>
)}
{/* <Tab /> */}
</div>
)
}

View File

@ -0,0 +1,33 @@
import { useRouter } from 'next/router'
import { Button } from '../atoms/Button'
const Pagination = ({ page, hasPrev, hasNext }: any) => {
const router = useRouter()
return (
<div className="flex gap-3 justify-between">
<Button
className="min-w-[110px] h-fit"
buttonClassName="flex justify-center w-full text-white content-center font-bold bg-teal-600 py-2 px-6 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={!hasPrev}
onClick={() => {
router.push(`?page=${Number(page) - 1}`)
}}
>
Previous
</Button>
<Button
className="min-w-[110px] h-fit"
buttonClassName="flex justify-center w-full text-white content-center font-bold bg-teal-600 py-2 px-6 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={!hasNext}
onClick={() => {
router.push(`?page=${Number(page) + 1}`)
}}
>
Next
</Button>
</div>
)
}
export default Pagination

View File

@ -0,0 +1,58 @@
import { useEffect, useState } from 'react'
import { Stats } from '../../../type'
const Stats = () => {
const [stats, setStats] = useState<Stats>(null)
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('/api/stats')
if (!response.ok) {
throw new Error('Failed to fetch data')
}
const data = await response.json()
setStats(data)
} catch (error) {
console.error('Error fetching stats:', error)
}
}
fetchData()
}, [])
return (
<div>
{stats ? (
<div className="grid grid-cols-2 row-gap-8 md:grid-cols-4">
<div className="flex flex-col text-center md:border-r gap-2">
<h6 className="text-4xl font-bold lg:text-5xl xl:text-6xl">{stats.totalPosts}</h6>
<p className="text-sm font-medium tracking-widest text-gray-800 uppercase lg:text-base">Recursos totales</p>
</div>
<div className="flex flex-col text-center md:border-r gap-2">
<h6 className="text-4xl font-bold lg:text-5xl xl:text-6xl">{stats.totalUsers}</h6>
<p className="text-sm font-medium tracking-widest text-gray-800 uppercase lg:text-base">Usuarios totales</p>
</div>
<div className="flex flex-col text-center md:border-r gap-2">
<div>
<h6 className="text-4xl font-bold lg:text-5xl xl:text-6xl">
{stats.totalShares.twitterShareCount + stats.totalShares.whatsappShareCount}
</h6>
</div>
<p className="text-sm font-medium tracking-widest text-gray-800 uppercase lg:text-base">
Recursos compartidos
</p>
</div>
<div className="text-center flex flex-col gap-2">
<h6 className="text-4xl font-bold lg:text-5xl xl:text-6xl">{stats.totalViews.views}</h6>
<p className="text-sm font-medium tracking-widest text-gray-800 uppercase lg:text-base">
Vistas totales de recursos
</p>
</div>
</div>
) : (
<p>Loading...</p>
)}
</div>
)
}
export default Stats

View File

@ -0,0 +1,59 @@
import { useTranslation } from 'next-i18next'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import github from '../../../public/images/github.svg'
import linkedin from '../../../public/images/linkedin.svg'
export function Footer() {
const { t } = useTranslation()
const router = useRouter()
return (
<div className="flex flex-col justify-between w-full bg-black lg:px-28 lg:py-12 p-6 text-white">
<div className="flex lg:flex-row gap-8 lg:justify-between flex-col lg:text-left text-center lg:mt-0 mt-8">
<div className="lg:w-[280px]">
<h4 className="font-extrabold text-base lg:text-2xl lg:block mb-4">
Encuentra recursos, guarda, y compartelos.
</h4>
</div>
{/* <div className="lg:text-left">
<ul className="flex flex-col gap-4">
<li className="text-green-600 font-bold">Titulo Footer</li>
<li className="cursor-pointer">Texto footer 1</li>
<li>Texto footer 2</li>
</ul>
</div> */}
{/* <div className="lg:text-left ">
<ul className="flex flex-col gap-4">
<li className="text-green-600 font-bold">Titulo Footer 2</li>
<li>Texto footer 1</li>
<li>Texto footer 2</li>
</ul>
</div> */}
{/* <div className="lg:text-left ">
<ul className="flex flex-col gap-4">
<li className="text-green-600 font-bold">Titulo Footer 3</li>
<li>Texto footer 1</li>
<li>Texto footer 2</li>
</ul>
</div> */}
</div>
<div className="flex lg:flex-row lg:justify-between flex-col items-center">
<p>Manuel Cebreiro.</p>
<div className="flex items-center w-[120px] lg:justify-between justify-evenly my-5">
<Link href={'https://github.com/ManuelCebreiro'} target="_blank" rel="noopener noreferrer">
<div className="flex items-center justify-center bg-white border-2 border-white rounded-full w-12 h-12 cursor-pointer">
<Image src={github} alt="github" className="w-10 h-10" />
</div>
</Link>
<Link href={'https://www.linkedin.com/in/manuelcebreiro'} target="_blank" rel="noopener noreferrer">
<div className="flex items-center justify-center bg-white border-2 border-white rounded-full w-12 h-12 cursor-pointer">
<Image src={linkedin} alt="github" className="w-12 h-12" />
</div>
</Link>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,73 @@
import { Button } from '@/components/atoms/Button'
import { signIn, useSession } from 'next-auth/react'
import { useRouter } from 'next/router'
import { SubmitHandler, useForm } from 'react-hook-form'
type FormData = {
email: string
password: string
}
export default function FormLogin() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm<FormData>()
const onSubmit: SubmitHandler<FormData> = (data) => {}
const router = useRouter()
const { data, status } = useSession()
if (status === 'loading') {
return <div>...loading</div>
}
status === 'authenticated' && router.push('/')
return (
<>
<div className="flex flex-col justify-center items-center py-48">
<h1 className="mb-5">Login</h1>
{/* <p className="mb-7 text-center">Estamos en proceso de añadir mas maneras de registrarse y logearse...</p> */}
{/* <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col justify-center items-center mb-5">
<div className="flex flex-col mb-4 w-60">
<label htmlFor="email">Email</label>
<input className="border-2 p-2" type="text" id="email" {...register('email')} />
<label htmlFor="password">Password</label>
<input className="border-2 p-2" type="password" id="password" {...register('password')} />
</div>
<Button
className="h-fit"
buttonClassName="flex justify-center w-full text-white content-center font-bold bg-teal-600 py-2 px-6"
>
Send
</Button>
</form> */}
{/* <h3 className="flex items-center w-60 mb-5">
<span className="flex-grow bg-gray-200 rounded h-1"></span>
<span className="mx-3 text-lg font-medium">or</span>
<span className="flex-grow bg-gray-200 rounded h-1"></span>
</h3>{' '} */}
<div className="flex flex-col gap-2">
<div className="flex w-60">
<Button
className="w-full bg-[#252424]"
buttonClassName="w-full flex justify-center text-white content-center font-bold px-5 py-4"
onClick={() => signIn('github')}
>
Sign in with Github
</Button>
</div>
<div className="flex w-60">
<Button
className="w-full bg-[#ff5555]"
buttonClassName="w-full flex justify-center text-white content-center font-bold px-5 py-4"
onClick={() => signIn('google')}
>
Sign in with Google
</Button>
</div>
</div>
</div>
</>
)
}

View File

@ -0,0 +1,123 @@
import { signOut, useSession } from 'next-auth/react'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { GiHamburgerMenu } from 'react-icons/gi'
import { GrClose } from 'react-icons/gr'
import iconWorldResource from '../../../public/images/devWorldResources-removebg.svg'
import { User } from '../../../type'
import { Button } from '../atoms/Button'
import { MenuNavbar } from '../molecules/MenuNavbar'
export function Navbar() {
const router = useRouter()
const [open, setOpen] = useState<Boolean>()
// const [users, setUsers] = useState<User[]>()
const [dataUserCurrent, setDataUserCurrent] = useState<User>()
const { status, data: session } = useSession()
useEffect(() => {
// Hago un get del usuario para traerme los datos de este.
const fetchUser = async () => {
try {
const response = await fetch(`/api/user?email=${session?.user?.email}`)
const userData = await response.json()
setDataUserCurrent(userData)
} catch (error: any) {
console.error('Error fetching user:', error.message)
}
}
if (session?.user?.email) {
fetchUser()
}
}, [session])
return (
<>
<div className="flex items-center justify-between w-full h-20 lg:py-4 lg:px-14 px-4 mb-1 bg-white">
<div className="w-40 h-14">
<Image
quality={100}
src={iconWorldResource}
alt="Home review"
width={100}
height={100}
className="object-contain cursor-pointer w-full"
onClick={() => {
router.push('/')
}}
/>
</div>
<span className="font-semibold text-xl">{}</span>
<MenuNavbar dataUserCurrent={dataUserCurrent} />
<div className="md:hidden">
<div
onClick={() => {
setOpen(!open)
}}
>
{open && <GrClose />}
{!open && <GiHamburgerMenu />}
</div>
{open ? (
<div className=" gap-3 absolute top-20 left-0 w-full">
<div className="w-full absolute bg-gray-100 flex flex-col items-center gap-5 p-4 z-30">
<Button
className="!w-full py-2 px-4 flex justify-center"
onClick={() => {
router.push('/')
}}
>
Home
</Button>
{dataUserCurrent?.isAdmin && (
<Button
className="!w-full py-2 px-4 flex justify-center"
onClick={() => {
router.push('/create')
}}
>
Create
</Button>
)}
{dataUserCurrent && (
<Button
className="!w-full py-2 px-4 flex justify-center"
onClick={() => {
router.push('/perfil')
}}
>
Perfil
</Button>
)}
{!dataUserCurrent && <Button className="!w-full py-2 px-4 flex justify-center">Register</Button>}
{status === 'authenticated' ? (
<Button
className="!w-full py-2 px-4 flex justify-center"
onClick={() => {
signOut()
}}
>
Logout
</Button>
) : (
<Button
buttonClassName="btn-primary-500 font-bold hover:opacity-50"
onClick={() => {
router.push('/login')
}}
>
Login
</Button>
)}
</div>
</div>
) : (
''
)}
</div>
</div>
</>
)
}

View File

@ -0,0 +1,156 @@
import Image from 'next/image'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { FaFilterCircleXmark } from 'react-icons/fa6'
import { Post } from '../../../type'
import AutoCompleteInput from '../atoms/AutoCompleteInput'
import { Button } from '../atoms/Button'
// type SectionCategoryProps = {
// onCategoryClick: (value: string) => void
// caterogySelect: string
// page: number
// selectedTags: string[]
// onTagClick: (value: string) => void
// }
export function SectionCategories({
onCategoryClick,
caterogySelect,
page
}: // selectedTags,
// onTagClick
// setSelectedTags
any) {
const [category, setCategory] = useState([])
const [textSearched, setTextSearched] = useState('')
const [posts, setPosts] = useState<Post[]>([])
// const [tags, setTags] = useState([])
const router = useRouter()
useEffect(() => {
const getData = async () => {
try {
const responseCategory = await fetch('api/categories', {
cache: 'no-store'
})
if (!responseCategory.ok) {
throw new Error('Failed')
}
const resultCategory = await responseCategory.json()
setCategory(resultCategory)
} catch (error) {
console.error(error)
}
}
getData()
}, [caterogySelect])
useEffect(() => {
const getData = async () => {
try {
const response = await fetch(`api/allPosts?category=${caterogySelect}`, {
cache: 'no-store'
})
// router.push(`?page=1`)
if (!response.ok) {
throw new Error('Failed')
}
const result = await response.json()
setPosts(result)
} catch (error) {
console.error(error)
}
}
getData()
}, [caterogySelect])
return (
<div className="flex flex-col justify-center items-center gap-6">
<div className="w-full flex flex-wrap justify-center lg:gap-3 gap-1">
{category?.map((cat: any) => (
<div
key={cat.id}
onClick={() => onCategoryClick(cat.slug)}
className={`cursor-pointer ${caterogySelect === cat.slug ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<div
className={`flex justify-between items-center px-4 py-2 w-fit h-12 rounded-xl border-2 border-gray-500 gap-2`}
style={{ backgroundColor: cat.color }}
>
<div className="w-8 h-8 ">
{/* bg-white rounded-full */}
<Image
src={`/images/${cat.img}.svg`}
width={32}
height={32}
alt="img post"
className="w-full h-full object-cover"
></Image>
</div>
<div>
<p className="font-bold ">{cat.title}</p>
</div>
</div>
</div>
))}
</div>
{caterogySelect && (
<div className="flex justify-center w-full md:px-8 md:gap-4 gap-2">
<AutoCompleteInput setTextSearched={setTextSearched} textSearched={textSearched} posts={posts} />
{caterogySelect && (
<Button
className="flex justify-center lg:z-0 "
buttonClassName="px-0 py-0 button"
onClick={() => {
// setSelectedTags('')
onCategoryClick('')
}}
disabled={!caterogySelect}
>
{/* <p className=" p-2">Quitar filtros</p> */}
<FaFilterCircleXmark className="w-6 h-6" />
</Button>
)}
</div>
)}
{/* {caterogySelect && (
<div className="flex flex-wrap gap-1">
{tags.map((tag) => (
<>
<Tag
key={tag.id}
color={tag.color}
name={tag.name}
onClick={() => {
console.log('Clicked')
onTagClick(tag.slug)
}}
className="cursor-pointer"
/>
<div
key={tag.id}
className={`flex justify-between items-center px-2 py-1 w-fit h-6 rounded-xl border-[1px] border-black border-solid gap-2 `}
style={{ backgroundColor: `${tag.color}80` }}
>
<div
className={`${
selectedTags.includes(tag.slug) ? 'opacity-20 cursor-not-allowed' : 'cursor-pointer'
} `}
onClick={() => {
console.log('Clicked')
onTagClick(tag.slug)
}}
>
<p className="text-xs font-bold ">{tag.name}</p>
</div>
</div>
</>
))}
</div>
)} */}
</div>
)
}

9
src/constants.ts Normal file
View File

@ -0,0 +1,9 @@
export const slugify = (str: string) => {
return str
.trim()
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-zA-Z0-9\s]/g, '')
.replace(/\s+/g, '-')
}

View File

@ -0,0 +1,18 @@
import { createContext, useContext, useState } from 'react'
const AppContext = createContext()
export const PostContext = ({ children }) => {
const [postData, setPostData] = useState([])
const updatePostData = (newData) => {
setPostData(newData)
}
return <AppContext.Provider value={{ postData, updatePostData }}>{children}</AppContext.Provider>
}
export const usePostContext = () => {
return useContext(AppContext)
}

View File

@ -0,0 +1,6 @@
import { NextRouter } from 'next/router'
export const languageRedirect = (router: NextRouter, language: string) => {
const { pathname, asPath, query } = router
router.push({ pathname, query }, asPath, { locale: language })
}

18
src/pages/_app.tsx Normal file
View File

@ -0,0 +1,18 @@
import AuthProvider from '@/providers/AuthProvider'
import '@/styles/globals.scss'
import { appWithTranslation } from 'next-i18next'
import type { AppProps } from 'next/app'
import { PostContext } from '../context/PostContext'
const i18nextConfig = require('../../next-i18next.config')
const App = ({ Component, pageProps }: AppProps) => {
return (
<PostContext>
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
</PostContext>
)
}
export default appWithTranslation(App, i18nextConfig)

14
src/pages/_document.tsx Normal file
View File

@ -0,0 +1,14 @@
import { Head, Html, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="es">
<Head>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}

View File

@ -0,0 +1,50 @@
import { NextApiRequest, NextApiResponse } from 'next'
import prisma from '../../../utils/connect'
export default async function allPosts(req: NextApiRequest, res: NextApiResponse) {
const { category } = req.query
if (req.method === 'GET') {
try {
let posts
if (category) {
const categoryFilter = typeof category === 'string' ? category : category[0]
posts = await prisma.post.findMany({
where: {
Category: {
slug: categoryFilter
}
},
orderBy: { createdAt: 'desc' },
include: {
Category: true,
Like: true,
comments: true,
Favorite: true
}
})
} else {
// Si no se proporciona una categoría, obtenemos todos los posts
posts = await prisma.post.findMany({
orderBy: { createdAt: 'desc' },
include: {
Category: true,
Like: true,
comments: true,
Favorite: true
}
})
}
res.status(200).json(posts)
} catch (error: any) {
console.error(error)
res.status(500).json({ message: 'Something went wrong', error: error.message })
}
} else {
res.setHeader('Allow', ['GET'])
res.status(405).end(`Method ${req.method} Not Allowed`)
}
}

View File

@ -0,0 +1,4 @@
import NextAuth from 'next-auth'
import { authOptions } from '../../../utils/auth'
export default NextAuth(authOptions)

View File

@ -0,0 +1,12 @@
import { NextApiRequest, NextApiResponse } from 'next'
import prisma from '../../utils/connect'
export default async function GET(req: NextApiRequest, res: NextApiResponse) {
try {
const categories = await prisma.category.findMany()
res.status(200).json(categories)
} catch (error: any) {
console.error(error)
res.status(500).json({ message: 'Something went wrong', error: error.message })
}
}

View File

@ -0,0 +1,40 @@
// import { NextApiRequest, NextApiResponse } from 'next'
// import { createComment, deleteComment, getAllComments } from '../../../../prisma/comment'
// export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// try {
// switch (req.method) {
// case 'GET': {
// const comments = await getAllComments(req.query.postSlug as string)
// console.log(comments, 'comments')
// res.status(200).json(comments)
// break
// }
// case 'POST': {
// console.log('EMPIEZA')
// // Manejar la lógica para el método POST
// // const session = await getAuthSession()
// console.log(req.body.session, 'session')
// await createComment(req.body, req.body.session?.user?.email, req.body.session)
// console.log('aqui llega?')
// res.status(200).json({ message: 'Comment created successfully' })
// break
// }
// case 'DELETE': {
// console.log('DELETE')
// await deleteComment(req.body.id)
// res.status(200).json({ message: 'Comment deleted successfully ' })
// }
// default:
// // Manejar otros métodos HTTP si es necesario
// res.status(405).json({ message: 'Method Not Allowed' })
// break
// }
// } catch (error: any) {
// console.error(error)
// const status = error.message === 'Not Authenticated' ? 401 : 500
// res.status(status).json({ message: 'Something went wrong', error: error.message })
// }
// }

View File

@ -0,0 +1,31 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { createComment, deleteComment, getAllComments } from '../../../../prisma/comment'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
switch (req.method) {
case 'GET': {
const comments = await getAllComments(req.query.postSlug as string)
res.status(200).json(comments)
break
}
case 'POST': {
await createComment(req.body, req.body.session?.user?.email, req.body.session)
res.status(200).json({ message: 'Comment created successfully' })
break
}
case 'DELETE': {
await deleteComment(req.body.id)
res.status(200).json({ message: 'Comment deleted successfully ' })
}
default:
res.status(405).json({ message: 'Method Not Allowed' })
break
}
} catch (error: any) {
console.error(error)
const status = error.message === 'Not Authenticated' ? 401 : 500
res.status(status).json({ message: 'Something went wrong', error: error.message })
}
}

View File

@ -0,0 +1,58 @@
import { addFavorite, deleteFavorite } from '../../../../prisma/favorite'
import { getUserByEmail } from '../../../../prisma/user'
export default async function handler(req, res) {
try {
switch (req.method) {
case 'POST': {
const { email, postId, session } = req.body
if (!email) {
res.status(400).json({ message: 'User email is required' })
return
}
if (!postId) {
res.status(400).json({ message: 'Post ID is required' })
return
}
const user = await getUserByEmail(email as string)
if (!user) {
res.status(404).json({ message: 'User not found' })
return
}
const newFavorite = await addFavorite(postId, user.email, session)
res.status(200).json(newFavorite)
break
}
case 'DELETE': {
const { email, postId, session } = req.body
if (!email) {
res.status(400).json({ message: 'User email is required' })
return
}
if (!postId) {
res.status(400).json({ message: 'Post ID is required' })
return
}
const user = await getUserByEmail(email as string)
if (!user) {
res.status(404).json({ message: 'User not found' })
return
}
try {
await deleteFavorite(postId, user.email, session)
res.status(200).json({ message: 'Favorite removed successfully.' })
} catch (error) {
res.status(500).json({ message: 'Error removing favorite', error: error.message })
}
break
}
default:
res.status(405).json({ message: 'Method Not Allowed' })
break
}
} catch (error: any) {
console.error(error)
const status = error.message === 'Not Authenticated' ? 401 : 500
res.status(status).json({ message: 'Something went wrong', error: error.message })
}
}

View File

@ -0,0 +1,60 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { addLike, deleteLike } from '../../../../prisma/like'
import { getUserByEmail } from '../../../../prisma/user'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
switch (req.method) {
case 'POST': {
const { email, postId, session } = req.body
if (!email) {
res.status(400).json({ message: 'User email is required' })
return
}
if (!postId) {
res.status(400).json({ message: 'Post ID is required' })
return
}
const user = await getUserByEmail(email as string)
if (!user) {
res.status(404).json({ message: 'User not found' })
return
}
const newLike = await addLike(postId, user.email, session)
res.status(200).json(newLike)
break
}
case 'DELETE': {
const { email, postId, session } = req.body
if (!email) {
res.status(400).json({ message: 'User email is required' })
return
}
if (!postId) {
res.status(400).json({ message: 'Post ID is required' })
return
}
const user = await getUserByEmail(email as string)
if (!user) {
res.status(404).json({ message: 'User not found' })
return
}
try {
await deleteLike(postId, user.email, session)
res.status(200).json({ message: 'Like eliminado correctamente.' })
} catch (error) {
res.status(500).json({ message: 'Error al quitar el like', error: error.message })
}
break
}
default:
res.status(405).json({ message: 'Method Not Allowed' })
break
}
} catch (error: any) {
console.error(error)
const status = error.message === 'Not Authenticated' ? 401 : 500
res.status(status).json({ message: 'Something went wrong', error: error.message })
}
}

39
src/pages/api/post.ts Normal file
View File

@ -0,0 +1,39 @@
import { NextApiRequest, NextApiResponse } from 'next'
import prisma from '../../utils/connect'
export default async function POST(req: NextApiRequest, res: NextApiResponse) {
const { userEmail, slug } = req.body
if (!userEmail) {
res.status(401).json({ error: 'Not Authenticated' })
return
}
const user = await prisma.user.findUnique({
where: {
email: userEmail
}
})
if (!user || !user.isAdmin) {
res.status(403).json({ error: 'Not Authorized', type: 'unAuthorized' })
return
}
try {
const existingPost = await prisma.post.findUnique({
where: {
slug: slug
}
})
if (existingPost) {
res.status(400).json({ error: 'Post with this slug already exists', type: 'duplicate' })
return
}
const post = await prisma.post.create({
data: { ...req.body }
})
res.status(200).json(post)
} catch (error) {
console.error('Error creating post:', error)
res.status(500).json({ error: 'Error creating post' })
}
}

View File

@ -0,0 +1,67 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { deletePost, editPost } from '../../../../prisma/post'
import prisma from '../../../utils/connect'
//GET SINGLE POST
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// try {
// const { slug } = req.query
// const post = await prisma.post.update({
// where: { slug: String(slug) },
// data: { views: { increment: 1 } },
// include: { user: true }
// // include: {
// // comments: {
// // include: {
// // user: true
// // }
// // }
// // }
// })
// // console.log(post, 'POST BACK')
// res.status(200).json(post)
// } catch (error: any) {
// console.error(error)
// res.status(500).json({ message: 'Something went wrong', error: error.message })
// }
try {
const { slug } = req.query
const { body } = req
switch (req.method) {
case 'GET': {
const post = await prisma.post.update({
where: { slug: String(slug) },
data: { views: { increment: 1 } },
include: {
user: true,
Category: true,
Like: true,
comments: true,
Favorite: true
// , Tags: true
}
})
res.status(200).json(post)
break
}
case 'DELETE': {
await deletePost(String(slug))
res.status(200).json({ message: 'Post deleted successfully' })
break
}
case 'PUT': {
await editPost(String(slug), body)
res.status(200).json({ message: 'Post updated successfully' })
break
}
default:
res.status(405).json({ message: 'Method Not Allowed' })
break
}
} catch (error: any) {
console.error(error)
res.status(500).json({ message: 'Something went wrong', error: error.message })
}
}

54
src/pages/api/posts.ts Normal file
View File

@ -0,0 +1,54 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { resolve } from 'url'
import prisma from '../../utils/connect'
export default async function GET(req: NextApiRequest, res: NextApiResponse) {
const url = resolve(`${process.env.NEXTAUTH_URL_INTERNAL}`, req.url || 'page=1') //pendiente de quitar /api/posts?page=1 ESTO ES UNA PRUEBA QUITALO SI NO VA
const { searchParams } = new URL(url)
const page = searchParams.get('page')
let cat = searchParams.get('cat')
// let tags = searchParams.get('tags')
const POST_PER_PAGE = 4
const pageNumber = page ? parseInt(page, 10) : 1
const query = {
take: POST_PER_PAGE,
skip: POST_PER_PAGE * (pageNumber - 1),
where: {
...(cat && { catSlug: cat })
// ...(tags && {
// Tags: {
// some: {
// slug: {
// in: tags.split(',')
// }
// }
// }
// })
},
orderBy: { createdAt: 'desc' },
include: {
Category: true,
Like: true,
comments: true,
Favorite: true
// Tags: true
}
}
try {
// const [posts, count] = await prisma.$transaction([
// prisma.post.findMany(query),
// prisma.post.count({ where: query.where })
// ])
const posts = await prisma.post.findMany({
...query,
orderBy: { createdAt: 'desc' }
})
const count = await prisma.post.count({ where: query.where })
res.status(200).json({ posts, count })
} catch (error: any) {
console.error(error)
res.status(500).json({ message: 'Something went wrong', error: error.message })
}
}

View File

@ -0,0 +1,45 @@
import { NextApiRequest, NextApiResponse } from 'next'
import prisma from '../../../utils/connect'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'GET') {
return res.status(405).json({ message: 'Method Not Allowed' })
}
const userEmail = req.query.userEmail as string
if (!userEmail) {
return res.status(400).json({ message: 'User email is required' })
}
try {
const user = await prisma.user.findUnique({
where: { email: userEmail },
include: {
Favorite: {
include: {
post: {
include: {
Like: true,
Favorite: true,
Category: true,
comments: true
// _count: true
}
}
}
}
}
})
if (!user) {
return res.status(404).json({ message: 'User not found' })
}
const favoritePosts = user.Favorite.map((favorite) => favorite.post)
return res.status(200).json(favoritePosts)
} catch (error: any) {
console.error(error)
res.status(500).json({ message: 'Something went wrong', error: error.message })
}
}

View File

@ -0,0 +1,45 @@
import { NextApiRequest, NextApiResponse } from 'next'
import prisma from '../../../utils/connect'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'GET') {
return res.status(405).json({ message: 'Method Not Allowed' })
}
const userEmail = req.query.userEmail as string
if (!userEmail) {
return res.status(400).json({ message: 'User email is required' })
}
try {
const user = await prisma.user.findUnique({
where: { email: userEmail },
include: {
Like: {
include: {
post: {
include: {
Like: true,
Category: true,
Favorite: true,
comments: true
// _count: true
}
}
}
}
}
})
if (!user) {
return res.status(404).json({ message: 'User not found' })
}
const favoritePosts = user.Like.map((favorite) => favorite.post)
return res.status(200).json(favoritePosts)
} catch (error: any) {
console.error(error)
res.status(500).json({ message: 'Something went wrong', error: error.message })
}
}

25
src/pages/api/share.ts Normal file
View File

@ -0,0 +1,25 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method Not Allowed' })
}
const { postId, platform } = req.body
try {
const updatedPost = await prisma.post.update({
where: { id: postId },
data: {
whatsappShareCount: platform === 'whatsapp' ? { increment: 1 } : undefined,
twitterShareCount: platform === 'twitter' ? { increment: 1 } : undefined
}
})
return res.status(200).json({ message: 'Post shared successfully', post: updatedPost })
} catch (error) {
console.error('Error sharing post:', error)
return res.status(500).json({ message: 'Internal Server Error' })
}
}

View File

@ -0,0 +1,22 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { getAllStats } from '../../../../prisma/stats'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { method } = req
switch (method) {
case 'GET':
const stats = await getAllStats()
res.status(200).json(stats)
break
default:
res.status(405).json({ message: 'Method Not Allowed' })
break
}
} catch (error: any) {
console.error(error)
res.status(500).json({ message: 'Something went wrong', error: error.message })
}
}

Some files were not shown because too many files have changed in this diff Show More