63 Commits

Author SHA1 Message Date
SrJuggernaut 4e03c43ded fix: session api update 2024-07-30 18:20:11 -06:00
SrJuggernaut c979e6540f refactor: team applications 2024-07-30 18:08:53 -06:00
SrJuggernaut 7a87eac395 chore: update dependencies 2024-07-30 18:08:28 -06:00
SrJuggernaut ff0d24bbb6 feat: static applications dashboard 2024-02-16 13:08:56 -06:00
SrJuggernaut ddeed0a6ef fix: missing helper texts 2024-02-08 15:54:31 -06:00
SrJuggernaut e105edbbee fix: session related bugs 2024-02-08 15:37:57 -06:00
SrJuggernaut 9878ca7f1c feat: updated dependencies &package name 2024-02-06 20:27:24 -06:00
SrJuggernaut 7d5c0aeea9 chore: updated old formik forms to use hooks 2024-02-06 20:25:27 -06:00
SrJuggernaut 7f758d9d0b feat: updated cuenta page 2024-02-06 20:19:02 -06:00
SrJuggernaut 820865ac79 feat: useManageErrors hook 2024-02-06 20:14:59 -06:00
SrJuggernaut 345f79e53a feat: equipo pages now uses data from database package 2024-02-06 14:52:29 -06:00
SrJuggernaut 9403b28040 fix: login form recuperar contraseña 2024-02-06 14:47:46 -06:00
SrJuggernaut c21617aec3 feat: session with current user 2024-02-06 12:50:00 -06:00
SrJuggernaut 7696c4f371 fix: remove not longer used fonts 2024-02-05 14:10:43 -06:00
SrJuggernaut 84f02c9bc5 fix: api team-applications route 2024-02-05 14:08:46 -06:00
SrJuggernaut 0042cbee31 fix: fix image config 2024-02-05 12:40:00 -06:00
SrJuggernaut 71c14fee52 chore: updated dependencies 2024-01-27 14:08:36 -06:00
SrJuggernaut 21bd696a30 feat: recover password flow 2024-01-24 11:28:01 -06:00
SrJuggernaut 5f9b972983 feat: http verbs 2024-01-23 17:02:11 -06:00
SrJuggernaut 57f5f80969 feat: basic apply form 2024-01-18 22:23:21 -06:00
SrJuggernaut 984799d502 feat: metadataBase 2024-01-12 11:09:02 -06:00
SrJuggernaut 8d8b5e1646 feat: session 2024-01-11 20:57:17 -06:00
SrJuggernaut 4f37fd4734 fix: equipo div inside p 2024-01-05 15:18:23 -06:00
SrJuggernaut b7e273ae06 feat: state redux toolkit & feedback slice 2024-01-05 15:08:11 -06:00
SrJuggernaut ab82d0797d fix: remove unused logs 2024-01-04 22:50:43 -06:00
SrJuggernaut 8c95537324 feat: static login & register pages 2024-01-04 22:24:58 -06:00
SrJuggernaut a6e072703d feat: update dependencies 2024-01-04 22:11:10 -06:00
SrJuggernaut 7d39bb3d89 feat: improve ui components 2024-01-04 21:46:10 -06:00
SrJuggernaut 0c74c0a0a9 feat: move appwrite to entgamers-database package 2024-01-03 17:31:22 -06:00
SrJuggernaut b393e0cdb0 feat: static equipo unirse 2024-01-03 15:03:08 -06:00
SrJuggernaut ba466dfd80 chore: update dependencies 2024-01-01 18:34:42 -06:00
SrJuggernaut ee8bf42aad chore: moving from yarn to bun 2024-01-01 17:52:41 -06:00
SrJuggernaut b2756b0654 chore: remove old mui types 2024-01-01 17:52:10 -06:00
SrJuggernaut b01e211acb feat: static unirse 2023-10-06 12:27:28 -06:00
SrJuggernaut 33a3e7bb70 feat: static pages 2023-09-19 21:20:33 -06:00
SrJuggernaut a8c579b94e feat: static homepage 2023-09-18 14:29:54 -06:00
SrJuggernaut bde70454dc feat: nextjs13 boilerplate 2023-09-17 13:41:24 -06:00
SrJuggernaut 2135a4b55d feat: start over and layout 2023-09-16 21:48:09 -06:00
SrJuggernaut eb334f6357 feat: eslint update 2023-09-15 12:36:07 -06:00
SrJuggernaut 14b52a7800 fix: remove bundle analyzer 2023-09-14 21:15:53 -06:00
SrJuggernaut f11ae1c4f3 revert: revert to yarn to deploy 2023-09-14 21:08:33 -06:00
SrJuggernaut f5a9a88f84 fix: pm2 script 2023-09-14 14:52:25 -06:00
SrJuggernaut f59a7cc091 fix: interpreter to use pm2 2 2023-09-14 14:49:04 -06:00
SrJuggernaut 3a831acaee fix: interpreter to use pm2 2023-09-14 14:39:51 -06:00
SrJuggernaut b514c6bc6f fix: bun doesnt run on pm2 2023-09-14 14:21:14 -06:00
SrJuggernaut ce87aa5ee3 feat: moving deployments to bun 2023-09-14 14:14:12 -06:00
SrJuggernaut b26d5d8eba chore: updated dependencies 2023-09-14 13:01:22 -06:00
SrJuggernaut 390f8bc858 feat: riot review 2023-07-11 15:46:34 -06:00
SrJuggernaut d926d3a5ef fix: github action name, wrong triggers 2023-01-04 15:47:45 -06:00
SrJuggernaut fdefa84ec7 fix: github action naming 2023-01-04 15:46:14 -06:00
SrJuggernaut 9db5e5cebf feat: setup github action to run on new server 2023-01-04 15:44:18 -06:00
SrJuggernaut 35105441cb docs: deploy env vars 2022-10-09 13:56:41 -05:00
SrJuggernaut 7986456b9c fix: production git ref 2022-10-09 13:20:42 -05:00
SrJuggernaut f9aea7eea4 fix: glass text contrast 2022-10-01 13:34:31 -05:00
SrJuggernaut a68098e7e2 ci: use yarn instead npm 2022-10-01 13:33:29 -05:00
SrJuggernaut 864ff91255 fix: server use isomorphic-fetch 2022-09-30 19:32:40 -05:00
SrJuggernaut c69a166c1f revert: deploy as sudo
Refs: 624b225
2022-09-30 18:23:38 -05:00
SrJuggernaut 731c9b8962 fix: pass only required env to deploy 2022-09-30 17:50:41 -05:00
SrJuggernaut 624b2251c4 feat: post deploy as sudo 2022-09-30 13:57:27 -05:00
SrJuggernaut b73cc51e08 fix: pass environment to actions 2022-09-30 13:41:56 -05:00
SrJuggernaut a35e99f8ff ci: deploy using pm2
* ci: pm2 configuration file

* ci: github action deploy preview

* ci: github action deploy production

* ci: env variables now pass to pm2
2022-09-29 21:54:58 -05:00
SrJuggernaut 4f8c4f6492 docs: initial documentation 2022-09-28 18:09:10 -05:00
SrJuggernaut c3dae929c6 feat: static site
* feat: mui support & basic theming

* feat: entgamers favicon

* feat: public images until dynamic content can be used

* feat: entgamers & gaming assets

* feat: eslint extra rules

* feat: mui theme modifications

* feat: fontawesome, gsap, bundle analyzer

* feat: common interfaces

* feat: basic layout

* chore: upadted dependencies

* chore: updated dependencies

* feat: updated link styles

* feat: layout now have better interfaces

* feat: basic seo component

* feat: static website

* feat: env variable rules in .gitignore

* feat: added lint to pre-commit
2022-09-26 12:01:26 -05:00
181 changed files with 4667 additions and 6254 deletions
+11 -1
View File
@@ -1,7 +1,6 @@
# App variables
SITE_NAME="EntGamers"
DISCORD_JOIN_WEBHOOK_URL="https://discord.com/api/webhooks/XXXXXXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
# Deployment variables
@@ -15,3 +14,14 @@ DEPLOY_PATH=""
SSH_PRIVATE_KEY=""
SSH_KNOWN_HOSTS=""
# Appwrite required variables
NEXT_PUBLIC_APPWRITE_ENDPOINT=""
NEXT_PUBLIC_APPWRITE_PROJECT_ID=""
APPWRITE_API_KEY=""
# Website Variables
NEXT_PUBLIC_SITE_URL="https://entgamers.com"
IMAGE_DOMAINS="https://domain.com,http://another.domain.com/route/"
+1
View File
@@ -0,0 +1 @@
src/styled-system
+30 -27
View File
@@ -1,28 +1,33 @@
{
"env": {
"browser": true,
"es2021": true
"es2021": true,
"node": true
},
"extends": [
"plugin:react/recommended",
"standard",
"plugin:@next/next/recommended"
"next/core-web-vitals",
"standard-with-typescript",
"plugin:react/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react",
"@emotion",
"@typescript-eslint"
"react"
],
"rules": {
"indent": [
"react/react-in-jsx-scope": "off",
"react/jsx-uses-react": "off",
"indent": "off",
"@typescript-eslint/indent": [
"error",
2,
{
"SwitchCase": 1
}
],
"react/jsx-indent": [
"error",
2
],
@@ -30,23 +35,21 @@
"@typescript-eslint/no-use-before-define": [
"error"
],
"react/react-in-jsx-scope": "off",
"react/jsx-filename-extension": [
"warn",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
"extensions": [
".tsx"
]
"argsIgnorePattern": "^_"
}
],
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error",
"@emotion/pkg-renaming": "error",
"react/no-unknown-property": ["error", { "ignore": ["css"] }]
},
"settings": {
"react": {
"version": "detect"
}
"react-hooks/exhaustive-deps": "off",
"react/no-unknown-property": [
"error",
{
"ignore": [
"css"
]
}
]
}
}
+5 -15
View File
@@ -10,22 +10,13 @@ jobs:
environment: preview
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v2
- uses: oven-sh/setup-bun@v1
- uses: kielabokkie/ssh-key-and-known-hosts-action@v1
with:
node-version: "16"
- name: Setup Host SSH
run: |
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
shell: bash
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-host: ${{ secrets.DEPLOY_HOST }}
- name: Install pm2
run: npm install pm2 --location=global
run: bun install pm2 -g
shell: bash
- name: Deploy using pm2
run: pm2 deploy ecosystem.config.js preview
@@ -33,7 +24,6 @@ jobs:
# Deploy environment variables
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
SSH_USERNAME: ${{ secrets.SSH_USERNAME }}
SSH_HOST: ${{ secrets.SSH_HOST }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
# App environment variables
APP_NAME: ${{ secrets.APP_NAME }}
+5 -15
View File
@@ -10,22 +10,13 @@ jobs:
environment: production
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v2
- uses: oven-sh/setup-bun@v1
- uses: kielabokkie/ssh-key-and-known-hosts-action@v1
with:
node-version: "16"
- name: Setup Host SSH
run: |
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
shell: bash
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-host: ${{ secrets.DEPLOY_HOST }}
- name: Install pm2
run: npm install pm2 --location=global
run: bun install pm2 -g
shell: bash
- name: Deploy using pm2
run: pm2 deploy ecosystem.config.js production
@@ -33,7 +24,6 @@ jobs:
# Deploy environment variables
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
SSH_USERNAME: ${{ secrets.SSH_USERNAME }}
SSH_HOST: ${{ secrets.SSH_HOST }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
# App environment variables
APP_NAME: ${{ secrets.APP_NAME }}
+5 -15
View File
@@ -8,22 +8,13 @@ jobs:
environment: preview
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v2
- uses: oven-sh/setup-bun@v1
- uses: kielabokkie/ssh-key-and-known-hosts-action@v1
with:
node-version: "16"
- name: Setup Host SSH
run: |
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
shell: bash
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-host: ${{ secrets.DEPLOY_HOST }}
- name: Install pm2
run: npm install pm2 --location=global
run: bun install pm2 -g
shell: bash
- name: Deploy using pm2
run: pm2 deploy ecosystem.config.js preview setup
@@ -31,7 +22,6 @@ jobs:
# Deploy environment variables
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
SSH_USERNAME: ${{ secrets.SSH_USERNAME }}
SSH_HOST: ${{ secrets.SSH_HOST }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
# App environment variables
APP_NAME: ${{ secrets.APP_NAME }}
+5 -15
View File
@@ -8,22 +8,13 @@ jobs:
environment: production
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v2
- uses: oven-sh/setup-bun@v1
- uses: kielabokkie/ssh-key-and-known-hosts-action@v1
with:
node-version: "16"
- name: Setup Host SSH
run: |
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
shell: bash
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-host: ${{ secrets.DEPLOY_HOST }}
- name: Install pm2
run: npm install pm2 --location=global
run: bun install pm2 -g
shell: bash
- name: Deploy using pm2
run: pm2 deploy ecosystem.config.js production setup
@@ -31,7 +22,6 @@ jobs:
# Deploy environment variables
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
SSH_USERNAME: ${{ secrets.SSH_USERNAME }}
SSH_HOST: ${{ secrets.SSH_HOST }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
# App environment variables
APP_NAME: ${{ secrets.APP_NAME }}
+165 -4
View File
@@ -1,5 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Created by https://www.toptal.com/developers/gitignore/api/node,yarn,nextjs
# Edit at https://www.toptal.com/developers/gitignore?templates=node,yarn,nextjs
### NextJS ###
# dependencies
/node_modules
/.pnp
@@ -26,12 +28,171 @@ yarn-error.log*
.pnpm-debug.log*
# local env files
.env
.env.*
!.env.example
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
### Node ###
# Logs
logs
*.log
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
### yarn ###
# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
.yarn/*
!.yarn/releases
!.yarn/patches
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
# if you are NOT using Zero-installs, then:
# comment the following lines
# !.yarn/cache
# and uncomment the following lines
# .pnp.*
# End of https://www.toptal.com/developers/gitignore/api/node,yarn,nextjs
# Panda Css
src/styled-system
# Vscode
.vscode
+5
View File
@@ -0,0 +1,5 @@
{
"plugins": {
"@pandacss/dev/postcss": {}
}
}
Executable
BIN
View File
Binary file not shown.
+4 -6
View File
@@ -2,8 +2,8 @@ module.exports = {
apps: [
{
name: process.env.APP_NAME || 'entgamers-website',
script: 'npm',
args: 'start',
script: 'yarn',
args: 'run start',
env: {
NODE_ENV: 'production',
PORT: process.env.PORT || 3000,
@@ -19,8 +19,7 @@ module.exports = {
ref: 'origin/production',
repo: 'https://github.com/SrJuggernaut/entgamers_pro',
path: process.env.DEPLOY_PATH,
'pre-deploy': 'yarn install && yarn run build',
'post-deploy': 'pm2 startOrRestart ecosystem.config.js',
'post-deploy': 'pm2 --silent startOrRestart ecosystem.config.js',
env: {
APP_NAME: process.env.APP_NAME,
PORT: process.env.PORT,
@@ -34,8 +33,7 @@ module.exports = {
ref: 'origin/preview',
repo: 'https://github.com/SrJuggernaut/entgamers_pro',
path: process.env.DEPLOY_PATH,
'pre-deploy': 'yarn install && yarn run build',
'post-deploy': 'pm2 startOrRestart ecosystem.config.js',
'post-deploy': 'pm2 --silent startOrRestart ecosystem.config.js',
env: {
APP_NAME: process.env.APP_NAME,
PORT: process.env.PORT,
+11 -7
View File
@@ -1,11 +1,15 @@
/** @type {import('next').NextConfig} */
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
openAnalyzer: false
const imageDomains = (process.env.IMAGE_DOMAINS ?? '').split(',').map(domain => {
const getDataRegex = /(?<protocol>[\w]+)?:\/\/(?<hostname>[\w.-]+)?((?<=[\d]{0,4}):(?<port>[\d]{0,4}))?\/?(?<pathname>.*)?$/
const groups = getDataRegex.exec(domain).groups ?? {}
return groups
})
const nextConfig = withBundleAnalyzer({
reactStrictMode: true
})
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
remotePatterns: imageDomains
}
}
module.exports = nextConfig
+38 -30
View File
@@ -1,52 +1,60 @@
{
"name": "next-template",
"name": "entgamers_pro",
"version": "0.1.0",
"private": true,
"scripts": {
"develop": "next dev",
"build": "next build",
"prestart": "bun install && next build",
"start": "next start",
"lint": "next lint",
"postinstall": "husky install"
"prepare": "panda codegen && husky install"
},
"dependencies": {
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@fortawesome/fontawesome-svg-core": "^6.2.0",
"@fortawesome/free-brands-svg-icons": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fontsource/open-sans": "^5.0.20",
"@fontsource/permanent-marker": "^5.0.8",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-brands-svg-icons": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@mui/material": "^5.10.6",
"formik": "^2.2.9",
"gsap": "^3.11.1",
"@reduxjs/toolkit": "^2.0.1",
"@tanstack/match-sorter-utils": "^8.11.8",
"@tanstack/react-table": "^8.19.3",
"appwrite": "^13.0.1",
"date-fns": "^3.3.1",
"entgamers-database": "0.0.26",
"entgamers-panda-preset": "0.1.5",
"formik": "^2.4.5",
"framer-motion": "^10.17.6",
"isomorphic-fetch": "^3.0.0",
"next": "12.3.1",
"next-connect": "^0.13.0",
"next": "^14.0.4",
"node-appwrite": "^11.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"sharp": "^0.31.0",
"swiper": "^8.4.2",
"yup": "^0.32.11"
"react-redux": "^9.0.4",
"sharp": "^0.33.1",
"yup": "^1.3.3"
},
"devDependencies": {
"@commitlint/cli": "^17.1.2",
"@commitlint/config-conventional": "^17.1.0",
"@emotion/eslint-plugin": "^11.10.0",
"@next/bundle-analyzer": "^12.3.1",
"@types/isomorphic-fetch": "^0.0.36",
"@types/node": "18.7.18",
"@types/react": "18.0.20",
"@typescript-eslint/eslint-plugin": "^5.38.0",
"@commitlint/cli": "^18.4.4",
"@commitlint/config-conventional": "^18.4.4",
"@pandacss/dev": "^0.23.0",
"@types/isomorphic-fetch": "^0.0.39",
"@types/node": "^20.10.6",
"@types/react": "^18.2.46",
"@types/react-dom": "^18.2.18",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^5.38.0",
"eslint": "^8.23.1",
"eslint-config-next": "12.3.1",
"eslint": "^8.0.1",
"eslint-config-next": "latest",
"eslint-config-standard": "^17.0.0",
"eslint-config-standard-with-typescript": "latest",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-n": "^15.2.5",
"eslint-plugin-n": "^15.0.0 || ^16.0.0 ",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.0.1",
"eslint-plugin-react": "^7.31.8",
"husky": "^8.0.1",
"typescript": "4.8.3"
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "latest",
"husky": "^8.0.3",
"typescript": "*"
}
}
+12
View File
@@ -0,0 +1,12 @@
import { defineConfig } from '@pandacss/dev'
export default defineConfig({
presets: ['entgamers-panda-preset'],
gitignore: true,
preflight: true,
include: ['./src/**/*.{js,jsx,ts,tsx}'],
exclude: [],
outdir: 'src/styled-system',
jsxFactory: 'panda',
jsxFramework: 'react'
})
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Before

Width:  |  Height:  |  Size: 315 KiB

After

Width:  |  Height:  |  Size: 315 KiB

Before

Width:  |  Height:  |  Size: 362 KiB

After

Width:  |  Height:  |  Size: 362 KiB

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 314 KiB

Before

Width:  |  Height:  |  Size: 305 KiB

After

Width:  |  Height:  |  Size: 305 KiB

Before

Width:  |  Height:  |  Size: 447 KiB

After

Width:  |  Height:  |  Size: 447 KiB

Before

Width:  |  Height:  |  Size: 398 KiB

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 208 B

After

Width:  |  Height:  |  Size: 208 B

Before

Width:  |  Height:  |  Size: 660 B

After

Width:  |  Height:  |  Size: 660 B

Before

Width:  |  Height:  |  Size: 666 B

After

Width:  |  Height:  |  Size: 666 B

Before

Width:  |  Height:  |  Size: 630 B

After

Width:  |  Height:  |  Size: 630 B

Before

Width:  |  Height:  |  Size: 255 B

After

Width:  |  Height:  |  Size: 255 B

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 269 B

Before

Width:  |  Height:  |  Size: 270 B

After

Width:  |  Height:  |  Size: 270 B

Before

Width:  |  Height:  |  Size: 270 B

After

Width:  |  Height:  |  Size: 270 B

Before

Width:  |  Height:  |  Size: 268 B

After

Width:  |  Height:  |  Size: 268 B

Before

Width:  |  Height:  |  Size: 253 B

After

Width:  |  Height:  |  Size: 253 B

Before

Width:  |  Height:  |  Size: 270 B

After

Width:  |  Height:  |  Size: 270 B

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 269 B

Before

Width:  |  Height:  |  Size: 266 B

After

Width:  |  Height:  |  Size: 266 B

Before

Width:  |  Height:  |  Size: 266 B

After

Width:  |  Height:  |  Size: 266 B

Before

Width:  |  Height:  |  Size: 461 B

After

Width:  |  Height:  |  Size: 461 B

Before

Width:  |  Height:  |  Size: 438 B

After

Width:  |  Height:  |  Size: 438 B

Before

Width:  |  Height:  |  Size: 467 B

After

Width:  |  Height:  |  Size: 467 B

Before

Width:  |  Height:  |  Size: 465 B

After

Width:  |  Height:  |  Size: 465 B

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File
+68
View File
@@ -0,0 +1,68 @@
import Typography from '@/components/ui/Typography'
import { css, cx } from '@/styled-system/css'
import { Center } from '@/styled-system/jsx'
import { center, container } from '@/styled-system/patterns'
import { button, card } from '@/styled-system/recipes'
import NextImage from 'next/image'
import NextLink from 'next/link'
import { type FC } from 'react'
const Clanes: FC = () => {
return (
<section
id="clanes"
className={cx(center({}), css({
minHeight: '75vh',
backgroundImage: 'url(/images/backgrounds/bricks.png)'
}))}
>
<div
className={cx(card({ variant: 'glass' }).body, container({}))}
>
<div
className={card({ variant: 'glass' }).content}
>
<Typography variant="h2" align='center'>Clanes</Typography>
<div
className={css({
display: 'grid',
alignItems: 'center',
gridTemplateColumns: { base: '1fr 1fr', smDown: '1fr' },
gap: 'medium'
})}
>
<div
className={css({
order: { base: 0, smDown: 1 }
})}
>
<Typography variant="body1">Los clanes son espacios donde compartir nuestros gustos con otros usuarios, dándonos la oportunidad de organizar proyectos y eventos en los cuales formar parte.</Typography>
<Center>
<NextLink
className={button()}
href="/clanes"
>
Ver Clanes
</NextLink>
</Center>
</div>
<div
className={css({
order: { base: 1, smDown: 0 }
})}
>
<NextImage
src="/images/Clanes.png"
alt="Clanes"
width={1200}
height={630}
/>
</div>
</div>
</div>
</div>
</section>
)
}
export default Clanes
+78
View File
@@ -0,0 +1,78 @@
'use client'
import IconButton from '@/components/ui/IconButton'
import Typography from '@/components/ui/Typography'
import { useAppDispatch } from '@/hooks/useAppDispatch'
import { useAppSelector } from '@/hooks/useAppSelector'
import { removeAlert } from '@/state/feedbackSlice'
import { css } from '@/styled-system/css'
import { alert } from '@/styled-system/recipes/alert'
import { faTimes } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { AnimatePresence, motion } from 'framer-motion'
import { type FC } from 'react'
import { createPortal } from 'react-dom'
const FeedbackConsumer: FC = () => {
const { alerts } = useAppSelector(state => state.feedback)
const dispatch = useAppDispatch()
return (
<>
{alerts.length > 0 && createPortal(
(
<AnimatePresence>
<motion.div
key="alerts"
className={css({
display: 'flex',
flexDirection: 'column',
gap: 'medium',
position: 'fixed',
bottom: 'medium',
left: 'medium',
padding: 'medium',
zIndex: 'modalBackdrop',
width: 'calc(100vw - 32px)',
maxWidth: '400px'
})}
initial={{ opacity: 0 }}
animate={{ opacity: 1, y: 0, transition: { duration: 0.3, ease: 'backIn' } }}
exit={{ opacity: 0, x: 400, transition: { duration: 0.3, ease: 'backOut' } }}
>
<AnimatePresence>
{alerts.map((currentAlert) => (
<motion.div
key={currentAlert.id}
// This is a workaround for PandaCSS to auto-generate styles and avoid Alerts with non-generated styles. See https://panda-css.com/docs/guides/dynamic-styling#runtime-conditions
className={alert({
severity: currentAlert.severity === 'success' ? 'success' : currentAlert.severity === 'info' ? 'info' : currentAlert.severity === 'warning' ? 'warning' : currentAlert.severity === 'error' ? 'error' : undefined
}).body}
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0, transition: { duration: 0.3, ease: 'backIn' } }}
exit={{ opacity: 0, x: 400, transition: { duration: 0.3, ease: 'backOut' } }}
>
<IconButton
size="small"
className={alert().closeButton}
onClick={() => dispatch(removeAlert(currentAlert.id))}
>
<FontAwesomeIcon icon={faTimes} fixedWidth size='sm'/>
</IconButton>
<Typography variant="h3" component="div">
{currentAlert.title}
</Typography>
<Typography variant="body1">
{currentAlert.message}
</Typography>
</motion.div>
))}
</AnimatePresence>
</motion.div>
</AnimatePresence>
),
document.body,
'alerts'
)}
</>
)
}
export default FeedbackConsumer
+132
View File
@@ -0,0 +1,132 @@
import Typography from '@/components/ui/Typography'
import { css, cx } from '@/styled-system/css'
import { Center, Container } from '@/styled-system/jsx'
import { iconButton } from '@/styled-system/recipes'
import { faArrowDown } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import NextImage from 'next/image'
import { type FC } from 'react'
const layerCss = css({
backgroundPositionY: 'bottom',
backgroundPositionX: 'x-start',
backgroundRepeat: 'repeat',
backgroundSize: 'initial',
height: '100vh',
width: '100%',
willChange: 'background-position-y',
animationName: 'bgMotion',
animationTimingFunction: 'linear',
animationIterationCount: 'infinite'
})
const Hero: FC = () => {
return (
<section
className={cx(layerCss, css({
backgroundImage: 'url(/images/backgrounds/MysteriousForestNightLayer01.png)',
marginTop: '-76px',
animationDuration: '175s',
position: 'relative'
}))}
>
<div
className={cx(layerCss, css({
backgroundImage: 'url(/images/backgrounds/MysteriousForestNightLayer02.png)',
animationDuration: '150s'
}))}
>
<div
className={cx(layerCss, css({
backgroundImage: 'url(/images/backgrounds/MysteriousForestNightLayer03.png)',
animationDuration: '125s'
}))}
>
<div
className={cx(layerCss, css({
backgroundImage: 'url(/images/backgrounds/MysteriousForestNightLayer04.png)',
animationDuration: '100s'
}))}
>
<div
className={cx(layerCss, css({
backgroundImage: 'url(/images/backgrounds/MysteriousForestNightLayer05.png)',
animationDuration: '75s'
}))}
>
<Center
className={cx(layerCss, css({
backgroundImage: 'url(/images/backgrounds/MysteriousForestNightLayer06.png)',
animationDuration: '50s'
}))}
>
<Container
className={css({
display: 'grid',
gridTemplateColumns: { base: '1fr 1fr', smDown: '1fr' },
alignItems: 'center'
})}
>
<div
className={css({
order: { base: 0, smDown: 1 }
})}
>
<Typography
variant="h1"
align="center"
>
EntGamers
</Typography>
<Typography
variant="h2"
align="center"
color="text"
>
Comunidad de y para los gamers
</Typography>
</div>
<div
className={css({
order: { base: 1, smDown: 0 }
})}
>
<NextImage
src="/images/EntGamers.png"
alt="EntGamers"
width={500}
height={500}
priority
/>
</div>
</Container>
</Center>
</div>
</div>
</div>
</div>
<a
href="#clanes"
className={cx(iconButton({
color: 'primary',
size: 'large'
}), css({
position: 'absolute',
bottom: '45px',
right: '50%',
animationName: 'bounce',
animationDuration: '1s',
animationIterationCount: 'infinite',
transform: 'translateX(50%)',
zIndex: 1,
'&:hover': {
animationPlayState: 'paused'
}
}))}
>
<FontAwesomeIcon icon={faArrowDown} size='lg' fixedWidth />
</a>
</section>
)
}
export default Hero
+61
View File
@@ -0,0 +1,61 @@
'use client'
import { useAppDispatch } from '@/hooks/useAppDispatch'
import { useAppSelector } from '@/hooks/useAppSelector'
import { setClanes, setCurrentUser, setSession, setStatus } from '@/state/sessionSlice'
import { AppwriteException } from 'appwrite'
import { getClanes } from 'entgamers-database/frontend/clanes'
import { getCurrentUser, getSession } from 'entgamers-database/frontend/session'
import { useCallback, useEffect, type FC } from 'react'
const SessionConsumer: FC = () => {
const { status, session, user, clanes } = useAppSelector((state) => state.session)
const dispatch = useAppDispatch()
const ensureSession = useCallback(async () => {
try {
if (status !== 'initializing' || session !== undefined) return
dispatch(setStatus('loading'))
const currentSession = await getSession('current')
const currentUser = await getCurrentUser()
dispatch(setSession(currentSession))
dispatch(setCurrentUser(currentUser))
} catch (error) {
dispatch(setSession())
dispatch(setCurrentUser())
throw error
} finally {
dispatch(setStatus('idle'))
}
}, [])
useEffect(() => {
ensureSession()
.catch((error) => {
if (error instanceof AppwriteException) {
console.error(error)
}
})
}, [])
useEffect(() => {
if (user !== undefined && clanes === undefined) {
getClanes()
.then((clanes) => {
dispatch(setClanes(clanes))
})
.catch((error) => {
if (error instanceof AppwriteException) {
console.error(error)
}
})
} else if (user === undefined && clanes !== undefined) {
dispatch(setClanes())
}
}, [user])
return (
<>
</>
)
}
export default SessionConsumer
+75
View File
@@ -0,0 +1,75 @@
import Typography from '@/components/ui/Typography'
import { css, cx } from '@/styled-system/css'
import { Center } from '@/styled-system/jsx'
import { container } from '@/styled-system/patterns'
import { button, card } from '@/styled-system/recipes'
import { type FC } from 'react'
const layerCss = css({
backgroundPositionY: 'bottom',
backgroundPositionX: 'x-start',
backgroundRepeat: 'repeat',
backgroundSize: 'initial',
minHeight: '75vh',
width: '100%',
willChange: 'background-position-y',
animationName: 'bgMotion',
animationTimingFunction: 'linear',
animationIterationCount: 'infinite'
})
const Social: FC = () => {
return (
<section
className={css({
backgroundImage: 'url(/images/backgrounds/SkyNightLayer01.png)',
backgroundPositionY: 'center',
backgroundPositionX: 'center'
})}
>
<div
className={cx(layerCss, css({
backgroundImage: 'url(/images/backgrounds/SkyNightLayer02.png)',
animationDuration: '150s'
}))}
>
<div
className={cx(layerCss, css({
backgroundImage: 'url(/images/backgrounds/SkyNightLayer03.png)',
animationDuration: '125s'
}))}
>
<Center
className={cx(layerCss, css({
backgroundImage: 'url(/images/backgrounds/SkyNightLayer04.png)',
animationDuration: '100s'
}))}
>
<div
className={cx(card({ variant: 'glass' }).body, container({}))}
>
<div
className={card({ variant: 'glass' }).content}
>
<Typography variant="h2" align='center'>Redes Sociales</Typography>
<Typography variant="body1">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptate deleniti dolore quas sed nemo sit, officia in rem nesciunt quisquam possimus ab! Labore sed reprehenderit quae, hic earum tempora placeat cumque id eos itaque perferendis nulla officia fuga porro, quis, unde facere accusamus repudiandae non?
</Typography>
<Center>
<a
className={button()}
href="/links"
>
Nuestros Links
</a>
</Center>
</div>
</div>
</Center>
</div>
</div>
</section>
)
}
export default Social
+19
View File
@@ -0,0 +1,19 @@
'use client'
import store from '@/state/store'
import { type FC, type ReactNode } from 'react'
import { Provider } from 'react-redux'
export interface StateProviderProps {
children: ReactNode
}
const StateProvider: FC<StateProviderProps> = ({ children }) => {
return (
<Provider
store={store}
>
{children}
</Provider>
)
}
export default StateProvider
+123
View File
@@ -0,0 +1,123 @@
import { css, cx } from '@/styled-system/css'
import { Container } from '@/styled-system/jsx'
import { center } from '@/styled-system/patterns'
import { button, card, iconButton } from '@/styled-system/recipes'
import { type TeamMember } from '@/types/User'
import { faFacebook, faInstagram, faTwitch, faTwitter, faYoutube } from '@fortawesome/free-brands-svg-icons'
import { faGlobe } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import NextImage from 'next/image'
import NextLink from 'next/link'
import { type FC } from 'react'
const team: TeamMember[] = [
{
image: '/images/team/SrJuggernaut.png',
name: 'SrJuggernaut',
role: 'administrator',
description: 'Soy desarrollador web y me gusta jugar videojuegos.',
socialNetworks: [
{ url: 'https://www.facebook.com/SrJuggernaut', label: 'SrJuggernaut Facebook', icon: faFacebook },
{ url: 'https://twitter.com/SrJuggernaut', label: 'SrJuggernaut Twitter', icon: faTwitter },
{ url: 'https://youtube.com/juggernautplays', label: 'SrJuggernaut YouTube', icon: faYoutube },
{ url: 'https://twitch.tv/juggernautplays', label: 'SrJuggernaut Twitch', icon: faTwitch },
{ url: 'https://www.instagram.com/sr_juggernaut', label: 'SrJuggernaut Instagram', icon: faInstagram },
{ url: 'https://srjuggernaut.dev/', label: 'SrJuggernaut Website', icon: faGlobe }
]
}
]
const Team: FC = () => {
return (
<section
className={center({
minHeight: '75vh',
backgroundImage: 'url(/images/backgrounds/MysteriousForest.jpg)'
})}
>
<Container>
<div
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 'medium',
flexWrap: 'wrap'
})}
>
{team.map((member, index) => (
<div
key={`team-member-${index}`}
className={cx(card({ variant: 'retro' }).body, css({
maxWidth: '300px',
textAlign: 'center'
}))}
>
<div
className={cx(card({ variant: 'retro' }).media, center())}
>
<NextImage
src={member.image}
alt={member.name}
width={120}
height={120}
/>
</div>
<div
className={card({ variant: 'retro' }).content}
>
<h3>{member.name}</h3>
<p>{member.description}</p>
<div
className={css({
display: 'flex',
justifyContent: 'center',
gap: 'small',
flexWrap: 'wrap'
})}
>
{member.socialNetworks.map((socialNetwork, index) => (
<a
key={`team-member-${index}-social-network`}
className={iconButton()}
href={socialNetwork.url}
>
<FontAwesomeIcon icon={socialNetwork.icon} fixedWidth />
</a>
))}
</div>
</div>
</div>
))}
</div>
<div
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-evenly',
gap: 'medium',
paddingBlock: 'large',
width: '100%'
})}
>
<NextLink
className={button({ color: 'info' })}
href="/equipo"
>
Ver el equipo completo
</NextLink>
<NextLink
className={button({ color: 'primary' })}
href="/equipo/unirse"
>
Únete al equipo
</NextLink>
</div>
</Container>
</section>
)
}
export default Team
+59
View File
@@ -0,0 +1,59 @@
import Typography from '@/components/ui/Typography'
import { css } from '@/styled-system/css'
import { Container } from '@/styled-system/jsx'
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { type FC } from 'react'
const ClanesPage: FC = () => {
return (
<Container>
<Typography variant="h1" align="center">Clanes</Typography>
<Typography variant="body1">Los clanes son espacios donde compartir nuestros gustos con otros usuarios, dándonos la oportunidad de organizar proyectos y eventos en los cuales formar parte.</Typography>
<div
className={css({
display: 'grid',
gridTemplateColumns: { base: '1fr 1fr', smDown: '1fr' },
gap: 'medium'
})}
>
<div>
<Typography variant="h2">Beneficios de los clanes</Typography>
<Typography variant="body1">La intención de EntGamers es brindar beneficios a los clanes que les permitan operar en un ambiente de comunicación y colaboración.</Typography>
<ul className="fa-ul">
<li><FontAwesomeIcon icon={faChevronRight} listItem /> Espacio en el servidor de Discord.</li>
<li><FontAwesomeIcon icon={faChevronRight} listItem /> Apoyo de la administración con proyectos y eventos.</li>
<li><FontAwesomeIcon icon={faChevronRight} listItem /> Apoyo del equipo de moderación.</li>
</ul>
</div>
<div>
<Typography variant="h2">Requisitos para formar un clan</Typography>
<Typography variant="body1">Todos los clanes deben cumplir con los siguientes requisitos:</Typography>
<ul className="fa-ul">
<li><FontAwesomeIcon icon={faChevronRight} listItem /> Tener un encargado.</li>
<li><FontAwesomeIcon icon={faChevronRight} listItem /> Fomentar el compañerismo y la comunidad.</li>
<li><FontAwesomeIcon icon={faChevronRight} listItem /> Aportar contenido de forma periódica para la comunidad.</li>
<li><FontAwesomeIcon icon={faChevronRight} listItem /> Realizar al menos una actividad mensual con los integrantes.</li>
</ul>
</div>
</div>
<Typography variant="h2">Clanes activos</Typography>
<div
className={css({
backgroundColor: 'info',
color: 'info.contrast',
borderRadius: 'medium',
padding: 'medium',
marginBlock: 'medium',
'& a': {
color: 'info.contrast',
fontWeight: 'bold'
}
})}
>
Esta sección está en construcción. Puedes ver los clanes activos en nuestro <a href="http://discord.gg/nqwzHJC">Servidor de Discord</a>.
</div>
</Container>
)
}
export default ClanesPage
+77
View File
@@ -0,0 +1,77 @@
'use client'
import Button from '@/components/ui/Button'
import ButtonGroup from '@/components/ui/ButtonGroup'
import useSession from '@/hooks/useSession'
import { css } from '@/styled-system/css'
import { AnimatePresence, motion } from 'framer-motion'
import { useState, type FC } from 'react'
import UpdateEmail from './UpdateEmail'
import UpdatePassword from './UpdatePassword'
import UpdateUserName from './UpdateUserName'
import UpdateUserPreferences from './UpdateUserPreferences'
type Tab = 'perfil' | 'login'
const CuentaTabs: FC = () => {
useSession('/login')
const [currentTab, setCurrentTab] = useState<Tab>('perfil')
return (
<>
<ButtonGroup
fullWidth={true}
>
<Button
fullWidth
onClick={() => { setCurrentTab('perfil') }}
disabled={currentTab === 'perfil'}
>
Perfil
</Button>
<Button
fullWidth
onClick={() => { setCurrentTab('login') }}
disabled={currentTab === 'login'}
>
Login
</Button>
</ButtonGroup>
<div
className={css({
overflow: 'hidden',
marginTop: 'medium'
})}
>
<AnimatePresence
initial={false}
mode='wait'
>
{currentTab === 'login' && (
<motion.div
transition={{ duration: 0.15, ease: 'easeInOut' }}
initial={{ opacity: 0, x: '-100%' }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: '100%' }}
key="login">
<UpdateEmail />
<UpdatePassword />
</motion.div>
)}
{currentTab === 'perfil' && (
<motion.div
transition={{ duration: 0.15, ease: 'easeInOut' }}
initial={{ opacity: 0, x: '-100%' }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: '100%' }}
key="perfil">
<UpdateUserName />
<UpdateUserPreferences />
</motion.div>
)}
</AnimatePresence>
</div>
</>
)
}
export default CuentaTabs
+129
View File
@@ -0,0 +1,129 @@
'use client'
import Button from '@/components/ui/Button'
import Typography from '@/components/ui/Typography'
import FormGroup from '@/components/ui/form/FormGroup'
import Input from '@/components/ui/form/Input'
import PasswordInput from '@/components/ui/form/PasswordInput'
import { useAppDispatch } from '@/hooks/useAppDispatch'
import useSession from '@/hooks/useSession'
import { addAlert } from '@/state/feedbackSlice'
import { nanoid } from '@reduxjs/toolkit'
import { AppwriteException } from 'appwrite'
import { updateEmail } from 'entgamers-database/frontend/session'
import { useFormik } from 'formik'
import { type FC } from 'react'
import { object, string } from 'yup'
interface UpdateEmailData {
email: string
password: string
}
const updateEmailSchema = object({
email: string().email('El correo electrónico no es válido').required('El correo electrónico es requerido'),
password: string().required('La contraseña es requerida')
})
const UpdateEmail: FC = () => {
const { status, session } = useSession('/login')
const dispatch = useAppDispatch()
const formik = useFormik<UpdateEmailData>({
initialValues: {
email: '',
password: ''
},
onSubmit: async ({ email, password }) => {
try {
await updateEmail(email, password)
dispatch(addAlert({
id: nanoid(),
title: 'Correo actualizado',
message: 'Ahora puedes iniciar sesión',
severity: 'success'
}))
} catch (error) {
if (error instanceof AppwriteException) {
dispatch(addAlert({
id: nanoid(),
message: error.message,
title: 'Error mientras se actualizaba el correo',
severity: 'error'
}))
} else {
dispatch(addAlert({
id: nanoid(),
message: 'Error desconocido',
title: 'Error mientras se actualizaba el correo',
severity: 'error'
}))
}
}
},
validationSchema: updateEmailSchema,
validateOnMount: true
})
if (status !== 'idle' || session === undefined) {
// TODO: Replace with Skeleton
return null
}
return (
<>
<Typography variant="h2">Cambia tu correo</Typography>
<form
onSubmit={formik.handleSubmit}
>
<FormGroup>
<label
htmlFor="email"
>
Correo
</label>
<Input
id="email"
type="email"
name="email"
value={formik.values.email}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
status={formik.touched.email !== undefined && formik.errors.email !== undefined ? 'danger' : undefined}
/>
{formik.touched.email !== undefined && formik.errors.email !== undefined && (
<Typography variant="caption" color="danger">{formik.errors.email}</Typography>
)}
</FormGroup>
<FormGroup>
<label
htmlFor="password"
>
Contraseña
</label>
<PasswordInput
id="password"
name="password"
value={formik.values.password}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
status={formik.touched.password !== undefined && formik.errors.password !== undefined ? 'danger' : undefined}
fullWidth
/>
{formik.touched.password !== undefined && formik.errors.password !== undefined && (
<Typography variant="caption" color="danger">{formik.errors.password}</Typography>
)}
</FormGroup>
<FormGroup>
<Button
type="submit"
disabled={formik.isSubmitting || !formik.isValid}
>
Actualizar contraseña
</Button>
</FormGroup>
</form>
</>
)
}
export default UpdateEmail
+160
View File
@@ -0,0 +1,160 @@
'use client'
import Button from '@/components/ui/Button'
import Typography from '@/components/ui/Typography'
import FormGroup from '@/components/ui/form/FormGroup'
import PasswordInput from '@/components/ui/form/PasswordInput'
import { useAppDispatch } from '@/hooks/useAppDispatch'
import useSession from '@/hooks/useSession'
import { addAlert } from '@/state/feedbackSlice'
import { nanoid } from '@reduxjs/toolkit'
import { AppwriteException } from 'appwrite'
import { updatePassword } from 'entgamers-database/frontend/session'
import { useFormik } from 'formik'
import { type FC } from 'react'
import { object, ref, string } from 'yup'
interface UpdatePasswordData {
password: string
confirmPassword: string
currentPassword: string
}
const updatePasswordSchema = object({
password: string()
.min(6, 'La contraseña debe tener al menos 6 caracteres')
.matches(/[a-z]/, 'La contraseña debe tener al menos una letra minúscula')
.matches(/[A-Z]/, 'La contraseña debe tener al menos una letra mayúscula')
.matches(/[0-9]/, 'La contraseña debe tener al menos un número')
.required('La contraseña es requerida'),
confirmPassword: string().oneOf([ref('password')], 'Las contraseñas no coinciden').required('La confirmación de la contraseña es requerida'),
currentPassword: string().required('La contraseña actual es requerida')
})
const UpdatePassword: FC = () => {
const { status, session } = useSession('/login')
const dispatch = useAppDispatch()
const formik = useFormik<UpdatePasswordData>({
initialValues: {
password: '',
confirmPassword: '',
currentPassword: ''
},
onSubmit: async ({ password, currentPassword }) => {
try {
await updatePassword(password, currentPassword)
dispatch(addAlert({
id: nanoid(),
title: 'Contrasenya actualizada',
message: 'Ahora puedes iniciar sesión',
severity: 'success'
}))
} catch (error) {
if (error instanceof AppwriteException) {
dispatch(addAlert({
id: nanoid(),
message: error.message,
title: 'Error mientras se actualizaba la contraseña',
severity: 'error'
}))
} else {
dispatch(addAlert({
id: nanoid(),
message: 'Error desconocido',
title: 'Error mientras se actualizaba la contraseña',
severity: 'error'
}))
}
}
},
validationSchema: updatePasswordSchema,
validateOnMount: true
})
if (status !== 'idle' || session === undefined) {
// TODO: Replace with Skeleton
return null
}
return (
<>
<Typography variant="h2">Actualizar contraseña</Typography>
<form
onSubmit={formik.handleSubmit}
>
<FormGroup>
<label
htmlFor="password"
>
Nueva contraseña
</label>
<PasswordInput
id="password"
name="password"
value={formik.values.password}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
status={formik.touched.password !== undefined && formik.errors.password !== undefined ? 'danger' : undefined}
fullWidth
/>
{formik.touched.password !== undefined && formik.errors.password !== undefined && (
<Typography variant="caption" color="danger">
{formik.errors.password}
</Typography>
)}
</FormGroup>
<FormGroup>
<label
htmlFor="confirmPassword"
>
Confirmar nueva contraseña
</label>
<PasswordInput
id="confirmPassword"
name="confirmPassword"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.confirmPassword}
status={formik.touched.confirmPassword !== undefined && formik.errors.confirmPassword !== undefined ? 'danger' : undefined}
fullWidth
/>
{formik.touched.confirmPassword !== undefined && formik.errors.confirmPassword !== undefined && (
<Typography variant="caption" color="danger">
{formik.errors.confirmPassword}
</Typography>
)}
</FormGroup>
<FormGroup>
<label
htmlFor="currentPassword"
>
Contraseña actual
</label>
<PasswordInput
id="currentPassword"
name="currentPassword"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.currentPassword}
status={formik.touched.currentPassword !== undefined && formik.errors.currentPassword !== undefined ? 'danger' : undefined}
fullWidth
/>
{formik.touched.currentPassword !== undefined && formik.errors.currentPassword !== undefined && (
<Typography variant="caption" color="danger">
{formik.errors.currentPassword}
</Typography>
)}
</FormGroup>
<FormGroup>
<Button
type="submit"
disabled={formik.isSubmitting || !formik.isValid}
>
Actualizar contraseña
</Button>
</FormGroup>
</form>
</>
)
}
export default UpdatePassword
+115
View File
@@ -0,0 +1,115 @@
'use client'
import Button from '@/components/ui/Button'
import Typography from '@/components/ui/Typography'
import FormGroup from '@/components/ui/form/FormGroup'
import Input from '@/components/ui/form/Input'
import { useAppDispatch } from '@/hooks/useAppDispatch'
import useSession from '@/hooks/useSession'
import { addAlert } from '@/state/feedbackSlice'
import { nanoid } from '@reduxjs/toolkit'
import { AppwriteException } from 'appwrite'
import { updateName } from 'entgamers-database/frontend/session'
import { useFormik } from 'formik'
import { useEffect, type FC } from 'react'
import { object, string } from 'yup'
interface UpdateUserNameData {
name: string
}
const UpdateUserNameSchema = object({
name: string().required('El nombre es requerido')
})
const UpdateUserName: FC = () => {
const { status, session, user } = useSession('/login')
const dispatch = useAppDispatch()
const formik = useFormik<UpdateUserNameData>({
initialValues: {
name: ''
},
onSubmit: async ({ name }) => {
try {
await updateName(name)
dispatch(addAlert({
id: nanoid(),
title: 'Nombre actualizado',
message: 'Se actualizo correctamente el nombre',
severity: 'success'
}))
} catch (error) {
if (error instanceof AppwriteException) {
dispatch(addAlert({
id: nanoid(),
message: error.message,
title: 'Error mientras se actualizaba el nombre',
severity: 'error'
}))
} else {
dispatch(addAlert({
id: nanoid(),
message: 'Error desconocido',
title: 'Error mientras se actualizaba el nombre',
severity: 'error'
}))
}
}
},
validationSchema: UpdateUserNameSchema,
validateOnMount: true,
initialTouched: { name: true }
})
useEffect(() => {
if (status === 'idle' && session !== undefined && user !== undefined) {
formik.setValues({
name: user?.name ?? ''
})
.catch(console.error)
}
}, [status, session, user])
if (status !== 'idle' || session === undefined) {
// TODO: Replace with Skeleton
return null
}
return (
<>
<Typography variant="h2">Cambia tu nombre de usuario</Typography>
<form
onSubmit={formik.handleSubmit}
>
<FormGroup>
<label
htmlFor="name"
>
Nombre de usuario
</label>
<Input
id="name"
name="name"
type="text"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.name}
status={formik.touched.name !== undefined && formik.errors.name !== undefined ? 'danger' : undefined}
/>
{formik.touched.name !== undefined && formik.errors.name !== undefined && (
<Typography variant="caption" color="danger">{formik.errors.name}</Typography>
)}
</FormGroup>
<FormGroup
>
<Button
type="submit"
disabled={formik.isSubmitting || !formik.isValid}
>
Actualizar nombre de usuario
</Button>
</FormGroup>
</form>
</>
)
}
export default UpdateUserName
+111
View File
@@ -0,0 +1,111 @@
'use client'
import Button from '@/components/ui/Button'
import Typography from '@/components/ui/Typography'
import FormGroup from '@/components/ui/form/FormGroup'
import TextArea from '@/components/ui/form/TextArea'
import { useAppDispatch } from '@/hooks/useAppDispatch'
import useManageError from '@/hooks/useManageError'
import useSession from '@/hooks/useSession'
import { setCurrentUser } from '@/state/sessionSlice'
import { updatePreferences, type UserPreferences } from 'entgamers-database/frontend/session'
import { useFormik } from 'formik'
import { useEffect, type FC } from 'react'
import { array, object, string, type ObjectSchema } from 'yup'
const socialLinksSchema: ObjectSchema<UserPreferences> = object({
bio: string().max(280, 'La descripción debe tener menos de 280 caracteres'),
profilePicture: string().url('La imagen debe ser una URL'),
socialLinks: array().of(
object({
label: string().required('La etiqueta es requerida'),
url: string().url('La URL debe ser una URL').required('La URL es requerida')
})
).min(0)
})
const UpdateUserPreferences: FC = () => {
const { status, session, user } = useSession('/login')
const dispatch = useAppDispatch()
const { manageError } = useManageError()
const formik = useFormik<UserPreferences>({
initialValues: {
bio: '',
profilePicture: '',
socialLinks: []
},
onSubmit: async ({ bio, profilePicture, socialLinks }) => {
try {
const updatedUserWithPreferences = await updatePreferences({ bio, profilePicture, socialLinks })
dispatch(setCurrentUser(updatedUserWithPreferences))
} catch (error) {
manageError(error, 'Error mientras se actualizaba las preferencias', ' Error desconocido mientras se actualizaba las preferencias', 'error')
}
},
validationSchema: socialLinksSchema,
validateOnMount: true
})
useEffect(() => {
if (status === 'idle' && session !== undefined) {
formik.setValues({
bio: user?.prefs.bio ?? '',
profilePicture: user?.prefs.profilePicture ?? '',
socialLinks: user?.prefs.socialLinks ?? []
})
.catch(console.error)
}
}, [status, session])
if (status !== 'idle' || session === undefined) {
// TODO: Replace with Skeleton
return null
}
return (
<>
<Typography
variant='h3'
>
Preferencias
</Typography>
<form
onSubmit={formik.handleSubmit}
>
<FormGroup>
<label
htmlFor='bio'
>
Biografia
</label>
<TextArea
id='bio'
name='bio'
value={formik.values.bio}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
disabled={formik.isSubmitting}
status={formik.touched.bio !== undefined && formik.errors.bio !== undefined ? 'danger' : undefined}
/>
{formik.touched.bio !== undefined && formik.errors.bio !== undefined && (
<Typography variant="caption" color="danger">
{formik.errors.bio}
</Typography>
)}
</FormGroup>
{/* TODO: Add Profile Picture and Social Links fields */}
<FormGroup>
<Button
type='submit'
disabled={formik.isSubmitting || !formik.isValid}
fullWidth
>
Actualizar
</Button>
</FormGroup>
</form>
</>
)
}
export default UpdateUserPreferences
+17
View File
@@ -0,0 +1,17 @@
import Typography from '@/components/ui/Typography'
import { Container } from '@/styled-system/jsx'
import { type FC } from 'react'
import CuentaTabs from './CuentaTabs'
const CuentaPage: FC = () => {
return (
<Container>
<Typography variant="h1" align="center">Cuenta</Typography>
<Typography variant="body1">
Desde aquí puedes administrar las preferencias y ajustes de tu cuenta.
</Typography>
<CuentaTabs/>
</Container>
)
}
export default CuentaPage
@@ -0,0 +1,73 @@
'use client'
import TeamApplications from '@/app/dashboard/_components/TeamApplications'
import Button from '@/components/ui/Button'
import ButtonGroup from '@/components/ui/ButtonGroup'
import Typography from '@/components/ui/Typography'
import useSession from '@/hooks/useSession'
import { css } from '@/styled-system/css'
import { ADMIN_CLAN_ID } from 'entgamers-database/frontend/clanes/administrative'
import { AnimatePresence, motion } from 'framer-motion'
import { useState, type FC } from 'react'
type Tab = undefined | 'teamApplications'
const DashboardTabs: FC = () => {
const { clanes, belongToClan } = useSession('/login')
const [currentTab, setCurrentTab] = useState<Tab>(undefined)
return (
<>
{clanes !== undefined && (
<ButtonGroup
fullWidth
>
<Button fullWidth onClick={() => { setCurrentTab(undefined) }} disabled={currentTab === undefined}>
Panel
</Button>
{belongToClan(ADMIN_CLAN_ID) && (
<Button fullWidth onClick={() => { setCurrentTab('teamApplications') }} disabled={currentTab === 'teamApplications'}>
Aplicaciones
</Button>
)}
</ButtonGroup>
)}
<div
className={css({
overflow: 'hidden',
marginTop: 'medium'
})}
>
<AnimatePresence
initial={false}
mode='wait'
>
{currentTab === undefined && (
<motion.div
transition={{ duration: 0.15, ease: 'easeInOut' }}
initial={{ opacity: 0, x: '-100%' }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: '100%' }}
key="dashboard"
>
<Typography variant="body1">Selecciona una de las opciones de arriba para comenzar.</Typography>
</motion.div>
)}
{currentTab === 'teamApplications' && (
<motion.div
transition={{ duration: 0.15, ease: 'easeInOut' }}
initial={{ opacity: 0, x: '-100%' }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: '100%' }}
key="teamApplications"
>
<TeamApplications />
</motion.div>
)}
</AnimatePresence>
</div>
</>
)
}
export default DashboardTabs
@@ -0,0 +1,14 @@
import ApplicationsList from '@/app/dashboard/_components/teamApplications/ApplicationsList'
import Typography from '@/components/ui/Typography'
import { type FC } from 'react'
const TeamApplications: FC = () => {
return (
<>
<Typography variant="h2">Aplicaciones</Typography>
<ApplicationsList />
</>
)
}
export default TeamApplications
@@ -0,0 +1,48 @@
import DebouncedInput from '@/components/ui/form/DebouncedInput'
import { type Column } from '@tanstack/react-table'
import { type TeamApplication, type TeamApplicationRole, type TeamApplicationStatus } from 'entgamers-database/types/teamApplications'
import { type FC } from 'react'
import RoleSelector from './RoleSelector'
import StatusSelector from './StatusSelector'
export interface ApplicationsFilterProps {
column: Column<TeamApplication, unknown>
}
const ApplicationsFilter: FC<ApplicationsFilterProps> = ({ column }) => {
const columnFilterValue = column.getFilterValue()
switch (column.id) {
case 'status':
return (
<StatusSelector
id={`${column.id}-status-filter`}
value={columnFilterValue as TeamApplicationStatus}
onChange={value => { column.setFilterValue(value) }}
allowEmpty
/>
)
case 'role':
return (
<RoleSelector
id={`${column.id}-role-filter`}
value={columnFilterValue as TeamApplicationRole}
onChange={value => { column.setFilterValue(value) }}
allowEmpty
/>
)
default:
return (
<DebouncedInput
fullWidth
type="text"
value={(columnFilterValue ?? '') as string}
onChange={value => { column.setFilterValue(value) }}
placeholder="Buscar..."
className="w-36 border shadow rounded"
list={column.id + 'list'}
/>
)
}
}
export default ApplicationsFilter
@@ -0,0 +1,272 @@
import IconButton from '@/components/ui/IconButton'
import { Table, TableBody, TableCell, TableContainer, TableHead, TableHeadCell, TableRow } from '@/components/ui/Table'
import useManageError from '@/hooks/useManageError'
import { css } from '@/styled-system/css'
import { formatDate } from '@/utilities/date'
import { type TeamApplication, type TeamApplicationList } from '@/utilities/teamApplication'
import { faChevronLeft, faChevronRight, faSort, faSortAsc, faSortDesc } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable, type ColumnFiltersState, type PaginationState, type RowData, type SortingState } from '@tanstack/react-table'
import { getAllTeamApplications, updateTeamApplication } from 'entgamers-database/frontend/database/teamApplications'
import { Query } from 'entgamers-database/lib/appwrite'
import { useEffect, useState, type FC } from 'react'
import ApplicationsFilter from './ApplicationsFilter'
import StatusUpdater from './StatusUpdater'
declare module '@tanstack/table-core' {
interface TableMeta<TData extends RowData> {
updateRow: (id: string, value: Partial<TData>) => Promise<void>
}
}
const columnHelper = createColumnHelper<TeamApplication>()
const columns = [
columnHelper.accessor('$id', {
header: 'ID',
enableColumnFilter: false
}),
columnHelper.accessor('status', {
header: 'Estado',
cell: StatusUpdater,
getUniqueValues () {
return ['Pending', 'Accepted', 'Rejected']
}
}),
columnHelper.accessor('role', {
header: 'Rol'
}),
columnHelper.accessor('name', {
header: 'Nombre'
}),
columnHelper.accessor('message', {
header: 'Mensaje',
minSize: 450
}),
columnHelper.accessor('email', {
header: 'Correo'
}),
columnHelper.accessor('discord', {
header: 'Discord'
}),
columnHelper.accessor('$createdAt', {
header: 'Creado',
enableColumnFilter: false,
cell: (info) => {
return formatDate(new Date(info.getValue()))
}
}),
columnHelper.accessor('$updatedAt', {
header: 'Actualizado',
enableColumnFilter: false,
cell: (info) => {
return formatDate(new Date(info.getValue()))
}
})
]
const ApplicationsList: FC = () => {
const { manageError } = useManageError()
const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 10 })
const [sorting, setSorting] = useState<SortingState>([{ id: '$createdAt', desc: true }])
const [applications, setApplications] = useState<TeamApplicationList>({ total: 0, documents: [] })
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([{ id: 'status', value: 'Pending' }])
const table = useReactTable({
data: applications.documents,
columns,
initialState: {
columnVisibility: {
$id: false,
email: false
}
},
state: {
pagination,
sorting,
columnFilters
},
meta: {
updateRow: async (id: string, value: Partial<TeamApplication>) => {
const updatedTeamApplication = await updateTeamApplication(id, value)
const newApplications = applications.documents.map((application) => application.$id === updatedTeamApplication.$id ? updatedTeamApplication : application)
setApplications({ total: applications.total, documents: newApplications })
}
},
manualPagination: true,
rowCount: applications.total,
onPaginationChange: setPagination,
enableSorting: true,
manualSorting: true,
onSortingChange: setSorting,
manualFiltering: true,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel()
})
useEffect(() => {
const query: string[] = [
Query.limit(pagination.pageSize),
Query.offset(pagination.pageIndex * pagination.pageSize)
]
if (sorting.length > 0) {
const sort: string = sorting[0].desc ? Query.orderDesc(sorting[0].id) : Query.orderAsc(sorting[0].id)
query.push(sort)
}
if (columnFilters.length > 0) {
const filter: string[] = columnFilters.map((columnFilter) => {
return Query.contains(columnFilter.id, columnFilter.value as string)
})
query.push(...filter)
}
getAllTeamApplications(query)
.then((applicationList) => { setApplications(applicationList) })
.catch((error) => {
if (error instanceof Error && error.name === 'AbortError') return
manageError(error, 'Error al obtener las aplicaciones', 'Error desconocido al obtener las aplicaciones', 'error')
})
}, [pagination, sorting, columnFilters])
// TODO: Better UI Controls for: column visibility. Quantity selector.
return (
<>
<div
className={css({
display: 'flex',
gap: 'small',
marginBottom: 'small'
})}
>
{table.getAllLeafColumns().map((column) => (
<div key={column.id}>
<label htmlFor={`${column.id}-view`}>
<input
type="checkbox"
id={`${column.id}-view`}
checked={column.getIsVisible()}
onChange={column.getToggleVisibilityHandler()}
/> {column.columnDef.header?.toString() ?? column.id}
</label>
</div>
))}
</div>
<TableContainer>
<Table>
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHeadCell
key={header.id}
className={css({
verticalAlign: 'top',
position: 'relative',
'&:hover > [data-is-resizing]': {
backgroundColor: 'border'
}
})}
style={{ minWidth: header.getSize() }}
>
<div
className={css({
display: 'flex',
flexDirection: 'column'
})}
>
<div
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 'small'
})}
>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext()) }
{header.column.getCanSort() && (
<IconButton
size="small"
onClick={header.column.getToggleSortingHandler()}
>
<FontAwesomeIcon icon={header.column.getIsSorted() === 'asc' ? faSortAsc : header.column.getIsSorted() === 'desc' ? faSortDesc : faSort} size="sm" fixedWidth />
</IconButton>
)}
</div>
{header.column.getCanFilter()
? (
<ApplicationsFilter column={header.column}/>
)
: null
}
</div>
<div
className={css({
position: 'absolute',
top: 0,
right: 0,
height: '100%',
width: '5px',
cursor: 'col-resize',
userSelect: 'none',
touchAction: 'none',
'&:hover': {
backgroundColor: 'border'
},
'&[data-is-resizing=true]': {
backgroundColor: 'primary'
}
})}
style={{
transform: `translateX(${1 * (table.getState().columnSizingInfo
.deltaOffset ?? 0)}px)`
}}
data-is-resizing={header.column.getIsResizing()}
onDoubleClick={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
onMouseDown={header.getResizeHandler()}
/>
</TableHeadCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<div
className={css({
display: 'flex',
gap: 'small',
justifyContent: 'space-between',
alignItems: 'center',
paddingBlock: 'medium'
})}
>
<IconButton
onClick={() => { table.previousPage() }}
disabled={!table.getCanPreviousPage()}
>
<FontAwesomeIcon icon={faChevronLeft} fixedWidth />
</IconButton>
Pagina {table.getState().pagination.pageIndex + 1} de {table.getPageCount()}
<IconButton
onClick={() => { table.nextPage() }}
disabled={!table.getCanNextPage()}
>
<FontAwesomeIcon icon={faChevronRight} fixedWidth />
</IconButton>
</div>
</>
)
}
export default ApplicationsList
@@ -0,0 +1,46 @@
import { css } from '@/styled-system/css'
import { type TeamApplicationRole } from 'entgamers-database/types/teamApplications'
import { type FC } from 'react'
export interface RoleSelectorProps {
id: string
value: TeamApplicationRole
onChange: (role: TeamApplicationRole) => void
allowEmpty?: boolean
}
const RoleSelector: FC<RoleSelectorProps> = ({ id, value, onChange, allowEmpty }) => {
/* TODO: Change for Select UI Component when it's ready */
return (
<select
id={`${id}-status`}
className={css({
width: '100%',
border: 'none',
background: 'transparent',
color: 'inherit',
outline: 'none',
cursor: 'pointer',
fontSize: 'inherit',
fontWeight: 'inherit',
lineHeight: 'inherit',
padding: '0',
borderRadius: '0',
'&:focus': {
outline: 'none'
}
})}
value={value}
onChange={(event) => {
onChange(event.target.value as TeamApplicationRole)
}}
>
{allowEmpty === true && <option value="">Todos</option>}
<option value="Admin">Administrador</option>
<option value="Collaborator">Colaborador</option>
<option value="Moderator">Moderador</option>
</select>
)
}
export default RoleSelector
@@ -0,0 +1,46 @@
import { css } from '@/styled-system/css'
import { type TeamApplicationStatus } from 'entgamers-database/types/teamApplications'
import { type FC } from 'react'
export interface StatusSelectorProps {
id: string
value: TeamApplicationStatus
onChange: (status: TeamApplicationStatus) => void
allowEmpty?: boolean
}
const StatusSelector: FC<StatusSelectorProps> = ({ id, value, onChange, allowEmpty }) => {
/* TODO: Change for Select UI Component when it's ready */
return (
<select
id={`${id}-status`}
className={css({
width: '100%',
border: 'none',
background: 'transparent',
color: 'inherit',
outline: 'none',
cursor: 'pointer',
fontSize: 'inherit',
fontWeight: 'inherit',
lineHeight: 'inherit',
padding: '0',
borderRadius: '0',
'&:focus': {
outline: 'none'
}
})}
value={value}
onChange={(event) => {
onChange(event.target.value as TeamApplicationStatus)
}}
>
{allowEmpty === true && <option value="">Todos</option>}
<option value="Pending">Pendiente</option>
<option value="Accepted">Aceptado</option>
<option value="Rejected">Rechazado</option>
</select>
)
}
export default StatusSelector
@@ -0,0 +1,21 @@
import { type CellContext } from '@tanstack/react-table'
import { type TeamApplication, type TeamApplicationStatus } from 'entgamers-database/types/teamApplications'
import { type FC } from 'react'
import StatusSelector from './StatusSelector'
const StatusUpdater: FC<CellContext<TeamApplication, TeamApplicationStatus>> = ({ cell: { id, row }, table }) => {
return (
<>
<StatusSelector
id={`${id}-status`}
value={row.original.status}
onChange={(status) => {
table.options.meta?.updateRow(row.original.$id, { status })
.catch(console.error)
}}
/>
</>
)
}
export default StatusUpdater
+15
View File
@@ -0,0 +1,15 @@
import Typography from '@/components/ui/Typography'
import { Container } from '@/styled-system/jsx'
import { type FC } from 'react'
import DashboardTabs from './_components/DashboardTabs'
const page: FC = () => {
return (
<Container>
<Typography variant="h1" align="center">Panel de control</Typography>
<DashboardTabs />
</Container>
)
}
export default page
+237
View File
@@ -0,0 +1,237 @@
import Typography from '@/components/ui/Typography'
import { css, cx } from '@/styled-system/css'
import { Container } from '@/styled-system/jsx'
import { center } from '@/styled-system/patterns'
import { button, card } from '@/styled-system/recipes'
import { getClanMembers } from 'entgamers-database/backend/clanes'
import { getUser } from 'entgamers-database/backend/users'
import { ADMIN_CLAN_ID, COLLABORATOR_CLAN_ID, MODERATOR_CLAN_ID } from 'entgamers-database/lib/env'
import { type UserList } from 'entgamers-database/types/user'
import NextImage from 'next/image'
import NextLink from 'next/link'
import { type Models } from 'node-appwrite'
import { type FC } from 'react'
interface GetTeamsResponse {
admins: UserList & { id: string }
moderators: UserList & { id: string }
collaborators: UserList & { id: string }
}
const getTeams = async (): Promise<GetTeamsResponse> => {
// await ensureAdministrativeClans()
const adminMembers: Models.MembershipList = await getClanMembers(ADMIN_CLAN_ID)
const moderatorMembers: Models.MembershipList = await getClanMembers(MODERATOR_CLAN_ID)
const collaboratorMembers: Models.MembershipList = await getClanMembers(COLLABORATOR_CLAN_ID)
const adminsPromises = adminMembers.memberships.map(async membership => await getUser(membership.userId))
const moderatorsPromises = moderatorMembers.memberships.map(async membership => await getUser(membership.userId))
const collaboratorsPromises = collaboratorMembers.memberships.map(async membership => await getUser(membership.userId))
const [admins, moderators, collaborators] = await Promise.all([
Promise.all(adminsPromises), Promise.all(moderatorsPromises), Promise.all(collaboratorsPromises)
])
return { admins: { id: ADMIN_CLAN_ID, total: admins.length, users: admins }, moderators: { id: MODERATOR_CLAN_ID, total: moderators.length, users: moderators }, collaborators: { id: COLLABORATOR_CLAN_ID, total: collaborators.length, users: collaborators } }
}
const EquipoPage: FC = async () => {
const { admins, moderators, collaborators } = await getTeams()
return (
<Container>
<Typography variant="h1" align="center">Equipo</Typography>
<Typography variant="body1">
El equipo de EntGamers está formado por personas que se dedican a la administración de la comunidad, a la organización de eventos y a la creación de contenido. EntGamers siempre intenta recompensar a sus miembros más activos, por lo que si quieres formar parte de nuestro equipo, ¡no dudes en contactar con nosotros!
</Typography>
<Typography variant="h2" align="center">Administradores</Typography>
<Typography variant="body1">
Los administradores son quienes se encargan de que todo funcione como es debido en la comunidad, desde la moderación de los grupos hasta la organización de eventos.
</Typography>
{admins.total >= 1
? (
<Container
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 'medium',
flexWrap: 'wrap',
padding: 'medium',
width: '100%'
})}
>
{admins.users.map((user, index) => (
<div
key={`admin-${index}`}
className={cx(card({ variant: 'retro' }).body, css({
maxWidth: '300px',
textAlign: 'center'
}))}
>
<div
className={cx(card({ variant: 'retro' }).media, center())}
>
<NextImage
src={user.prefs.profilePicture !== undefined && user.prefs.profilePicture.trim() !== '' ? user.prefs.profilePicture.trim() : '/images/EntGamers.png'}
alt={user.name !== '' ? user.name : `Usuario ${index + 1} avatar`}
width={120}
height={120}
/>
</div>
<div
className={card({ variant: 'retro' }).content}
>
<Typography variant="h3" align="center">{user.name !== '' ? user.name : `Usuario ${index + 1}`}</Typography>
{user.prefs.bio !== undefined && user.prefs.bio !== '' && (
<Typography variant="body1">{user.prefs.bio}</Typography>
)}
</div>
</div>
))}
</Container>
)
: (
<Typography variant="body2" color="info">
Ups, parece que ahora mismo no hay administradores, pero en EntGamers siempre estamos estamos buscando gente que quiera organizar cosas para la comunidad, puedes contactarnos para formar parte de nuestro equipo haciendo click en el siguiente enlace.
</Typography>
)
}
<div className={center()}>
<NextLink
className={button({ color: 'info' })}
href={`/equipo/unirse?role=${ADMIN_CLAN_ID}`}
>
¡Quiero ser Administrador!
</NextLink>
</div>
<Typography variant="h2" align="center">Moderadores</Typography>
<Typography variant="body1">
Los moderadores son los encargados de mantener el orden en los grupos de la comunidad, así como de ayudar a los usuarios a resolver sus dudas.
</Typography>
{moderators.total >= 1
? (
<Container
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 'medium',
flexWrap: 'wrap',
padding: 'medium',
width: '100%'
})}
>
{moderators.users.map((user, index) => (
<div
key={`moderator-${index}`}
className={cx(card({ variant: 'retro' }).body, css({
maxWidth: '300px',
textAlign: 'center'
}))}
>
<div
className={cx(card({ variant: 'retro' }).media, center())}
>
<NextImage
src={user.prefs.profilePicture !== undefined && user.prefs.profilePicture.trim() !== '' ? user.prefs.profilePicture.trim() : '/images/EntGamers.png'}
alt={user.name !== '' ? user.name : `Usuario ${index + 1} avatar`}
width={120}
height={120}
/>
</div>
<div
className={card({ variant: 'retro' }).content}
>
<Typography variant="h3" align="center">{user.name !== '' ? user.name : `Usuario ${index + 1}`}</Typography>
{user.prefs.bio !== undefined && user.prefs.bio !== '' && (
<Typography variant="body1">{user.prefs.bio}</Typography>
)}
</div>
</div>
))}
</Container>
)
: (
<Typography variant="body2" color="info">
Ups, parece que ahora mismo no hay moderadores, pero en EntGamers siempre estamos buscando gente que quiera ayudar a la comunidad. si quieres ser moderador, puedes hacer click en el botón de abajo.
</Typography>
)
}
<div className={center()}>
<NextLink
className={button({ color: 'info' })}
href={`/equipo/unirse?role=${moderators.id}`}
>
¡Quiero ser moderador!
</NextLink>
</div>
<Typography variant="h2" align="center">Colaboradores</Typography>
<Typography variant="body1">
Los colaboradores son los encargados de crear contenido para la comunidad, como artículos, tutoriales, vídeos, eventos etc.
</Typography>
{collaborators.total >= 1
? (
<Container
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 'medium',
flexWrap: 'wrap',
padding: 'medium',
width: '100%'
})}
>
{collaborators.users.map((user, index) => (
<div
key={`collaborator-${index}`}
className={cx(card({ variant: 'retro' }).body, css({
maxWidth: '300px',
textAlign: 'center'
}))}
>
<div
className={cx(card({ variant: 'retro' }).media, center())}
>
<NextImage
src={user.prefs.profilePicture !== undefined && user.prefs.profilePicture.trim() !== '' ? user.prefs.profilePicture.trim() : '/images/EntGamers.png'}
alt={user.name !== '' ? user.name : `Usuario ${index + 1} avatar`}
width={120}
height={120}
/>
</div>
<div
className={card({ variant: 'retro' }).content}
>
<Typography variant="h3" align="center">{user.name !== '' ? user.name : `Usuario ${index + 1}`}</Typography>
{user.prefs.bio !== undefined && user.prefs.bio !== '' && (
<Typography variant="body1">{user.prefs.bio}</Typography>
)}
</div>
</div>
))}
</Container>
)
: (
<Typography variant="body2" color="info">
Ups, parece que ahora mismo no hay colaboradores, pero en EntGamers siempre estamos buscando gente que quiera ayudar a la comunidad. si quieres ser colaborador, puedes hacer click en el botón de abajo.
</Typography>
)
}
<div className={center()}>
<NextLink
className={button({ color: 'info' })}
href={`/equipo/unirse?role=${collaborators.id}`}
>
¡Quiero ser colaborador!
</NextLink>
</div>
</Container>
)
}
export default EquipoPage
+371
View File
@@ -0,0 +1,371 @@
'use client'
import Button from '@/components/ui/Button'
import Typography from '@/components/ui/Typography'
import FormGroup from '@/components/ui/form/FormGroup'
import Input from '@/components/ui/form/Input'
import TextArea from '@/components/ui/form/TextArea'
import { useAppDispatch } from '@/hooks/useAppDispatch'
import useManageError from '@/hooks/useManageError'
import { addAlert } from '@/state/feedbackSlice'
import { css } from '@/styled-system/css'
import { teamApplicationDataSchema } from '@/utilities/teamApplication'
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon, type FontAwesomeIconProps } from '@fortawesome/react-fontawesome'
import { nanoid } from '@reduxjs/toolkit'
import { ADMIN_CLAN_ID, COLLABORATOR_CLAN_ID, MODERATOR_CLAN_ID } from 'entgamers-database/frontend/clanes/administrative'
import { createTeamApplication, type TeamApplicationData } from 'entgamers-database/frontend/database/teamApplications'
import { useFormik } from 'formik'
import { AnimatePresence, motion } from 'framer-motion'
import { useSearchParams } from 'next/navigation'
import { useEffect, type FC } from 'react'
const ApplyForm: FC = () => {
const searchParams = useSearchParams()
const { manageError } = useManageError()
const dispatch = useAppDispatch()
const formik = useFormik <Omit<TeamApplicationData, 'status'>>({
initialValues: {
name: '',
email: '',
discord: '',
message: '',
role: 'Moderator'
},
onSubmit: async (values) => {
try {
await createTeamApplication(values)
dispatch(addAlert({
id: nanoid(),
title: 'Formulario enviado',
message: 'Gracias por interesarte en unirte al equipo',
severity: 'success'
}))
} catch (error) {
manageError(error, 'Error al enviar el formulario', 'Error desconocido al enviar el formulario', 'error')
}
},
validationSchema: teamApplicationDataSchema,
validateOnMount: true
})
useEffect(() => {
if (searchParams.has('role')) {
formik.setFieldValue('role', searchParams.get('role'))
.catch((error) => {
console.error(error)
})
}
}, [])
return (
<form
onSubmit={formik.handleSubmit}
>
<div
className={css({
display: 'flex',
flexDirection: { base: 'column', md: 'row' },
justifyContent: 'space-evenly',
gap: 'medium'
})}
>
<Button
type='button'
onClick={() => {
formik.setFieldValue('role', MODERATOR_CLAN_ID)
.catch((error) => {
console.error(error)
})
}}
disabled={formik.values.role === MODERATOR_CLAN_ID}
>
Moderador
</Button>
<Button
type='button'
onClick={() => {
formik.setFieldValue('role', ADMIN_CLAN_ID)
.catch((error) => {
console.error(error)
})
}}
disabled={formik.values.role === ADMIN_CLAN_ID}
>
Administrador
</Button>
<Button
type='button'
onClick={() => {
formik.setFieldValue('role', COLLABORATOR_CLAN_ID)
.catch((error) => {
console.error(error)
})
}}
disabled={formik.values.role === 'Collaborator'}
>
Colaborador
</Button>
</div>
<div
className={css({
display: 'grid',
gridTemplateColumns: { base: '1fr', md: '1fr 1fr' },
gap: 'medium'
})}
>
{formik.submitCount <= 0
? (
<div
className={css({
order: { base: 2, md: 1 }
})}
>
<FormGroup>
<label htmlFor='name'>Nombre</label>
<Input
id='name'
type='text'
value={formik.values.name}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.touched.name !== undefined && formik.errors.name !== undefined
? (
<Typography variant='caption' color='danger'>
{formik.errors.name}
</Typography>
)
: (
<Typography variant='caption' color='info'>
Tu nombre.
</Typography>
)}
</FormGroup>
<FormGroup>
<label htmlFor='email'>Email</label>
<Input
id='email'
type='email'
value={formik.values.email}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.touched.email !== undefined && formik.errors.email !== undefined
? (
<Typography variant='caption' color='danger'>
{formik.errors.email}
</Typography>
)
: (
<Typography variant='caption' color='info'>
Tu email, para poder contactarte.
</Typography>
)
}
</FormGroup>
<FormGroup>
<label htmlFor='discord'>Nombre de Discord</label>
<Input
id='discord'
type='text'
value={formik.values.discord}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.touched.discord !== undefined && formik.errors.discord !== undefined
? (
<Typography variant='caption' color='danger'>
{formik.errors.discord}
</Typography>
)
: (
<Typography variant='caption' color='info'>
Tu nombre de Discord, para poder contactarte.
</Typography>
)
}
</FormGroup>
<FormGroup>
<label htmlFor='message'>Mensaje</label>
<TextArea
id='message'
value={formik.values.message}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.touched.message !== undefined && formik.errors.message !== undefined
? (
<Typography variant='caption' color='danger'>
{formik.errors.message}
</Typography>
)
: (
<Typography variant='caption' color='info'>
¿Por que te gustaría unirte al equipo?, ¿Que te gustaría hacer?, etc.
</Typography>
)
}
</FormGroup>
<div
className={css({
paddingBlock: 'medium'
})}
>
<Button
type='submit'
disabled={!formik.isValid || !formik.dirty}
fullWidth
>
Enviar
</Button>
</div>
</div>
)
: (
<div
className={css({
order: { base: 2, md: 1 },
paddingBlock: 'medium'
})}
>
<div
className={css({
backgroundColor: 'surface',
borderRadius: 'medium',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
})}
>
<Typography variant='h2' align="center">¡Gracias por interesarte en unirte al equipo!</Typography>
<Typography variant='body1'>
El equipo de EntGamers se pondrá en contacto contigo a la brevedad posible.
</Typography>
</div>
</div>
)
}
<div
className={css({
overflow: 'hidden',
order: { base: 1, md: 2 },
paddingBlock: 'medium'
})}
>
<AnimatePresence mode='wait' initial={false}>
{formik.values.role === MODERATOR_CLAN_ID && (
<motion.div
key={'motion-moderator'}
transition={{ duration: 0.15, ease: 'easeInOut' }}
initial={{ opacity: 0, x: '-250px' }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: '250px' }}
>
<Typography variant='h2' align="center">Moderadores</Typography>
<Typography variant='body1'>
El equipo de moderación de EntGamers se encarga de moderar los distintos espacios en los que se desenvuelve la comunidad, como los grupos de Facebook, Discord, Etc.
</Typography>
<Typography variant='h3'>Requisitos</Typography>
<ul className="fa-ul">
<li>
<FontAwesomeIcon icon={faChevronRight as FontAwesomeIconProps['icon']} fixedWidth listItem /> <strong>Imparcialidad</strong>
<br />
La comunidad esta conformada por amigos y conocidos, por lo tanto es importante poder actuar de forma imparcial y responsable.
</li>
</ul>
<Typography variant='h3'>Beneficios</Typography>
<ul className="fa-ul">
<li>
<FontAwesomeIcon icon={faChevronRight as FontAwesomeIconProps['icon']} fixedWidth listItem /> <strong>Experiencia</strong>
<br />
Uno de los objetivos de la comunidad es brindar experiencia en gestión y desarrollo de proyectos equiparable a un entorno laboral, que sea comprobable y útil.
</li>
</ul>
</motion.div>
)}
{formik.values.role === COLLABORATOR_CLAN_ID && (
<motion.div
key={'motion-collaborator'}
transition={{ duration: 0.15, ease: 'easeInOut' }}
initial={{ opacity: 0, x: '-250px' }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: '250px' }}
>
<Typography variant='h2' align='center'>Colaborador</Typography>
<Typography variant='body1'>
Los colaboradores son personas ajenas al staff central de EntGamers que nos ayudan a traer contenido, eventos y actividades a la comunidad.
</Typography>
<Typography variant='h3'>Requisitos</Typography>
<ul className="fa-ul">
<li>
<FontAwesomeIcon icon={faChevronRight as FontAwesomeIconProps['icon']} fixedWidth listItem /> <strong>Profesionalismo</strong>
<br />
La comunidad siempre intenta conseguir el mayor nivel de calidad en todos sus proyectos, por lo que buscamos gente dispuesta a otorgar este nivel de profesionalismo para el disfrute de la comunidad.
</li>
</ul>
<Typography variant='h3'>Beneficios</Typography>
<ul className="fa-ul">
<li>
<FontAwesomeIcon icon={faChevronRight as FontAwesomeIconProps['icon']} fixedWidth listItem /> <strong>Apoyo</strong>
<br />
Puedes contar con el apoyo de la comunidad para tus proyectos, ya sea en forma de difusión, asesoramiento o recursos.
</li>
</ul>
</motion.div>
)}
{formik.values.role === ADMIN_CLAN_ID && (
<motion.div
key={'motion-administrator'}
transition={{ duration: 0.15, ease: 'easeInOut' }}
initial={{ opacity: 0, x: '-250px' }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: '250px' }}
>
<Typography variant='h2' align='center'>Administradores</Typography>
<Typography variant='body1'>
Los administradores son quienes se encargan de que todo funcione como es debido en la comunidad, desde la moderación de los grupos hasta la organización de eventos y actividades. Son los responsables de que la comunidad siga creciendo y mejorando.
</Typography>
<Typography variant='h3'>Requisitos</Typography>
<ul className="fa-ul">
<li>
<FontAwesomeIcon icon={faChevronRight as FontAwesomeIconProps['icon']} fixedWidth listItem /> <strong>Profesionalismo</strong>
<br />
La comunidad siempre intenta conseguir el mayor nivel de calidad en todos sus proyectos, por lo que buscamos gente dispuesta a otorgar este nivel de profesionalismo para el disfrute de la comunidad.
</li>
<li>
<FontAwesomeIcon icon={faChevronRight as FontAwesomeIconProps['icon']} fixedWidth listItem /> <strong>Constancia</strong>
<br />
La comunidad busca gente que en sus posibilidades sea activa, que pueda estar al tanto de lo que pasa en ella.
</li>
<li>
<FontAwesomeIcon icon={faChevronRight as FontAwesomeIconProps['icon']} fixedWidth listItem /> <strong>Proactividad</strong>
<br />
La comunidad esta en constante crecimiento, por eso, buscamos gente que ayude a buscar nuevas oportunidades para diferentes proyectos y actividades de interés a la comunidad.
</li>
</ul>
<Typography variant='h3'>Beneficios</Typography>
<ul className="fa-ul">
<li>
<FontAwesomeIcon icon={faChevronRight as FontAwesomeIconProps['icon']} fixedWidth listItem /> <strong>Experiencia</strong>
<br />
Uno de los objetivos de la comunidad es brindar experiencia en gestión y desarrollo de proyectos equiparable a un entorno laboral, que sea comprobable y útil.
</li>
<li>
<FontAwesomeIcon icon={faChevronRight as FontAwesomeIconProps['icon']} fixedWidth listItem /> <strong>Capacitación</strong>
<br />
La comunidad buscara dar capacitación a sus miembros en lo referido a herramientas y procedimientos utilizados.
</li>
<li></li>
</ul>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</form>
)
}
export default ApplyForm
+20
View File
@@ -0,0 +1,20 @@
import Typography from '@/components/ui/Typography'
import { Container } from '@/styled-system/jsx'
import { ensureTeamApplicationsCollection } from 'entgamers-database/backend/database/teamApplications'
import { type FC } from 'react'
import ApplyForm from './ApplyForm'
const EquipoUnirsePage: FC = async () => {
await ensureTeamApplicationsCollection()
return (
<Container>
<Typography variant="h1" align="center">Únete al Bosque</Typography>
<Typography variant="body1">
El equipo de EntGamers está formado por personas que se dedican a la administración de la comunidad, a la organización de eventos y a la creación de contenido. Aquí podrás enterarte cuales son las funciones de cada uno de los miembros del equipo y como puedes unirte a nosotros.
</Typography>
<ApplyForm />
</Container>
)
}
export default EquipoUnirsePage

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

+1
View File
@@ -0,0 +1 @@
@layer reset, base, tokens, recipes, utilities;
+51
View File
@@ -0,0 +1,51 @@
import '@/app/global.css'
import Footer from '@/components/layout/Footer'
import Header from '@/components/layout/Header'
import { css } from '@/styled-system/css'
import '@fontsource/open-sans/latin-300.css'
import '@fontsource/open-sans/latin-400.css'
import '@fontsource/open-sans/latin-700.css'
import '@fontsource/permanent-marker/latin-400.css'
import { config } from '@fortawesome/fontawesome-svg-core'
import '@fortawesome/fontawesome-svg-core/styles.css'
import { type Metadata } from 'next'
import { type FC, type ReactNode } from 'react'
import FeedbackConsumer from './FeedbackConsumer'
import SessionConsumer from './SessionConsumer'
import StateProvider from './StateProvider'
config.autoAddCss = false
export const metadata: Metadata = {
title: 'Home | EntGamers',
description: 'Una comunidad de jugadores, para jugadores',
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL ?? 'https://entgamers.pro')
}
interface RootLayoutProps {
children: ReactNode
}
const RootLayout: FC<RootLayoutProps> = ({ children }) => {
return (
<html lang="en">
<body>
<StateProvider>
<Header />
<main
className={css({
paddingBlock: 'medium',
minHeight: 'calc(100vh - 60px - 72px)'
})}
>
{children}
</main>
<Footer />
<FeedbackConsumer />
<SessionConsumer />
</StateProvider>
</body>
</html>
)
}
export default RootLayout
+110
View File
@@ -0,0 +1,110 @@
'use client'
import Button from '@/components/ui/Button'
import Typography from '@/components/ui/Typography'
import FormGroup from '@/components/ui/form/FormGroup'
import Input from '@/components/ui/form/Input'
import PasswordInput from '@/components/ui/form/PasswordInput'
import { useAppDispatch } from '@/hooks/useAppDispatch'
import { useAppSelector } from '@/hooks/useAppSelector'
import useManageError from '@/hooks/useManageError'
import { setCurrentUser, setSession, setStatus } from '@/state/sessionSlice'
import { getCurrentUser, login } from 'entgamers-database/frontend/session'
import { useFormik } from 'formik'
import NextLink from 'next/link'
import { useRouter } from 'next/navigation'
import { useEffect, type FC } from 'react'
import { object, string } from 'yup'
interface LoginData {
email: string
password: string
}
const loginSchema = object({
email: string().email('El correo electrónico no es válido').required('El correo electrónico es requerido'),
password: string().required('La contraseña es requerida')
})
const LoginForm: FC = () => {
const dispatch = useAppDispatch()
const { manageError } = useManageError()
const session = useAppSelector((state) => state.session)
const router = useRouter()
const formik = useFormik<LoginData>({
initialValues: {
email: '',
password: ''
},
onSubmit: async ({ email, password }) => {
dispatch(setStatus('loading'))
try {
const session = await login(email, password)
const user = await getCurrentUser()
dispatch(setSession(session))
dispatch(setCurrentUser(user))
} catch (error) {
manageError(error, 'Error mientras se iniciaba sesión', 'Error desconocido mientras se iniciaba sesión', 'error')
} finally {
dispatch(setStatus('idle'))
}
},
validationSchema: loginSchema,
validateOnMount: true
})
useEffect(() => {
if (session.status === 'idle' && session.session !== undefined) {
router.push('/')
}
}, [session])
return (
<form
onSubmit={formik.handleSubmit}
>
<FormGroup>
<label htmlFor="email">Correo electrónico</label>
<Input
id="email"
name="email"
type="email"
onChange={formik.handleChange}
value={formik.values.email}
status={formik.touched.email !== undefined && formik.errors.email !== undefined ? 'danger' : undefined}
onBlur={formik.handleBlur}
fullWidth
/>
{formik.touched.email !== undefined && formik.errors.email !== undefined && (
<Typography variant="caption" color="danger">{formik.errors.email}</Typography>
)}
</FormGroup>
<FormGroup>
<label htmlFor="password">Contraseña</label>
<PasswordInput
id="password"
name="password"
onChange={formik.handleChange}
value={formik.values.password}
status={formik.touched.password !== undefined && formik.errors.password !== undefined ? 'danger' : undefined}
onBlur={formik.handleBlur}
fullWidth
/>
{formik.touched.password !== undefined && formik.errors.password !== undefined && (
<Typography variant="caption" color="danger">{formik.errors.password}</Typography>
)}
</FormGroup>
<Typography variant="caption" color="muted">
¿Perdiste tu contraseña? <NextLink href="/recover-password">Recupérala</NextLink>
</Typography>
<FormGroup>
<Button
type="submit"
>
Iniciar sesión
</Button>
</FormGroup>
</form>
)
}
export default LoginForm
+53
View File
@@ -0,0 +1,53 @@
import LoginForm from '@/app/login/LoginForm'
import Typography from '@/components/ui/Typography'
import { css, cx } from '@/styled-system/css'
import { Center } from '@/styled-system/jsx'
import { container } from '@/styled-system/patterns'
import { card } from '@/styled-system/recipes'
import NextLink from 'next/link'
import { type FC } from 'react'
const LoginPage: FC = () => {
return (
<>
<Center
className={cx(
container(),
css({
minHeight: 'calc(100vh - 60px - 72px)'
}))
}
>
<div
className={cx(
card().body,
css({
width: '100%',
maxWidth: { sm: 'breakpoint-sm' },
overflow: 'visible'
})
)}
>
<div
className={
card().header
}
>
<Typography align="center" variant="h1">
Iniciar sesión
</Typography>
</div>
<div
className={card().content}
>
<LoginForm />
<Typography variant="caption" align="center" >
¿No tienes una cuenta? <NextLink href="/register">Regístrate</NextLink>
</Typography>
</div>
</div>
</Center>
</>
)
}
export default LoginPage
+22
View File
@@ -0,0 +1,22 @@
import Typography from '@/components/ui/Typography'
import { css } from '@/styled-system/css'
import { Center } from '@/styled-system/jsx'
import { button } from '@/styled-system/recipes'
import NextLink from 'next/link'
import { type FC } from 'react'
const NotFoundPage: FC = () => {
return (
<Center
flexDirection="column"
width="100%"
height="calc(100vh - 60px - 72px)"
gap="medium"
>
<Typography variant="h1" className={css({ fontSize: '100px' })}>404</Typography>
<Typography variant="h2" color="text">El árbol que buscas no está aquí</Typography>
<NextLink className={button({})} href="/">Volver Al inicio</NextLink>
</Center>
)
}
export default NotFoundPage
+1
View File
@@ -0,0 +1 @@
EntGamers
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

+18
View File
@@ -0,0 +1,18 @@
import Clanes from '@/app/Clanes'
import Hero from '@/app/Hero'
import Social from '@/app/Social'
import Team from '@/app/Team'
import { type FC } from 'react'
const HomePage: FC = async () => {
return (
<>
<Hero />
<Clanes />
<Social />
<Team />
</>
)
}
export default HomePage
@@ -0,0 +1,88 @@
'use client'
import Button from '@/components/ui/Button'
import Typography from '@/components/ui/Typography'
import FormGroup from '@/components/ui/form/FormGroup'
import Input from '@/components/ui/form/Input'
import { useAppDispatch } from '@/hooks/useAppDispatch'
import useManageError from '@/hooks/useManageError'
import { addAlert } from '@/state/feedbackSlice'
import { nanoid } from '@reduxjs/toolkit'
import { createPasswordRecovery } from 'entgamers-database/frontend/session'
import { useFormik } from 'formik'
import { type FC } from 'react'
import { object, string } from 'yup'
interface RecoverPasswordData {
email: string
}
const recoverPasswordSchema = object({
email: string().email('El correo electrónico no es válido').required('El correo electrónico es requerido')
})
const CreateRecoverPasswordForm: FC = () => {
const { manageError } = useManageError()
const dispatch = useAppDispatch()
const formik = useFormik<RecoverPasswordData>({
initialValues: {
email: ''
},
onSubmit: async ({ email }) => {
try {
const callBackUrl = `${process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000'}/recover-password`
await createPasswordRecovery(email, callBackUrl)
dispatch(addAlert({
id: nanoid(),
title: 'Solicitud de recuperación de contraseña enviada',
message: 'Si el correo electrónico está registrado, se enviarán instrucciones para la recuperación de contraseña',
severity: 'success'
}))
} catch (error) {
manageError(error, 'Error mientras se registraba', 'Error desconocido mientras se registraba', 'error')
}
},
validationSchema: recoverPasswordSchema,
validateOnMount: true
})
return (
<form
onSubmit={formik.handleSubmit}
>
<FormGroup>
<label htmlFor="email">
Correo electrónico
</label>
<Input
id="email"
name="email"
type="email"
value={formik.values.email}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
status={formik.touched.email !== undefined && formik.errors.email !== undefined ? 'danger' : 'success'}
/>
{formik.touched.email !== undefined && formik.errors.email !== undefined
? (
<Typography variant="caption" color="danger">
{formik.errors.email}
</Typography>
)
: (<Typography variant="caption" color="info">
Por favor, introduce el correo electrónico con el que te has registrado. Te enviaremos un correo con instrucciones para la recuperación de contraseña
</Typography>)
}
</FormGroup>
<FormGroup>
<Button
type="submit"
disabled={formik.isSubmitting}
>
Enviar correo de recuperación
</Button>
</FormGroup>
</form>
)
}
export default CreateRecoverPasswordForm
@@ -0,0 +1,28 @@
'use client'
import CreateRecoverPasswordForm from '@/app/recover-password/CreateRecoverPasswordForm'
import { useSearchParams } from 'next/navigation'
import { useEffect, useState, type FC } from 'react'
import UpdateRecoverPasswordForm, { type UpdateRecoverPasswordFormProps } from './UpdateRecoverPasswordForm'
const ManageRecoverPassword: FC = () => {
const [recoverData, setRecoverData] = useState<UpdateRecoverPasswordFormProps | undefined>()
const searchParams = useSearchParams()
useEffect(() => {
const userId = searchParams.get('userId')
const secret = searchParams.get('secret')
if (userId !== null && secret !== null) {
setRecoverData({ userId, secret })
}
}, [])
if (recoverData === undefined) {
return <CreateRecoverPasswordForm />
} else {
return <UpdateRecoverPasswordForm
{...recoverData}
/>
}
}
export default ManageRecoverPassword
@@ -0,0 +1,126 @@
import Button from '@/components/ui/Button'
import Typography from '@/components/ui/Typography'
import FormGroup from '@/components/ui/form/FormGroup'
import PasswordInput from '@/components/ui/form/PasswordInput'
import { useAppDispatch } from '@/hooks/useAppDispatch'
import useManageError from '@/hooks/useManageError'
import { addAlert } from '@/state/feedbackSlice'
import { nanoid } from '@reduxjs/toolkit'
import { updatePasswordRecovery } from 'entgamers-database/frontend/session'
import { useFormik } from 'formik'
import { useRouter } from 'next/navigation'
import { type FC } from 'react'
import { object, ref, string } from 'yup'
export interface UpdateRecoverPasswordFormProps {
userId: string
secret: string
}
interface UpdateRecoverPasswordData extends UpdateRecoverPasswordFormProps {
password: string
confirmPassword: string
}
const updateRecoverPasswordSchema = object({
password: string()
.min(6, 'La contraseña debe tener al menos 6 caracteres')
.matches(/[a-z]/, 'La contraseña debe tener al menos una letra minúscula')
.matches(/[A-Z]/, 'La contraseña debe tener al menos una letra mayúscula')
.matches(/[0-9]/, 'La contraseña debe tener al menos un número')
.required('La contraseña es requerida'),
confirmPassword: string().oneOf([ref('password')], 'Las contraseñas no coinciden').required('La confirmación de la contraseña es requerida')
})
const UpdateRecoverPasswordForm: FC<UpdateRecoverPasswordFormProps> = (props) => {
const dispatch = useAppDispatch()
const { manageError } = useManageError()
const router = useRouter()
const formik = useFormik<UpdateRecoverPasswordData>({
initialValues: {
password: '',
confirmPassword: '',
userId: props.userId,
secret: props.secret
},
onSubmit: async ({ confirmPassword, password, secret, userId }) => {
try {
if (password !== confirmPassword) throw new Error('Las contraseñas no coinciden')
await updatePasswordRecovery(userId, secret, password)
dispatch(addAlert({
id: nanoid(),
title: 'Contraseña actualizada',
message: 'Ahora puedes iniciar sesión',
severity: 'success'
}))
router.push('/login')
} catch (error) {
manageError(error, 'Error al recuperar contraseña', 'Error desconocido mientras se solicitaba la recuperación de contraseña', 'error')
}
},
validationSchema: updateRecoverPasswordSchema,
validateOnMount: true
})
return (
<form
onSubmit={formik.handleSubmit}
>
<FormGroup>
<label htmlFor="password">
Contraseña
</label>
<PasswordInput
id="password"
name="password"
value={formik.values.password}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
status={formik.touched.password !== undefined && formik.errors.password !== undefined ? 'danger' : undefined}
fullWidth
/>
{formik.touched.password !== undefined && formik.errors.password !== undefined
? (
<Typography variant="caption" color="danger">
{formik.errors.password}
</Typography>
)
: (
<Typography variant="caption" color="info">
Escribe tu nueva contraseña
</Typography>
)
}
</FormGroup>
<FormGroup>
<label htmlFor="confirmPassword">
Confirmar contraseña
</label>
<PasswordInput
id="confirmPassword"
name="confirmPassword"
value={formik.values.confirmPassword}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
status={formik.touched.confirmPassword !== undefined && formik.errors.confirmPassword !== undefined ? 'danger' : undefined}
fullWidth
/>
{formik.touched.confirmPassword !== undefined && formik.errors.confirmPassword !== undefined && (
<Typography variant="caption" color="danger">
{formik.errors.confirmPassword}
</Typography>
)}
</FormGroup>
<FormGroup>
<Button
type="submit"
disabled={formik.isSubmitting}
>
Actualizar contraseña
</Button>
</FormGroup>
</form>
)
}
export default UpdateRecoverPasswordForm
+46
View File
@@ -0,0 +1,46 @@
import ManageRecoverPassword from '@/app/recover-password/ManageRecoverPassword'
import Typography from '@/components/ui/Typography'
import { css, cx } from '@/styled-system/css'
import { Center } from '@/styled-system/jsx'
import { container } from '@/styled-system/patterns'
import { card } from '@/styled-system/recipes'
import { type FC } from 'react'
const page: FC = () => {
return (
<Center
className={cx(
container(),
css({
minHeight: 'calc(100vh - 60px - 72px)'
}))
}
>
<div
className={cx(
card().body,
css({
width: '100%',
maxWidth: { sm: 'breakpoint-sm' },
overflow: 'visible'
})
)}
>
<div
className={
card().header
}
>
<Typography variant="h1" align="center">Recuperar contraseña</Typography>
</div>
<div
className={card().content}
>
<ManageRecoverPassword />
</div>
</div>
</Center>
)
}
export default page
+121
View File
@@ -0,0 +1,121 @@
'use client'
import Button from '@/components/ui/Button'
import Typography from '@/components/ui/Typography'
import FormGroup from '@/components/ui/form/FormGroup'
import Input from '@/components/ui/form/Input'
import PasswordInput from '@/components/ui/form/PasswordInput'
import { useAppDispatch } from '@/hooks/useAppDispatch'
import useManageError from '@/hooks/useManageError'
import { addAlert } from '@/state/feedbackSlice'
import { nanoid } from '@reduxjs/toolkit'
import { register } from 'entgamers-database/frontend/session'
import { useFormik } from 'formik'
import { type FC } from 'react'
import { object, ref, string } from 'yup'
interface RegisterData {
email: string
password: string
passwordConfirmation: string
}
const RegisterSchema = object({
email: string().email('El correo electrónico no es válido').required('El correo electrónico es requerido'),
password: string()
.min(6, 'La contraseña debe tener al menos 6 caracteres')
.matches(/[a-z]/, 'La contraseña debe tener al menos una letra minúscula')
.matches(/[A-Z]/, 'La contraseña debe tener al menos una letra mayúscula')
.matches(/[0-9]/, 'La contraseña debe tener al menos un número')
.required('La contraseña es requerida'),
passwordConfirmation: string().oneOf([ref('password')], 'Las contraseñas no coinciden').required('La confirmación de la contraseña es requerida')
})
const RegisterForm: FC = () => {
const dispatch = useAppDispatch()
const { manageError } = useManageError()
const formik = useFormik<RegisterData>({
initialValues: {
email: '',
password: '',
passwordConfirmation: ''
},
onSubmit: async ({ email, password }) => {
try {
await register(email, password)
dispatch(addAlert({
id: nanoid(),
title: 'Registro completado',
message: 'Ahora puedes iniciar sesión',
severity: 'success'
}))
formik.resetForm()
} catch (error) {
manageError(error, 'Error mientras se registraba', 'Error desconocido mientras se registraba', 'error')
}
},
validationSchema: RegisterSchema,
validateOnMount: true
})
return (
<form
onSubmit={formik.handleSubmit}
>
<FormGroup>
<label htmlFor="email">Correo electrónico</label>
<Input
id="email"
name="email"
type="email"
onChange={formik.handleChange}
value={formik.values.email}
status={formik.touched.email !== undefined && formik.errors.email !== undefined ? 'danger' : undefined}
onBlur={formik.handleBlur}
fullWidth
/>
{formik.touched.email !== undefined && formik.errors.email !== undefined && (
<Typography variant="caption" color="danger">{formik.errors.email}</Typography>
)}
</FormGroup>
<FormGroup>
<label htmlFor="password">Contraseña</label>
<PasswordInput
id="password"
name="password"
onChange={formik.handleChange}
value={formik.values.password}
status={formik.touched.password !== undefined && formik.errors.password !== undefined ? 'danger' : undefined}
onBlur={formik.handleBlur}
fullWidth
/>
{formik.touched.password !== undefined && formik.errors.password !== undefined && (
<Typography variant="caption" color="danger">{formik.errors.password}</Typography>
)}
</FormGroup>
<FormGroup>
<label htmlFor="passwordConfirmation">Confirmación de la contraseña</label>
<PasswordInput
id="passwordConfirmation"
name="passwordConfirmation"
onChange={formik.handleChange}
value={formik.values.passwordConfirmation}
status={formik.touched.passwordConfirmation !== undefined && formik.errors.passwordConfirmation !== undefined ? 'danger' : undefined}
onBlur={formik.handleBlur}
fullWidth
/>
{formik.touched.passwordConfirmation !== undefined && formik.errors.passwordConfirmation !== undefined && (
<Typography variant="caption" color="danger">{formik.errors.passwordConfirmation}</Typography>
)}
</FormGroup>
<FormGroup>
<Button
type="submit"
>
Registrarse
</Button>
</FormGroup>
</form>
)
}
export default RegisterForm
+54
View File
@@ -0,0 +1,54 @@
import RegisterForm from '@/app/register/RegisterForm'
import Typography from '@/components/ui/Typography'
import { css, cx } from '@/styled-system/css'
import { Center } from '@/styled-system/jsx'
import { container } from '@/styled-system/patterns'
import { card } from '@/styled-system/recipes'
import NextLink from 'next/link'
import { type FC } from 'react'
const RegisterPage: FC = () => {
return (
<>
<Center
className={cx(
container(),
css({
minHeight: 'calc(100vh - 60px - 72px)'
}))
}
>
<div
className={cx(
card().body,
css({
width: '100%',
maxWidth: { sm: 'breakpoint-sm' },
overflow: 'visible'
})
)}
>
<div
className={
card().header
}
>
<Typography align="center" variant="h1">
Regístrate
</Typography>
</div>
<div
className={card().content}
>
<RegisterForm />
<Typography variant="caption" align="center" >
¿Ya tienes una cuenta? <NextLink href="/login">Inicia sesión</NextLink>
</Typography>
</div>
</div>
</Center>
</>
)
}
export default RegisterPage
+1
View File
@@ -0,0 +1 @@
EntGamers
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File
+15
View File
@@ -0,0 +1,15 @@
import { type IconDefinition } from '@fortawesome/fontawesome-svg-core'
const trees: IconDefinition = {
icon: [
640,
512,
[],
'f7c5',
'M298.4 288H329c9 0 17-5 20.88-13c3.75-8.125 2.5-17.38-3.375-24.12L268.4 160h28.88c9.127 0 17.38-5.375 20.88-13.62c3.625-8.125 1.875-17.62-4.25-24.12L203.6 4.875c-6-6.5-17.25-6.5-23.25 0L69.97 122.3c-6 6.5-7.75 16-4.125 24.12C69.34 154.6 77.59 160 86.72 160h28.88L37.46 250.9c-5.875 6.875-7.125 16-3.375 24.12C37.96 283 45.84 288 54.96 288h30.63l-79.88 90.5c-6 6.75-7.377 16.12-3.625 24.25C5.834 410.8 14.08 416 23.09 416H160v64C160 497.7 174.3 512 192 512S224 497.7 224 480V416h136.9c9 0 17.25-5.25 21-13.25c3.75-8.125 2.5-17.5-3.5-24.25L298.4 288zM634.3 378.5L554.4 288h30.63c9 0 17-5 20.88-13c3.75-8.125 2.5-17.38-3.375-24.12L524.4 160h28.88c9.125 0 17.38-5.375 20.88-13.62c3.625-8.125 1.875-17.62-4.25-24.12l-110.3-117.4c-6-6.5-17.25-6.5-23.25 0l-95.14 101.3c11.13 15.38 14 35.25 6.377 52.88c-4 9.375-10.38 17.12-18.25 22.75l41.5 48.25c14 16.25 17.13 39.25 8.002 58.62c-4.25 8.875-10.5 16.12-18.13 21.5l41.63 47.13c8.625 9.875 13.37 14.2 13.62 26.7L416 480C416 497.7 430.3 512 448 512C465.7 512 480 497.7 480 480V416h136.9c9.002 0 17.25-5.25 21-13.25C641.7 394.6 640.3 385.3 634.3 378.5z'
],
iconName: 'trees',
prefix: 'fas'
}
export default trees
+3 -3
View File
@@ -1,6 +1,6 @@
import { FC, SVGProps } from 'react'
import { type FC, type SVGProps } from 'react'
const SvgComponent:FC<SVGProps<SVGSVGElement>> = (props) => (
const EntGamers: FC<SVGProps<SVGSVGElement>> = (props) => (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
@@ -375,4 +375,4 @@ const SvgComponent:FC<SVGProps<SVGSVGElement>> = (props) => (
</svg>
)
export default SvgComponent
export default EntGamers
View File
-38
View File
@@ -1,38 +0,0 @@
import Head from 'next/head'
import { FC } from 'react'
export type SeoProps = {
title?: string
description?: string
image?: string
}
const SITE_NAME = process.env.SITE_NAME || 'EntGamers'
const Seo: FC<SeoProps> = ({ title, description, image }) => {
return (
<Head>
{!!title && (
<>
<title key="title">{`${title} - ${SITE_NAME}`}</title>
<meta key="og_title" property="og:title" content={title} />
<meta key="twitter_title" property="twitter:title" content={title} />
</>
)}
{!!description && (
<>
<meta key="description" name="description" content={description} />
<meta key="og_description" property="og:description" content={description} />
<meta key="twitter_description" property="twitter:description" content={description} />
</>
)}
{!!image && (
<>
<meta key="og_image" property="og:image" content={image} />
<meta key="twitter_image" property="twitter:image" content={image} />
</>
)}
</Head>
)
}
export default Seo
+50
View File
@@ -0,0 +1,50 @@
import Typography from '@/components/ui/Typography'
import { css } from '@/styled-system/css'
import { Container } from '@/styled-system/jsx'
import { faChevronRight, faHeart } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import NextLink from 'next/link'
import { type FC } from 'react'
const Footer: FC = () => {
return (
<footer
className={css({
backgroundColor: 'surface',
color: 'text',
paddingY: 'medium'
})}
>
<Container
className={css({
display: 'grid',
gridTemplateColumns: { base: 'repeat(3, 1fr)', mdDown: '1fr' }
})}
>
<div>
<Typography variant="h3" component='div'> Acerca de </Typography>
<ul className="fa-ul">
<li><FontAwesomeIcon icon={faChevronRight} listItem fixedWidth /><NextLink href="/acerca-de"> EntGamers</NextLink></li>
<li><FontAwesomeIcon icon={faChevronRight} listItem fixedWidth /><NextLink href="/clanes"> Clanes</NextLink></li>
</ul>
</div>
<div>
<Typography variant="h3" component='div'> Contacto </Typography>
</div>
<div></div>
</Container>
<Container
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
})}
>
<Typography variant="body2" component='div'>
Hecho con <FontAwesomeIcon className={css({ color: 'red' })} icon={faHeart} /> por <a href="https://srjuggernaut.dev">SrJuggernaut</a>
</Typography>
</Container>
</footer>
)
}
export default Footer

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