Browse Source

first commit

c90Beretta 4 months ago
commit
0c4e97c779
47 changed files with 1813 additions and 0 deletions
  1. 5 0
      .firebaserc
  2. 27 0
      .gitignore
  3. 4 0
      .vscode/extensions.json
  4. 11 0
      .vscode/launch.json
  5. 54 0
      README.md
  6. 11 0
      astro.config.mjs
  7. 11 0
      firebase.json
  8. 25 0
      package.json
  9. 9 0
      public/favicon.svg
  10. BIN
      public/icon/fb_icon.png
  11. BIN
      public/icon/ig_icon.png
  12. BIN
      public/icon/slider_1.jpg
  13. BIN
      public/icon/wp_icon.png
  14. BIN
      public/icon/yt_icon.png
  15. BIN
      public/img/legislatura_64.png
  16. BIN
      public/img/logo.png
  17. 29 0
      src/components/Card.astro
  18. 41 0
      src/components/CardIcon.astro
  19. 35 0
      src/components/CardList.jsx
  20. 37 0
      src/components/CardNews.astro
  21. 28 0
      src/components/CardUser.astro
  22. 97 0
      src/components/Collapse.astro
  23. 66 0
      src/components/Dropdown.astro
  24. 3 0
      src/components/HeroBanner.astro
  25. 47 0
      src/components/LinedCard.astro
  26. 258 0
      src/components/Navbar.astro
  27. 30 0
      src/components/SearchBar.astro
  28. 85 0
      src/components/Slider.tsx
  29. 89 0
      src/constants/file-types.ts
  30. 59 0
      src/constants/http-status-codes.ts
  31. 18 0
      src/constants/index.ts
  32. 1 0
      src/env.d.ts
  33. 134 0
      src/hooks/useHttp.jsx
  34. 113 0
      src/layouts/Layout.astro
  35. 24 0
      src/models/Home.model.ts
  36. 30 0
      src/models/Noticia.model.ts
  37. 24 0
      src/models/Publicacion.model.ts
  38. 28 0
      src/models/QuienesSomos.model.ts
  39. 18 0
      src/models/Slide.model.ts
  40. 10 0
      src/pages/index.astro
  41. 74 0
      src/pages/licitacion/index.astro
  42. 242 0
      src/services/httpService.ts
  43. 1 0
      src/services/index.js
  44. 5 0
      src/styles/global.css
  45. 15 0
      src/types/responses.d.ts
  46. 8 0
      tailwind.config.mjs
  47. 7 0
      tsconfig.json

+ 5 - 0
.firebaserc

@@ -0,0 +1,5 @@
+{
+  "projects": {
+    "default": ""
+  }
+}

+ 27 - 0
.gitignore

@@ -0,0 +1,27 @@
+# build output
+dist/
+
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
+
+yarn.lock
+.firebase

+ 4 - 0
.vscode/extensions.json

@@ -0,0 +1,4 @@
+{
+  "recommendations": ["astro-build.astro-vscode"],
+  "unwantedRecommendations": []
+}

+ 11 - 0
.vscode/launch.json

@@ -0,0 +1,11 @@
+{
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "command": "./node_modules/.bin/astro dev",
+      "name": "Development server",
+      "request": "launch",
+      "type": "node-terminal"
+    }
+  ]
+}

+ 54 - 0
README.md

@@ -0,0 +1,54 @@
+# Astro Starter Kit: Basics
+
+```sh
+npm create astro@latest -- --template basics
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
+
+> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
+
+![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)
+
+## 🚀 Project Structure
+
+Inside of your Astro project, you'll see the following folders and files:
+
+```text
+/
+├── public/
+│   └── favicon.svg
+├── src/
+│   ├── components/
+│   │   └── Card.astro
+│   ├── layouts/
+│   │   └── Layout.astro
+│   └── pages/
+│       └── index.astro
+└── package.json
+```
+
+Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
+
+There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
+
+Any static assets, like images, can be placed in the `public/` directory.
+
+## 🧞 Commands
+
+All commands are run from the root of the project, from a terminal:
+
+| Command                   | Action                                           |
+| :------------------------ | :----------------------------------------------- |
+| `npm install`             | Installs dependencies                            |
+| `npm run dev`             | Starts local dev server at `localhost:4321`      |
+| `npm run build`           | Build your production site to `./dist/`          |
+| `npm run preview`         | Preview your build locally, before deploying     |
+| `npm run astro ...`       | Run CLI commands like `astro add`, `astro check` |
+| `npm run astro -- --help` | Get help using the Astro CLI                     |
+
+## 👀 Want to learn more?
+
+Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

+ 11 - 0
astro.config.mjs

@@ -0,0 +1,11 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+
+import tailwind from '@astrojs/tailwind';
+
+import react from '@astrojs/react';
+
+// https://astro.build/config
+export default defineConfig({
+  integrations: [tailwind(), react()]
+});

+ 11 - 0
firebase.json

@@ -0,0 +1,11 @@
+{
+  "hosting": {
+    "public": "dist",
+    "site": "",
+    "ignore": [
+      "firebase.json",
+      "**/.*",
+      "**/node_modules/**"
+    ]
+  }
+}

+ 25 - 0
package.json

@@ -0,0 +1,25 @@
+{
+  "name": "cale-astro",
+  "type": "module",
+  "version": "0.0.1",
+  "scripts": {
+    "dev": "astro dev",
+    "start": "astro dev",
+    "build": "astro check && astro build",
+    "preview": "astro preview",
+    "astro": "astro"
+  },
+  "dependencies": {
+    "@astrojs/check": "^0.9.4",
+    "@astrojs/react": "^3.6.2",
+    "@astrojs/tailwind": "^5.1.2",
+    "@types/react": "^18.3.12",
+    "@types/react-dom": "^18.3.1",
+    "astro": "^4.16.9",
+    "react": "^18.3.1",
+    "react-dom": "^18.3.1",
+    "react-html-parser": "^2.0.2",
+    "tailwindcss": "^3.4.14",
+    "typescript": "^5.6.3"
+  }
+}

File diff suppressed because it is too large
+ 9 - 0
public/favicon.svg


BIN
public/icon/fb_icon.png


BIN
public/icon/ig_icon.png


BIN
public/icon/slider_1.jpg


BIN
public/icon/wp_icon.png


BIN
public/icon/yt_icon.png


BIN
public/img/legislatura_64.png


BIN
public/img/logo.png


+ 29 - 0
src/components/Card.astro

@@ -0,0 +1,29 @@
+---
+interface Props {
+  titulo: string;
+  imagen?: string;
+  datos?: string;
+  color?: string;
+}
+
+const { titulo, imagen, color } = Astro.props;
+---
+
+<a
+  class="w-full h-full flex flex-col p-5 space-y-4 bg-stone-50 border-2 border-stone-50 rounded-md shadow-md shadow-slate-400"
+>
+  {
+    imagen && (
+      <div class="w-full h-auto flex flex-row justify-center">
+        <img class={`w-auto h-48 p-2 ${color}`} src={imagen} />
+      </div>
+    )
+  }
+  <div class="w-full">
+    <h2 class="text-2xl font-bold">
+      {titulo}
+    </h2>
+  </div>
+  <hr class="my-2" />
+  <slot />
+</a>

+ 41 - 0
src/components/CardIcon.astro

@@ -0,0 +1,41 @@
+---
+interface Props {
+  nombre: string;
+  puesto?: string;
+  icono?: string;
+  contacto?: boolean;
+}
+
+const { nombre, puesto, icono, contacto } = Astro.props;
+---
+
+<a
+  class="w-full h-full space-x-4 grid grid-cols-12 p-5 space-y-4 bg-stone-50 border-2 border-stone-50 rounded-md shadow-md shadow-slate-400"
+>
+  <div class="w-full h-auto flex flex-row justify-center col-span-4">
+    <div class="overflow-hidden h-32 w-32 rounded-full">
+      <img class="w-full h-full object-cover" src={icono} />
+    </div>
+  </div>
+  <div class="w-full flex flex-col col-span-8">
+    <h2 class="text-xl font-bold">
+      {nombre}
+    </h2>
+    <p class="text-lg font-semibold text-gray-400">
+      {puesto}
+    </p>
+  </div>
+  <hr class="my-2 col-span-12" />
+  {
+    contacto && (
+      <div class="flex flex-col">
+        <p>
+          <strong>Teléfono:</strong> 662234455
+        </p>
+        <p>
+          <strong>Correo:</strong> correo@ejemplo.com
+        </p>
+      </div>
+    )
+  }
+</a>

+ 35 - 0
src/components/CardList.jsx

@@ -0,0 +1,35 @@
+import React, { useEffect, useState } from "react";
+import { http } from "../services/httpService";
+// import ReactHtmlParser from "react-html-parser" ;
+
+const CardList = () => {
+  const [publicaciones, setPublicaciones] = useState([]);
+
+  const getPublicaciones = async () => {
+    try {
+      const res = await http.get(
+        "publico/publicacion?ordenar=creado-asc",
+        false
+      );
+      if (res.status === 200) {
+        setPublicaciones(res?.resultado);
+      }
+    } catch (e) {
+      console.log(e);
+    }
+  };
+
+  useEffect(() => {
+    getPublicaciones();
+    return () => {};
+  }, []);
+
+  return publicaciones.map((publicacion) => (
+    <div className="h-60 w-80 border-2 border-stone-400 border-r-2 m-2 p-2 overflow-hidden">
+      <p className="font-bold text-lg">{publicacion?.nombre}</p>
+      <p className="text-lg text-ellipsis">{publicacion?.descripcion}</p>
+    </div>
+  ));
+};
+
+export default CardList;

+ 37 - 0
src/components/CardNews.astro

@@ -0,0 +1,37 @@
+---
+interface Props {
+  id: string;
+  title: string;
+  description?: string;
+  content?: string;
+  image?: string;
+  date?: string;
+}
+
+const { id, title, description, content, image, date } = Astro.props;
+---
+
+<div class="grid grid-cols-12 p-4 duration-200 pace-x-5 space-y-4">
+  <div class="col-span-4 overflow-hidden h-96">
+    <img
+      src={image}
+      class="object-cover object-center shadow-md shadow-neutral-500"
+    />
+  </div>
+  <div class="col-span-8 flex flex-col px-4 items-end">
+    <a class="w-full" href=`/actividades/actividad?nota=${id}`>
+      <h2 class="text-2xl font-bold text-justify">{title}</h2>
+    </a>
+    <div class="w-full">
+      <p class="text-xl font-semibold text-justify" set:html={description} />
+    </div>
+    <div class="w-full h-full text-justify">
+      <strong>{date}</strong>
+    </div>
+    <div
+      class="p-3 bg-yellow-950 font-bold text-white rounded-md cursor-pointer"
+    >
+      Comentarios
+    </div>
+  </div>
+</div>

+ 28 - 0
src/components/CardUser.astro

@@ -0,0 +1,28 @@
+---
+interface Props {
+  puesto: string;
+  nombre: string;
+  imagen?: string;
+}
+
+const { puesto, imagen, nombre } = Astro.props;
+---
+
+<a
+  class="w-full h-full flex flex-col p-5 space-y-4 bg-stone-50 border-2 border-stone-50 rounded-md shadow-md shadow-slate-400"
+>
+  <div class="flex flex-row justify-center items-center">
+    <div class="w-80 h-80 overflow-hidden rounded-full">
+      <img class="w-full h-full object-cover object-center" src={imagen} />
+    </div>
+  </div>
+  <div class="w-full">
+    <h2 class="text-xl text-gray-400">
+      {puesto}
+    </h2>
+  </div>
+  <hr class="my-2" />
+  <div class="flex flex-col">
+    <p class="text-start text-lg font-semibold">{nombre}</p>
+  </div>
+</a>

+ 97 - 0
src/components/Collapse.astro

@@ -0,0 +1,97 @@
+---
+interface Props {
+  title: string;
+}
+
+const { title } = Astro.props as Props;
+---
+
+<div class="accordion group relative rounded-md border border-neutral-200">
+  <button
+    class="accordion__button flex w-full flex-1 items-center justify-between gap-2 p-3 text-left font-medium transition sm:px-4 text-xl"
+    type="button"
+    id={`${title} accordion menu button`}
+    aria-expanded="false"
+    aria-controls={`${title} accordion menu content`}
+  >
+    {title}
+    <svg
+      class="accordion__chevron h-7 w-7 shrink-0 transition-transform"
+      aria-hidden="true"
+      width="24"
+      height="24"
+      viewBox="0 0 24 24"
+      ><path
+        fill="none"
+        stroke="currentColor"
+        stroke-linecap="round"
+        stroke-linejoin="round"
+        stroke-width="2"
+        d="m6 9l6 6l6-6"></path></svg
+    >
+  </button>
+
+  <div
+    id={`${title} accordion menu content`}
+    aria-labelledby={`${title} accordion menu button`}
+    class="accordion__content hidden max-h-0 overflow-hidden transition-all duration-300 ease-in-out sm:px-4"
+  >
+    <slot />
+  </div>
+</div>
+<script>
+  function accordionSetup() {
+    const accordionMenus = document.querySelectorAll(
+      ".accordion"
+    ) as NodeListOf<HTMLElement>;
+    accordionMenus.forEach((accordionMenu) => {
+      const accordionButton = accordionMenu.querySelector(
+        ".accordion__button"
+      ) as HTMLElement;
+      const accordionChevron = accordionMenu.querySelector(
+        ".accordion__chevron"
+      ) as HTMLElement;
+      const accordionContent = accordionMenu.querySelector(
+        ".accordion__content"
+      ) as HTMLElement;
+
+      if (accordionButton && accordionContent && accordionChevron) {
+        accordionButton.addEventListener("click", (event) => {
+          if (!accordionMenu.classList.contains("active")) {
+            // if accordion is currently closed, so open it
+            accordionMenu.classList.add("active");
+            accordionButton.setAttribute("aria-expanded", "true");
+
+            // set max-height to the height of the accordion content
+            // this makes it animate properly
+            accordionContent.classList.remove("hidden");
+            accordionContent.style.maxHeight =
+              accordionContent.scrollHeight + "px";
+            accordionChevron.classList.add("rotate-180");
+          } else {
+            // accordion is currently open, so close it
+            accordionMenu.classList.remove("active");
+            accordionButton.setAttribute("aria-expanded", "false");
+
+            // set max-height to the height of the accordion content
+            // this makes it animate properly
+            accordionContent.style.maxHeight = "0px";
+            accordionChevron.classList.remove("rotate-180");
+            // delay to allow close animation
+            setTimeout(() => {
+              accordionContent.classList.add("hidden");
+            }, 300);
+          }
+          event.preventDefault();
+          return false;
+        });
+      }
+    });
+  }
+
+  // runs on initial page load
+  accordionSetup();
+
+  // runs on view transitions navigation
+  document.addEventListener("astro:after-swap", accordionSetup);
+</script>

+ 66 - 0
src/components/Dropdown.astro

@@ -0,0 +1,66 @@
+---
+interface Props {
+  id?: string;
+  nombre: string;
+  color?: string;
+  opciones: Option[];
+}
+
+interface Option {
+  titulo: string;
+  link: string;
+}
+
+const { id, nombre, color, opciones } = Astro.props;
+---
+
+<div class="relative inline-block text-left" id={`menu-button-${id}`} >
+  <div>
+    <button
+      type="button"
+      class=`px-3 py-2 text-lg ${color} font-medium border-b-2 border-b-[#444643] hover:border-b-stone-700 hover:text-gray-300 flex flex-row items-center`
+     
+      aria-expanded="false"
+      aria-haspopup="true"
+    >
+      {nombre}
+      <svg
+        class="-mr-1 size-5 text-gray-400"
+        viewBox="0 0 20 20"
+        fill="currentColor"
+        aria-hidden="true"
+        data-slot="icon"
+      >
+        <path
+          fill-rule="evenodd"
+          d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z"
+          clip-rule="evenodd"></path>
+      </svg>
+    </button>
+  </div>
+
+  <div
+    id={`menu-${id}`}
+    class="absolute right-0 z-10 transition ease-out duration-100 mt-2 w-56 origin-top-right rounded-md shadow-lg ring-1 ring-black/5 focus:outline-none scale-95 opacity-0 hidden bg-white"
+    role="menu"
+    aria-orientation="vertical"
+    aria-labelledby="menu-button"
+    tabindex="-1"
+  >
+    <div class="py-1" role="none">
+      {
+        opciones.map((opcion: Option, index: number) => (
+          <a
+            href={opcion.link}
+            class="block w-full px-4 py-2 text-md text-start text-gray-700 font-semibold hover:bg-gray-100"
+            role="menuitem"
+            tabindex="-1"
+            id={`menu-item-${id}-${index}`}
+          >
+            {opcion.titulo}
+          </a>
+        ))
+      }
+    </div>
+  </div>
+</div>

+ 3 - 0
src/components/HeroBanner.astro

@@ -0,0 +1,3 @@
+<div>
+
+</div>

+ 47 - 0
src/components/LinedCard.astro

@@ -0,0 +1,47 @@
+---
+interface Props {
+  title: string;
+  subtitle?: string;
+  titleOrientation: keyof typeof orientation;
+  imagen?: string;
+}
+
+const orientation = {
+  left: "start",
+  middle: "center",
+  right: "end",
+};
+
+const { title, subtitle, titleOrientation, imagen } = Astro.props;
+---
+
+<div class="w-full flex flex-col justify-center items-center my-10">
+  <p
+    class={`w-3/4 py-2 text-3xl font-bold text-${orientation[titleOrientation]}`}
+  >
+    {title}
+  </p>
+  <div class="inline-flex items-center justify-center w-full">
+    <hr class="w-full h-px my-4 bg-gray-200 border-0" />
+    <span class="absolute px-3 font-medium text-gray-900 bg-stone-100"
+      >{subtitle}</span
+    >
+  </div>
+  <div class="w-3/4 items-center">
+    <slot />
+  </div>
+  {
+    imagen && (
+      <div class="w-full flex flex-row justify-center my-5 gap-5">
+        <img src={imagen} class="w-1/4 rounded-md shadow-md shadow-gray-300" />
+        <div class="h-62 w-2 bg-amber-800" />
+        <div class="flex flex-col gap-4">
+          <p class="font-bold text-2xl text-yellow-950">Ganado Sonorense</p>
+          <p class="font-semibold text-xl">
+            La base de la calidad sonorense es su ganado.
+          </p>
+        </div>
+      </div>
+    )
+  }
+</div>

+ 258 - 0
src/components/Navbar.astro

@@ -0,0 +1,258 @@
+---
+import Dropdown from "./Dropdown.astro";
+
+interface Props {
+  floating?: boolean;
+}
+
+const { floating } = Astro.props;
+let floatConfig = "";
+let textColor = "text-white";
+
+if (floating) {
+  floatConfig = "z-20 absolute";
+  textColor = "text-white";
+}
+
+const opcLegislatura = [
+  {
+    titulo: "LXIV Legislatura",
+    link: "#",
+  },
+  {
+    titulo: "Historia del Congreso",
+    link: "#",
+  },
+  {
+    titulo: "Comisiones",
+    link: "#",
+  },
+  {
+    titulo: "Mesa Directiva",
+    link: "#",
+  },
+  {
+    titulo: "Grupos y Representaciones Parlamentarias",
+    link: "#",
+  },
+  {
+    titulo: "Iniciativas de Ley",
+    link: "#",
+  },
+  {
+    titulo: "Diputado Infantil",
+    link: "#",
+  },
+];
+
+const opcOrganizacion = [
+  {
+    titulo: "Organigrama",
+    link: "#",
+  },
+  {
+    titulo: "Programa de Gestión Institucional 2022 - 2025",
+    link: "#",
+  },
+  {
+    titulo: "Directorio",
+    link: "#",
+  },
+  {
+    titulo: "Buzón de Quejas",
+    link: "#",
+  },
+  {
+    titulo: "Noticias",
+    link: "#",
+  },
+  {
+    titulo: "Sitio de Interés",
+    link: "#",
+  },
+  {
+    titulo: "Código de ética",
+    link: "#",
+  },
+  {
+    titulo: "Código de conducta",
+    link: "#",
+  },
+];
+
+const opcTransparencia = [
+  {
+    titulo: "Acceso a la Información",
+    link: "#"
+  },
+  {
+    titulo: "Solicitud de Información",
+    link: "#"
+  },
+  {
+    titulo: "Informes varios",
+    link: "#"
+  },
+  {
+    titulo: "Ley de Austeridad y Ahorro del Estado de Sonora y sus Municipios",
+    link: "#"
+  },
+  {
+    titulo: "Ciclo presupuestario",
+    link: "#"
+  },
+];
+
+const opcServicios = [
+  {
+    titulo: "Transmisión en Vivo",
+    link: "#"
+  },
+  {
+    titulo: "Gaceta Parlamentaria",
+    link: "#"
+  },
+  {
+    titulo: "Atención Ciudadana",
+    link: "#"
+  },
+  {
+    titulo: "Biblioteca",
+    link: "#"
+  },
+  {
+    titulo: "Coordinación de Archivos",
+    link: "#"
+  },
+];
+---
+
+<nav class=`w-full ${floatConfig}`>
+  <div class="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8">
+    <div class="relative flex h-16 items-center justify-between">
+      <div class="absolute inset-y-0 left-0 flex items-center justify-between sm:hidden">
+        <!-- Mobile menu button-->
+        <button
+          type="button"
+          class="relative inline-flex items-center justify-center rounded-md p-2 text-black hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
+          aria-controls="mobile-menu"
+          aria-expanded="false"
+        >
+          <span class="absolute -inset-0.5"></span>
+          <span class="sr-only">Open main menu</span>
+          <svg
+            class="block h-6 w-6"
+            fill="none"
+            viewBox="0 0 24 24"
+            stroke-width="1.5"
+            stroke="currentColor"
+            aria-hidden="true"
+            data-slot="icon"
+          >
+            <path
+              stroke-linecap="round"
+              stroke-linejoin="round"
+              d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"></path>
+          </svg>
+          <svg
+            class="hidden h-6 w-6"
+            fill="none"
+            viewBox="0 0 24 24"
+            stroke-width="1.5"
+            stroke="currentColor"
+            aria-hidden="true"
+            data-slot="icon"
+          >
+            <path
+              stroke-linecap="round"
+              stroke-linejoin="round"
+              d="M6 18 18 6M6 6l12 12"></path>
+          </svg>
+        </button>
+      </div>
+      <div>
+        <div class="hidden sm:ml-6 sm:block">
+          <div class="flex space-x-4 m-5">
+            <a
+              href="/"
+              class=`px-3 py-2 text-lg font-medium ${textColor} hover:text-gray-300 hover:border-b-2 hover:border-b-stone-700`
+              >Inicio</a
+            >
+            <Dropdown
+              id="1"
+              nombre="Legislatura"
+              color={textColor}
+              opciones={opcLegislatura}
+            />
+            <Dropdown
+              id="2"
+              nombre="Organización"
+              color={textColor}
+              opciones={opcOrganizacion}
+            />
+            <Dropdown
+              id="3"
+              nombre="Transparencia"
+              color={textColor}
+              opciones={opcTransparencia}
+            />
+            <a
+              href="#"
+              class=`px-3 py-2 text-lg font-medium ${textColor} hover:text-gray-300 hover:border-b-2 hover:border-b-stone-700`
+              >Leyes</a
+            >
+            <Dropdown
+              id="4"
+              nombre="Servicios"
+              color={textColor}
+              opciones={opcServicios}
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</nav>
+
+<script lang="ts">
+  const toggleMenu = (menuId) => {
+    const menu = document.getElementById(`menu-${menuId}`);
+
+    const isOpen = menu.classList.contains("scale-100");
+    menu.classList.toggle("scale-100", !isOpen);
+    menu.classList.toggle("opacity-100", !isOpen);
+    menu.classList.toggle("scale-95", isOpen);
+    menu.classList.toggle("opacity-0", isOpen);
+    menu.classList.toggle("hidden", isOpen);
+  };
+
+  const closeAllMenus = (id, buttons) => {
+    buttons.forEach((btn) => {
+      const menuId = btn.getAttribute("id")?.replace("menu-button-", "");
+      if (id === menuId) {
+        return true;
+      }
+      const menu = document.getElementById(`menu-${menuId}`);
+
+      const isOpen = menu.classList.contains("scale-100");
+      if (!isOpen) {
+        return true;
+      }
+
+      toggleMenu(menuId);
+    });
+  };
+
+  document.addEventListener("DOMContentLoaded", () => {
+    const buttons = document.querySelectorAll("[id^='menu-button-']");
+
+    buttons.forEach((button) => {      
+      const menuId = button.getAttribute("id")?.replace("menu-button-", "");
+
+      button.addEventListener("click", () => {
+        closeAllMenus(menuId, buttons);
+        toggleMenu(menuId);
+      });
+    });
+  });
+</script>

+ 30 - 0
src/components/SearchBar.astro

@@ -0,0 +1,30 @@
+---
+
+---
+
+<div
+  class="w-full h-12 border-[1px] border-neutral-300 rounded-md grid grid-cols-12 overflow-hidden"
+>
+  <input
+    class="col-span-11 p-2 focus:outline-neutral-400 focus:rounded-md"
+    placeholder="Buscar"
+  />
+  <button
+    class="col-span-1 h-full bg-stone-800 hover:bg-stone-700 flex flex-row justify-center items-center"
+    ><svg
+      class="w-6 h-6 text-white"
+      aria-hidden="true"
+      xmlns="http://www.w3.org/2000/svg"
+      width="24"
+      height="24"
+      fill="none"
+      viewBox="0 0 24 24"
+    >
+      <path
+        stroke="currentColor"
+        stroke-linecap="round"
+        stroke-width="2"
+        d="m21 21-3.5-3.5M17 10a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z"></path>
+    </svg>
+  </button>
+</div>

+ 85 - 0
src/components/Slider.tsx

@@ -0,0 +1,85 @@
+import { useState, useEffect, useCallback } from "react";
+import type { Slide } from "../models/Slide.model";
+
+interface SliderProps {
+  slides: Slide[];
+  initialSlide?: number;
+  height?: string;
+  width?: string;
+  delay?: number;
+}
+
+const Slider: React.FC<SliderProps> = ({
+  slides,
+  initialSlide = 0,
+  height = "",
+  width = "",
+  delay = 30000,
+}) => {
+  const [currentSlide, setCurrentSlide] = useState(initialSlide);
+
+
+  useEffect(() => {
+    const interval = setInterval(() => {
+      setCurrentSlide((prev) => (prev + 1) % slides.length);
+    }, delay);
+    return () => clearInterval(interval);
+  }, [delay, slides.length]);
+
+  return (
+    <div
+      className={`relative w-${width} h-auto md:h-${height} overflow-hidden`}
+    >
+      <div
+        className="flex transition-transform duration-700 ease-in-out"
+        style={{ transform: `translateX(-${currentSlide * 100}%)` }}
+      >
+        {slides.map((slide) => (
+          <div key={slide.id} className="w-full flex-shrink-0">
+            <div
+              className="absolute inset-0 bg-gradient-to-t from-stone-950 to-transparent opacity-10"
+            >
+            </div>
+            <img
+              src={slide.image}
+              alt={slide.title}
+              className="h-full object-cover object-center"
+            />
+          </div>
+        ))}
+      </div>
+      <div className="absolute inset-0 flex justify-between items-center px-4">
+        <button
+          className="p-2 bg-stone-800 bg-opacity-80 hover:bg-opacity-100 ease-in text-white rounded-full w-10"
+          onClick={() =>
+            setCurrentSlide(
+              (prev) => (prev - 1 + slides.length) % slides.length
+            )
+          }
+        >
+          &#10094;
+        </button>
+        <button
+          className="p-2 bg-stone-800 bg-opacity-80 hover:bg-opacity-100 ease-in text-white rounded-full w-10"
+          onClick={() => setCurrentSlide((prev) => (prev + 1) % slides.length)}
+        >
+          &#10095;
+        </button>
+      </div>
+      <div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex space-x-2">
+        {slides.map((_, index) => (
+          <button
+            key={index}
+            className={`w-3 h-3 hover:opacity-100 rounded-full ${index === currentSlide
+              ? "bg-stone-300 opacity-60"
+              : "bg-stone-600 opacity-70"
+              }`}
+            onClick={() => setCurrentSlide(index)}
+          />
+        ))}
+      </div>
+    </div>
+  );
+};
+
+export default Slider;

+ 89 - 0
src/constants/file-types.ts

@@ -0,0 +1,89 @@
+export enum FileType {
+  /**Image */
+  PNG = 'image/png',
+  JPG = 'image/jpg',
+  JPEG = 'image/jpeg',
+  GIF = 'image/gif',
+  BMP = 'image/bmp',
+  WEBP = 'image/webp',
+  SVG = 'image/svg+xml',
+
+  /**Video and Audio */
+  MP4 = 'video/mp4',
+  MP3 = 'audio/mp3',
+  AVI = 'video/x-msvideo',
+  MOV = 'video/quicktime',
+  MKV = 'video/x-matroska',
+  WAV = 'audio/wav',
+  OGG = 'audio/ogg',
+  WEBM = 'video/webm',
+
+  /**Text */
+  CSV = 'text/csv',
+  TEXT = 'text/plain',
+  HTML = 'text/html',
+  MARKDOWN = 'text/markdown',
+  XML = 'text/xml',
+  JSON = 'application/json',
+
+  /**Application*/
+  PDF = 'application/pdf',
+  WORD = 'application/msword',
+  WORDX = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+  EXCEL = 'application/vnd.ms-excel',
+  EXCELX = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+  POWERPOINT = 'application/vnd.ms-powerpoint',
+  POWERPOINTX = 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+
+  ZIP = 'application/zip',
+  RAR = 'application/x-rar-compressed',
+  TAR = 'application/x-tar',
+  GZIP = 'application/gzip',
+}
+
+export const image_file_types = [
+  FileType.PNG,
+  FileType.JPG,
+  FileType.JPEG,
+  FileType.GIF,
+  FileType.BMP,
+  FileType.WEBP,
+  FileType.SVG
+]
+
+export const video_file_types = [
+  FileType.MP4,
+  FileType.MP3,
+  FileType.AVI,
+  FileType.MOV,
+  FileType.MKV,
+  FileType.WAV,
+  FileType.OGG,
+  FileType.WEBM
+]
+
+export const text_file_types = [
+  FileType.CSV,
+  FileType.TEXT,
+  FileType.HTML,
+  FileType.MARKDOWN,
+  FileType.XML,
+  FileType.JSON
+]
+
+export const application_file_types = [
+  FileType.PDF,
+  FileType.WORD,
+  FileType.WORDX,
+  FileType.EXCEL,
+  FileType.EXCELX,
+  FileType.POWERPOINT,
+  FileType.POWERPOINTX
+]
+
+export const zip_file_types = [
+  FileType.ZIP,
+  FileType.RAR,
+  FileType.TAR,
+  FileType.GZIP
+]

+ 59 - 0
src/constants/http-status-codes.ts

@@ -0,0 +1,59 @@
+const HTTP_STATUS_CODES = {
+  ACCEPTED: 202,
+  BAD_GATEWAY: 502,
+  BAD_REQUEST: 400,
+  CONFLICT: 409,
+  CONTINUE: 100,
+  CREATED: 201,
+  EXPECTATION_FAILED: 417,
+  FAILED_DEPENDENCY: 424,
+  FORBIDDEN: 403,
+  GATEWAY_TIMEOUT: 504,
+  GONE: 410,
+  HTTP_VERSION_NOT_SUPPORTED: 505,
+  IM_A_TEAPOT: 418,
+  INSUFFICIENT_SPACE_ON_RESOURCE: 419,
+  INSUFFICIENT_STORAGE: 507,
+  INTERNAL_SERVER_ERROR: 500,
+  LENGTH_REQUIRED: 411,
+  LOCKED: 423,
+  METHOD_FAILURE: 420,
+  METHOD_NOT_ALLOWED: 405,
+  MOVED_PERMANENTLY: 301,
+  MOVED_TEMPORARILY: 302,
+  MULTI_STATUS: 207,
+  MULTIPLE_CHOICES: 300,
+  NETWORK_AUTHENTICATION_REQUIRED: 511,
+  NO_CONTENT: 204,
+  NON_AUTHORITATIVE_INFORMATION: 203,
+  NOT_ACCEPTABLE: 406,
+  NOT_FOUND: 404,
+  NOT_IMPLEMENTED: 501,
+  NOT_MODIFIED: 304,
+  OK: 200,
+  PARTIAL_CONTENT: 206,
+  PAYMENT_REQUIRED: 402,
+  PERMANENT_REDIRECT: 308,
+  PRECONDITION_FAILED: 412,
+  PRECONDITION_REQUIRED: 428,
+  PROCESSING: 102,
+  PROXY_AUTHENTICATION_REQUIRED: 407,
+  REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
+  REQUEST_TIMEOUT: 408,
+  REQUEST_TOO_LONG: 413,
+  REQUEST_URI_TOO_LONG: 414,
+  REQUESTED_RANGE_NOT_SATISFIABLE: 416,
+  RESET_CONTENT: 205,
+  SEE_OTHER: 303,
+  SERVICE_UNAVAILABLE: 503,
+  SWITCHING_PROTOCOLS: 101,
+  TEMPORARY_REDIRECT: 307,
+  TOO_MANY_REQUESTS: 429,
+  UNAUTHORIZED: 401,
+  UNAVAILABLE_FOR_LEGAL_REASONS: 451,
+  UNPROCESSABLE_ENTITY: 422,
+  UNSUPPORTED_MEDIA_TYPE: 415,
+  USE_PROXY: 305,
+};
+
+export default Object.freeze(HTTP_STATUS_CODES);

+ 18 - 0
src/constants/index.ts

@@ -0,0 +1,18 @@
+import imagenNoDisponible from '@/assets/imagen-no-disponible.png'
+
+export const IS_DEV = import.meta.env.VITE_IS_DEV === 'true';
+export const SHOW_DEVTOOLS = import.meta.env.VITE_SHOW_DEVTOOLS === 'true';
+export const VERSION = import.meta.env.VITE_VERSION;
+export const PROJECT_NAME = import.meta.env.VITE_PROJECT_NAME;
+
+export const IMAGEN_NO_DISPONIBLE = imagenNoDisponible;
+
+export const REGEX = {
+  NUMEROS_ENTEROS: /^[0-9\b]+$/,
+  NUMEROS_DECIMALES: /^\d*\.?\d*$/,
+  CURRENCY_NUMBER_FORMAT: /^\$?\d{1,3}(?:,\d{3})*(?:\.\d{2})?$/,
+  DIVISA: /^[0-9.,]+$/,
+  CORREO: /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
+  TELEFONO: /^[0-9]{10,10}$/,
+  BASE_64_IMG: /^data:image\/(png|jpeg|jpg|gif);base64,[A-Za-z0-9+/]+={0,2}$/
+};

+ 1 - 0
src/env.d.ts

@@ -0,0 +1 @@
+/// <reference path="../.astro/types.d.ts" />

+ 134 - 0
src/hooks/useHttp.jsx

@@ -0,0 +1,134 @@
+import React from "react";
+import { useAlert } from "./useAlert";
+import httpCodes from "../constants/httpStatusCodes";
+
+const baseUrl = import.meta.env.VITE_API_URL;
+const localStorageKey = "usr_jwt";
+
+const defaultHeaders = {
+  "Content-Type": "application/json",
+  Accept: "application/json",
+};
+
+const makeHeaders = (token) =>
+  token
+    ? {
+        ...defaultHeaders,
+        Authorization: `Bearer ${token}`,
+      }
+    : defaultHeaders;
+
+const paramsToQuery = (params) =>
+  Object.keys(params)
+    .map(
+      (key) => encodeURIComponent(key) + "=" + encodeURIComponent(params[key])
+    )
+    .join("&");
+
+const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
+
+export function useHttp({
+  req = "GET",
+  url = null,
+  params = null,
+  body = null,
+  alert = false,
+}) {
+  const { showAlert } = useAlert();
+  const [response, setResponse] = React.useState(null);
+  const [error, setError] = React.useState(null);
+  const [loading, setLoading] = React.useState(true);
+
+  const refresh = React.useCallback(
+    async (showAlert, inlineParams = {}) => {
+      try {
+        if (!url || !params) {
+          setResponse(null);
+          setError(null);
+          setLoading(true);
+          return;
+        }
+        if (inlineParams.isCargando === false) {
+          setLoading(() => false);
+        } else {
+          setLoading(() => true);
+        }
+        const jwt =  localStorage.getItem(localStorageKey);
+        let fetchReq = {
+          method: req,
+          headers: makeHeaders(jwt),
+        };
+        if (body) {
+          fetchReq = { ...fetchReq, body: JSON.stringify(body) };
+        }
+        const paramsFinal = { ...params, ...inlineParams };
+        const str = `${baseUrl}${url}${
+          params && Object.keys(paramsFinal).length > 0
+            ? `?${paramsToQuery(paramsFinal)}`
+            : ""
+        }`;
+        const httpRes = await fetch(str, fetchReq);
+        const resBody = await httpRes.json();
+        switch (httpRes.status) {
+          case httpCodes.OK:
+            setResponse(resBody);
+            setError(null);
+            alert &&
+              showAlert({
+                severity: "success",
+                message: resBody.mensaje
+                  ? capitalize(resBody.mensaje)
+                  : "Solicitud completada correctamente!",
+              });
+            break;
+          case httpCodes.BAD_REQUEST:
+          case httpCodes.UNAUTHORIZED:
+            window["scrollTo"]({ top: 0, behavior: "smooth" });
+            setError(resBody.errores);
+            alert &&
+              showAlert({
+                severity: "warning",
+                message: resBody.mensaje
+                  ? capitalize(resBody.mensaje)
+                  : "Datos erróneos o inválidos.",
+              });
+            break;
+          case httpCodes.INTERNAL_SERVER_ERROR:
+          default:
+            alert &&
+              showAlert({
+                severity: "error",
+                message: resBody.mensaje
+                  ? capitalize(resBody.mensaje)
+                  : "Ocurrió un error en el servidor.",
+              });
+        }
+      } catch (error) {
+        alert &&
+          showAlert({
+            severity: "error",
+            message: "No se pudo establecer conexión con el servidor.",
+          });
+        console.error(error);
+      } finally {
+        setLoading(false);
+      }
+    },
+    [body, params, req, url, alert]
+  );
+
+  React.useEffect(() => {
+    let mounted = true;
+    if (mounted) {
+      refresh(showAlert);
+    }
+    return () => {
+      mounted = false;
+    };
+  }, [refresh, showAlert]);
+
+  return React.useMemo(
+    () => [response, loading, error, refresh],
+    [response, loading, error, refresh]
+  );
+}

+ 113 - 0
src/layouts/Layout.astro

@@ -0,0 +1,113 @@
+---
+import "../styles/global.css";
+import Navbar from "../components/Navbar.astro";
+
+interface Props {
+  title: string;
+  ogTitle?: string;
+  ogImage?: string;
+  ogUrl?: string;
+  ogDescription?: string;
+}
+
+const { title, ogTitle, ogImage, ogUrl, ogDescription } = Astro.props;
+---
+
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="description" content="Astro description" />
+    <!-- <meta name="viewport" content="width=device-width" /> -->
+    <meta property="og:title" content={ogTitle} />
+    <meta property="og:image" content={ogImage} />
+    <meta property="og:url" content={ogUrl} />
+    <meta property="og:description" content={ogDescription} />
+    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.1/css/all.min.css" integrity="sha512-5Hs3dF2AEPkpNAR7UiOHba+lRSJNeM2ECkwxUIxC1Q/FLycGTbNapWXB4tP889k5T5Ju8fs4b1P5z/iB4nMfSQ==" crossorigin="anonymous" referrerPolicy="no-referrer" />
+    <meta name="generator" content={Astro.generator} />
+    <title>{title}</title>
+  </head>
+  <body class="bg-white">
+    <header>
+      <div class="w-full flex justify-center bg-[#444643] text-white">
+        <div class="w-3/4 flex flex-col md:flex-row items-center p-3 gap-4 md:gap-8">
+          <div class="w-full md:w-1/2">Tehuantepec y Allende Col. Las Palmas Hermosillo, Sonora. C.P. 83270</div>
+          <div class="w-full md:w-1/2 flex flex-col md:flex-row justify-between gap-4 md:gap-8">
+            <div>Jueves, Noviembre 28, 2024</div>
+            <div>(662) 259-6700</div>
+            <div class="flex flex-col md:flex-row gap-2">
+              <div class="w-8 h-8 bg-white flex justify-center items-center">
+                <i class="fa-brands fa-facebook-f text-black"></i>
+              </div>
+              <div class="w-8 h-8 bg-white flex justify-center items-center">
+                <i class="fa-brands fa-x-twitter text-black"></i>
+              </div>
+              <div class="w-8 h-8 bg-white flex justify-center items-center">
+                <i class="fa-brands fa-instagram text-black"></i>
+              </div>
+              <div class="w-8 h-8 bg-white flex justify-center items-center">
+                <i class="fa-brands fa-youtube text-black"></i>
+              </div>
+              <div class="w-8 h-8 bg-white flex justify-center items-center">
+                <i class="fa-brands fa-flickr text-black"></i>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="w-full flex justify-center">
+        <div class="w-3/4 flex justify-between space-">
+          <img class="h-24 w-auto" src="/img/logo.png">
+          <img class="h-24 w-auto" src="/img/legislatura_64.png">
+        </div>
+      </div>
+      <div class="w-full flex justify-center bg-[#444643] text-white">
+        <div class="w-3/4 flex justify-between items-center">
+          <div>
+            <Navbar />
+          </div>
+          <div>
+            <button class="bg-white text-black p-2 ">Aviso de Privacidad</button>
+          </div>
+        </div>
+      </div>
+    </header>
+    <main>
+      <slot />
+    </main>
+    <footer
+      class="h-full w-full flex flex-col md:justify-center md:items-center bg-stone-800"
+    >
+
+    </footer>
+  </body>
+
+  <style is:global>
+    :root {
+      --accent: 136, 58, 234;
+      --accent-light: 224, 204, 250;
+      --accent-dark: 49, 10, 101;
+      --accent-gradient: linear-gradient(
+        45deg,
+        rgb(var(--accent)),
+        rgb(var(--accent-light)) 30%,
+        white 60%
+      );
+    }
+    html {
+      font-family: system-ui, sans-serif;
+    }
+    code {
+      font-family:
+        Menlo,
+        Monaco,
+        Lucida Console,
+        Liberation Mono,
+        DejaVu Sans Mono,
+        Bitstream Vera Sans Mono,
+        Courier New,
+        monospace;
+    }
+  </style>
+</html>

+ 24 - 0
src/models/Home.model.ts

@@ -0,0 +1,24 @@
+export class Home {
+  id: string;
+  foto: string;
+  titulo: string;
+  descripcion: string;
+  contenido: string;
+  fecha: string;
+
+  constructor(json?: Partial<Home>) {
+    this.id = "";
+    this.foto = "";
+    this.titulo = "";
+    this.descripcion = "";
+    this.contenido = "";
+    this.fecha = "";
+    if (json !== null) {
+      Object.assign(this, json);
+    }
+  }
+
+  static fromJson(json: Partial<Home>) {
+    return new Home(json);
+  }
+}

+ 30 - 0
src/models/Noticia.model.ts

@@ -0,0 +1,30 @@
+export class Noticia {
+  id: string;
+  titulo: string;
+  introduccion: string;
+  descripcion: string;
+  fecha: string;
+  foto: string;
+  categoria: string;
+
+  constructor(json?: Partial<Noticia>) {
+    this.id = "";
+    this.titulo = "";
+    this.introduccion = "";
+    this.descripcion = "";
+    this.fecha = "";
+    this.foto = "";
+    this.categoria = "";
+    if (json !== null) {
+      Object.assign(this, json);
+    }
+  }
+
+  static fromJson(json: Partial<Noticia>) {
+    return new Noticia(json);
+  }
+
+  static fromJsonList(data: Partial<Noticia>[]) {
+    return data.map((_data) => new Noticia(_data));
+  }
+}

+ 24 - 0
src/models/Publicacion.model.ts

@@ -0,0 +1,24 @@
+export class Publicacion {
+  id: string;
+  foto: string;
+  titulo: string;
+  descripcion: string;
+  contenido: string;
+  fecha: string;
+
+  constructor(json?: Partial<Publicacion>) {
+    this.id = "";
+    this.foto = "";
+    this.titulo = "";
+    this.descripcion = "";
+    this.contenido = "";
+    this.fecha = "";
+    if (json !== null) {
+      Object.assign(this, json);
+    }
+  }
+
+  static fromJson(json: Partial<Publicacion>) {
+    return new Publicacion(json);
+  }
+}

+ 28 - 0
src/models/QuienesSomos.model.ts

@@ -0,0 +1,28 @@
+export class QuienesSomos {
+  id: string;
+  foto?: string;
+  nombre: string;
+  descripcion?: string;
+  imagen?: string;
+  fecha?: string;
+
+  constructor(json?: Partial<QuienesSomos>) {
+    this.id = "";
+    this.foto = "";
+    this.nombre = "";
+    this.descripcion = "";
+    this.imagen = "";
+    this.fecha = "";
+    if (json !== null) {
+      Object.assign(this, json);
+    }
+  }
+
+  static fromJson(json: Partial<QuienesSomos>) {
+    return new QuienesSomos(json);
+  }
+
+  static fromJsonList(data: Partial<QuienesSomos>[]) {
+    return data.map((_data) => new QuienesSomos(_data));
+  }
+}

+ 18 - 0
src/models/Slide.model.ts

@@ -0,0 +1,18 @@
+export class Slide {
+  id: number;
+  image: string;
+  title: string;
+
+  constructor(json?: Partial<Slide>) {
+    this.id = 0;
+    this.image = "";
+    this.title = "";
+    if (json !== null) {
+      Object.assign(this, json);
+    }
+  }
+
+  static fromJson(json: Partial<Slide>) {
+    return new Slide(json);
+  }
+}

+ 10 - 0
src/pages/index.astro

@@ -0,0 +1,10 @@
+---
+import Layout from "../layouts/Layout.astro";
+import "../styles/global.css";
+---
+
+<Layout title="Congreo del Estado de Sonora" ogTitle="Congreo del Estado de Sonora">
+  <div class="w-full flex flex-col items-center bg-white bg-opacity-95">
+
+  </div>
+</Layout>

+ 74 - 0
src/pages/licitacion/index.astro

@@ -0,0 +1,74 @@
+---
+import Navbar from "../../components/Navbar.astro";
+import Layout from "../../layouts/Layout.astro";
+---
+
+<Layout title="Contactanos">
+  <div class="w-full h-full flex flex-row justify-center pb-10 pl-10 pr-10 bg-stone-100">
+    <div class="w-4/5 h-full grid grid-cols-12 p-10 bg-white gap-4">
+      <div class="col-span-full">
+        <h1 class="font-bold text-4xl">Formulario de Registro</h1>
+      </div>
+      <hr class="my-2 col-span-12 border-amber-950" />
+      <form class="w-full col-span-12 space-y-4">
+        <div class="w-full space-y-2">
+          <p class="w-full text-xl font-bold">Nombre del Solicitante</p>
+          <input
+            name="nombreSolicitante"
+            class="w-full p-2 border-2 border-amber-950 border-opacity-40 rounded-md"
+          />
+        </div>
+        <div class="w-full space-y-2">
+          <p class="w-full text-xl font-bold">Nombre fiscal</p>
+          <input
+            name="nombreFiscal"
+            class="w-full p-2 border-2 border-amber-950 border-opacity-40 rounded-md"
+          />
+        </div>
+        <div class="w-full space-y-2">
+          <p class="w-full text-xl font-bold">Tipo de Persona</p>
+          <select
+            class="w-full p-2 border-2 border-amber-950 border-opacity-40 rounded-md"
+          >
+          <option>Persona Física</option>
+          <option>Persona Moral</option>
+          </select>
+        </div>
+        <div class="w-full space-y-2">
+          <p class="w-full text-xl font-bold">RFC</p>
+          <input
+            name="rfc"
+            class="w-full p-2 border-2 border-amber-950 border-opacity-40 rounded-md"
+          />
+        </div>
+        <div class="w-full space-y-2">
+          <p class="w-full text-xl font-bold">Teléfono de Contacto(10 dígitos)</p>
+          <input
+            name="telefono"
+            class="w-full p-2 border-2 border-amber-950 border-opacity-40 rounded-md"
+          />
+        </div>
+        <div class="w-full space-y-2">
+          <p class="w-full text-xl font-bold">Correo</p>
+          <input
+            name="correo"
+            class="w-full p-2 border-2 border-amber-950 border-opacity-40 rounded-md"
+          />
+        </div>
+        <div class="w-full space-y-2">
+          <p class="w-full text-xl font-bold">Convocatoria</p>
+          <input
+            name="convocatoria"
+            class="w-full p-2 border-2 border-amber-950 border-opacity-40 rounded-md"
+          />
+        </div>
+        <div class="w-full space-y-2 flex flex-row justify-end">
+          <button
+            class="px-6 py-2 text-2xl bg-amber-950 hover:shadow-sm hover:shadow-amber-900 rounded-md text-white"
+            >Enviar</button
+          >
+        </div>
+      </form>
+    </div>
+  </div>
+</Layout>

+ 242 - 0
src/services/httpService.ts

@@ -0,0 +1,242 @@
+import type { TPaginacion, DefaultResponse } from "../types/responses";
+
+export interface IRequestParams {
+  expand?: string;
+  ordenar?: string | "id-desc" | "id-asc";
+  limite?: number;
+  pagina?: number;
+  buscar?: string;
+
+  [key: string]: any;
+}
+
+export interface IRequest {
+  req: string;
+  endpoint: string;
+  params: any;
+  body: any;
+}
+
+const API_URL = "https://upreson.api.edesarrollos.info/";
+
+export interface IHttpService {
+  get: <T>(
+    endpoint: string,
+    params?: any,
+    updateQueryParams?: boolean
+  ) => Promise<DefaultResponse<T>>;
+  getBlob: (endpoint: string, data: any) => Promise<Blob>;
+  downloadBlob: (
+    endpoint: string,
+    data: any,
+    fileName: string
+  ) => Promise<void>;
+  post: <T>(endpoint: string, body: any) => Promise<DefaultResponse<T>>;
+  postFormData: (endpoint: string, data: any) => Promise<DefaultResponse<any>>;
+  delete: <T>(endpoint: string, body: T) => Promise<DefaultResponse<any>>;
+  put: (endpoint: string, body: any) => Promise<any>;
+}
+
+export class HttpService implements IHttpService {
+  API_URL: string;
+
+  constructor(API_URL: string) {
+    this.API_URL = API_URL;
+  }
+
+  static DEFAULT_HEADERS = () => {
+    return {
+      "Content-Type": "application/json",
+      // Authorization: `Bearer ${localStorage.getItem("token")}`,
+    };
+  };
+
+  static FILE_HEADERS = () => {
+    return {
+      "Content-Type": "multipart/form-data",
+      // Authorization: `Bearer ${localStorage.getItem("token")}`,
+    };
+  };
+
+  static DEFAULT_REQUEST_PARAMS = {
+    limite: 10,
+    pagina: 1,
+    ordenar: "id-desc",
+  };
+
+  static DEFAULT_PAGINACION: TPaginacion = {
+    total: 0,
+    pagina: 1,
+    limite: 10,
+  };
+
+  static paramsToQuery = (params: any) => {
+    return Object.keys(params)
+      .map(
+        (key) => encodeURIComponent(key) + "=" + encodeURIComponent(params[key])
+      )
+      .join("&");
+  };
+
+  static EMPTY_REQUEST = (): IRequest => ({
+    req: "",
+    endpoint: "",
+    params: null,
+    body: null,
+  });
+
+  static GET_REQUEST = (endpoint: string, params: any = {}): IRequest => ({
+    req: "GET",
+    endpoint,
+    params,
+    body: null,
+  });
+
+  static POST_REQUEST = <T = any>(
+    endpoint: string,
+    body: T,
+    params: any = {}
+  ): IRequest => ({
+    req: "POST",
+    endpoint,
+    params,
+    body,
+  });
+
+  static DELETE_REQUEST = (endpoint: string, params: any = {}) =>
+    ({
+      req: "DELETE",
+      endpoint: `${endpoint}/eliminar`,
+      body: {
+        ...params,
+      },
+    } as IRequest);
+
+  get = async <T>(
+    endpoint: string,
+    params: any = HttpService.DEFAULT_REQUEST_PARAMS,
+    updateQueryParams: boolean = false
+  ) => {
+    const stringParams = params ? HttpService.paramsToQuery(params) : "";
+    const queryParams = `?${new URLSearchParams(stringParams).toString()}`;
+
+    let url = `${this.API_URL}${endpoint}`;
+    if (queryParams) {
+      url = `${this.API_URL}${endpoint}${queryParams}`;
+
+      if (updateQueryParams && window.location.search !== queryParams) {
+        //Actualizar los queryparams de la url actual
+        window.history.pushState(
+          {},
+          "",
+          window.location.pathname + `${queryParams}`
+        );
+      }
+    }
+
+    const _response = await fetch(url, {
+      method: "GET",
+      headers: HttpService.DEFAULT_HEADERS(),
+    });
+
+    const response = (await _response.json()) as DefaultResponse<T>;
+
+    return {
+      ...response,
+      isError: response?.status !== 200 ? true : false,
+      status: response?.status,
+      resultado: response?.resultado || response,
+    } as DefaultResponse<T>;
+  };
+
+  getBlob = async (endpoint: string, data: any) => {
+    const _response = await fetch(`${this.API_URL}${endpoint}`, {
+      method: "GET",
+      headers: HttpService.DEFAULT_HEADERS(),
+      body: JSON.stringify(data),
+    });
+    const response = await _response.blob();
+    return response;
+  };
+
+  downloadBlob = async (
+    endpoint: string,
+    data: any,
+    fileName: string = "fileName"
+  ) => {
+    const _response = await fetch(`${this.API_URL}${endpoint}`, {
+      method: "GET",
+      headers: HttpService.DEFAULT_HEADERS(),
+      body: JSON.stringify(data),
+    });
+    const blob = await _response.blob();
+
+    const urlBlob = URL.createObjectURL(blob);
+    const link = document.createElement("a");
+    link.href = urlBlob;
+    link.setAttribute("download", fileName);
+    document.body.appendChild(link);
+    link.click();
+    URL.revokeObjectURL(urlBlob);
+    link.remove();
+  };
+
+  post = async <T>(endpoint: string, body: any) => {
+    const _response = await fetch(`${this.API_URL}${endpoint}`, {
+      method: "POST",
+      headers: HttpService.DEFAULT_HEADERS(),
+      body: JSON.stringify(body),
+    });
+    const status = _response.status;
+    const response = (await _response.json()) as DefaultResponse<T>;
+
+    return {
+      ...response,
+      isError: status !== 200 ? true : false,
+      status: status,
+    } as DefaultResponse<T>;
+  };
+
+  postFormData = async (endpoint: string, data: any) => {
+    const _response = await fetch(`${this.API_URL}${endpoint}`, {
+      method: "POST",
+      headers: HttpService.FILE_HEADERS(),
+      body: data,
+    });
+
+    const response = await _response.json;
+
+    return {
+      ...response,
+      isError: _response?.status !== 200 ? true : false,
+      status: _response?.status,
+    } as DefaultResponse<any>;
+  };
+
+  delete = async <T = any>(endpoint: string, body: T) => {
+    const response = await fetch(`${this.API_URL}${endpoint}`, {
+      body: JSON.stringify(body),
+      method: "DELETE",
+      headers: HttpService.DEFAULT_HEADERS(),
+    });
+
+    const status = response.status;
+    const responseJson = await response.json();
+    return {
+      ...responseJson,
+      isError: status !== 200 ? true : false,
+      status: status,
+    } as DefaultResponse<any>;
+  };
+
+  put = async (endpoint: string, body: any) => {
+    const response = await fetch(`${this.API_URL}${endpoint}`, {
+      method: "PUT",
+      headers: HttpService.DEFAULT_HEADERS(),
+      body: JSON.stringify(body),
+    });
+    return response.json();
+  };
+}
+
+export const http = new HttpService(API_URL);

+ 1 - 0
src/services/index.js

@@ -0,0 +1 @@
+export * from "./httpService";

+ 5 - 0
src/styles/global.css

@@ -0,0 +1,5 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+#map { height: 180px; }

+ 15 - 0
src/types/responses.d.ts

@@ -0,0 +1,15 @@
+export type TPaginacion = {
+  limite: number;
+  total: number;
+  pagina: number;
+};
+
+export type DefaultResponse<T = any> = {
+  isError: boolean;
+  status: number;
+  resultado: T | null;
+  detalle: T | null;
+  paginacion: TPaginacion | null;
+  mensaje: string | null;
+  [key: string]: any
+}

+ 8 - 0
tailwind.config.mjs

@@ -0,0 +1,8 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+	content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
+	theme: {
+		extend: {},
+	},
+	plugins: [],
+}

+ 7 - 0
tsconfig.json

@@ -0,0 +1,7 @@
+{
+  "extends": "astro/tsconfigs/strict",
+  "compilerOptions": {
+    "jsx": "react-jsx",
+    "jsxImportSource": "react"
+  }
+}