Compare commits

..

8 Commits

Author SHA1 Message Date
e9ce2d1f9f feat/final commit
All checks were successful
Build Docker Image Front / run (push) Successful in 1m56s
Build Docker Image Back / run (push) Successful in 25s
2024-06-09 20:05:28 +02:00
3cb5a30270 feat: use drink api in front and more (#16)
All checks were successful
Build Docker Image Front / run (push) Successful in 28s
Build Docker Image Back / run (push) Successful in 27s
Reviewed-on: #16
Co-authored-by: Clement <c.boesmier@aptatio.com>
Co-committed-by: Clement <c.boesmier@aptatio.com>
2024-06-07 19:09:57 +02:00
2cb2166093 fix: correction prod docker (#15)
All checks were successful
Build Docker Image Front / run (push) Successful in 26s
Build Docker Image Back / run (push) Successful in 24s
Reviewed-on: #15
Co-authored-by: Clement <c.boesmier@aptatio.com>
Co-committed-by: Clement <c.boesmier@aptatio.com>
2024-05-21 10:40:13 +02:00
1593fa3493 feat: astro template for front and fix login(#13)
All checks were successful
Build Docker Image Front / run (push) Successful in 25s
Build Docker Image Back / run (push) Successful in 25s
Reviewed-on: #13
Co-authored-by: Clement <c.boesmier@aptatio.com>
Co-committed-by: Clement <c.boesmier@aptatio.com>
2024-05-21 10:07:56 +02:00
57a57c63ff feat: gestion-utilisateur (#1)
All checks were successful
Build Docker Image Front / run (push) Successful in 24s
Build Docker Image Back / run (push) Successful in 21s
Reviewed-on: #1
Co-authored-by: Clement <c.boesmier@aptatio.com>
Co-committed-by: Clement <c.boesmier@aptatio.com>
2024-05-20 12:48:34 +02:00
0c85df498e fix: rm port var in docker file (#12)
All checks were successful
Build Docker Image Front / run (push) Successful in 26s
Build Docker Image Back / run (push) Successful in 22s
Build Docker Image Front / run (pull_request) Successful in 23s
Build Docker Image Back / run (pull_request) Successful in 23s
JsDocs / coverage (pull_request) Successful in 23s
Test and coverage / coverage (pull_request) Successful in 1m38s
Reviewed-on: #12
Co-authored-by: Clement <c.boesmier@aptatio.com>
Co-committed-by: Clement <c.boesmier@aptatio.com>
2024-05-19 16:26:52 +02:00
e0c3580269 feat: open-trip-api-drink (#11)
All checks were successful
Build Docker Image Front / run (push) Successful in 28s
Build Docker Image Back / run (push) Successful in 23s
Reviewed-on: #11
Co-authored-by: Clement <c.boesmier@aptatio.com>
Co-committed-by: Clement <c.boesmier@aptatio.com>
2024-05-19 11:38:38 +02:00
1bbc7a2aa3 feat: init-express-backend (#6)
All checks were successful
Build Docker Image Front / run (push) Successful in 23s
Build Docker Image Back / run (push) Successful in 23s
Reviewed-on: #6
Co-authored-by: Clement <c.boesmier@aptatio.com>
Co-committed-by: Clement <c.boesmier@aptatio.com>
2024-05-06 21:20:29 +02:00
198 changed files with 19909 additions and 2651 deletions

View File

@ -6,9 +6,9 @@ on: #declancheur
- '*'
tags:
- v*
# pull_request:
# branches:
# - master
pull_request:
branches:
- master
jobs:
run: #jobs ID (nom du jobs)
@ -32,8 +32,6 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=semver,pattern=latest
labels: |
org.opencontainers.image.title=AstroFront
- name: Login to Gitea
uses: docker/login-action@v3

View File

@ -32,8 +32,6 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=semver,pattern=latest
labels: |
org.opencontainers.image.title=AstroFront
- name: Login to Gitea
uses: docker/login-action@v3
@ -48,8 +46,8 @@ jobs:
- name: Build and push back
uses: docker/build-push-action@v5
with:
context: ./front
context: ./Express/barAndCafe
push: true
file: ./front/Dockerfile
file: ./Express/barAndCafe/Dockerfile
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

46
.github/workflows/build_jsdoc_drink.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: JsDocs
on:
pull_request:
branches:
- '*'
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- name: Checkout # rapatrie le depot
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
git.lab-ouest.org/Epitech/ratrapage_T-WEB_drink_jsdocs
tags: |
type=edge
type=ref,event=pr
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=semver,pattern=latest
- name: Login to Gitea
uses: docker/login-action@v3
with:
registry: git.lab-ouest.org
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v3
- name: Build and push docs
uses: docker/build-push-action@v5
with:
context: ./Express/barAndCafe
push: true
file: ./Express/barAndCafe/Dockerfile_docs
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -17,3 +17,5 @@ jobs:
custom-title: Coverage report for backend
github-token: ${{ secrets.PR_TOCKEN}}
annotations: none #disable annotation
env:
OPEN_TRIP_MAPS_KEY: ${{ secrets.OPEN_TRIP_MAPS_KEY}}

View File

@ -1,5 +1,3 @@
# This Dockerfile allows you to run AstroJS in server mode
#########
# Build #
#########
@ -49,7 +47,8 @@ WORKDIR /home/node
EXPOSE 3000
# Add Healthcheck
HEALTHCHECK --interval=10s --timeout=10s --start-period=5s --retries=3 CMD wget --no-verbose --tries=1 --spider http://localhost:3000 || exit 1
# FIXME: faire en sorte que le healthcheck fonctionne
# HEALTHCHECK --interval=10s --timeout=10s --start-period=5s --retries=3 CMD wget --no-verbose --tries=1 --spider http://localhost:3000 || exit 1
# copy from build image
COPY --chown=node:node --from=BUILD_IMAGE /home/node/node_modules ./node_modules

View File

@ -0,0 +1,35 @@
#########
# Build #
#########
FROM docker.io/node:20-alpine as BUILD_IMAGE
# External deps (for node-gyp add: "python3 make g++")
RUN apk add --no-cache git
# run as non root user
USER node
# go to user repository
WORKDIR /home/node
# Add package json
ADD --chown=node:node package.json package-lock.json ./
# install dependencies from package lock
RUN npm ci
# Add project files
ADD --chown=node:node . .
# build
RUN npm run docs
# remove dev deps
RUN npm prune --omit=dev
##############
# Production #
##############
FROM httpd:alpine
COPY --from=BUILD_IMAGE /home/node/out/ /usr/local/apache2/htdocs/

View File

@ -1,5 +1,9 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
testPathIgnorePatterns: [
"<rootDir>/dist/"
],
testTimeout: 10000,
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverage: true,
@ -10,6 +14,7 @@ module.exports = {
'!/jest.config.js/',
'!/coverage/',
'!**/server.ts',
'!**/swaggerDef.ts'
],
coverageThreshold: {
global: {

View File

@ -3,13 +3,18 @@
"template": "node_modules/better-docs"
},
"tags": {
"allowUnknownTags": ["optional"]
"allowUnknownTags": true
},
"plugins": [
"node_modules/better-docs/typescript"
"node_modules/better-docs/typescript",
"plugins/markdown",
"jsdoc-mermaid"
],
"source": {
"include": ["./src"],
"includePattern": "\\.(jsx|js|ts|tsx)$"
},
"templates": {
"search": true
}
}

File diff suppressed because it is too large Load Diff

View File

@ -2,31 +2,42 @@
"main": "dist/server.js",
"scripts": {
"build": "rimraf dist && npx tsc",
"start": "node dist/index.js",
"start": "node dist/src/server.js",
"predev": "npm run build",
"dev": "npx tsc -w & nodemon dist/server.js",
"dev": "npx tsc -w & nodemon dist/src/server.js",
"predocs": "npm run build",
"docs": "jsdoc -c jsdoc.json",
"test": "jest"
"pretest": "npm run build",
"test": "ts-node -O '{\"module\":\"commonjs\"}' node_modules/jest/bin/jest.js"
},
"dependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.12.7",
"axios": "^1.6.8",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"rimraf": "^5.0.5"
"rimraf": "^5.0.5",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/jest": "^29.5.12",
"@types/rewire": "^2.5.30",
"@types/supertest": "^6.0.2",
"babel-cli": "^6.26.0",
"babel-preset-env": "^1.7.0",
"better-docs": "^2.7.3",
"jest": "^29.7.0",
"jsdoc": "^4.0.3",
"jsdoc-mermaid": "^1.0.0",
"nodemon": "^3.1.0",
"rewire": "^7.0.0",
"superagent": "^9.0.2",
"supertest": "^7.0.0",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"typedoc": "^0.25.13",
"typescript": "^5.4.5"
}

View File

@ -1,22 +1,169 @@
import express from "express";
import { getCity, getRadius, getPoiId, getBox } from "./openTripMaps"
import express from "express"
import cors from "cors"
/**
* Initialize Express application instance.
* @returns An initialized Express application object.
*/
const app = express();
const app = express()
app.use(cors())
/**
* Handle GET request for homepage route ('/').
* Send back a simple text response.
* Handle GET request for homepage route ('/welcome').
* Send back a simple json response.
* @param {express.Request} req - HTTP Request object.
* @param {express.Response} res - HTTP Response object.
*/
function getRoot(req: express.Request, res: express.Response) {
res.send("hello world from dev");
function getWelcome(req: express.Request, res: express.Response) {
const out = {hello:"world"}
res.send(out)
}
app.get("/", getRoot);
/**
* @openapi
* /welcome:
* get:
* summary: retun just hello
* description: Welcome to swagger-jsdoc!
* responses:
* 200:
* description: Returns a welcome json.
*/
app.get("/welcome", getWelcome)
export default app;
/**
* @openapi
* /otm/city:
* get:
* summary: return the drinks in a city
* description: return the drinks in a defined perimeter in a city
* parameters:
* - name: name
* in: query
* required: true
* description: the name of the city
* schema:
* type: string
* responses:
* 200:
* description: Return a list of bars and coffee in city in geoJSON format
* 400:
* description: Missing Argument Error
* 401:
* description: Missing OTM tocken
*/
app.get("/otm/city", getCity)
/**
* @openapi
* /otm/radius:
* get:
* summary: return the drinks in a radius
* description: return the drinks in a defined radius
* parameters:
* - name: lon
* in: query
* required: true
* description: longitude of the center of the radius
* schema:
* type: number
* minimum: -180
* maximum: 180
* - name: lat
* in: query
* required: true
* description: latitude of the center of the radius
* schema:
* type: number
* minimum: -90
* maximum: 90
* - name: radius
* in: query
* description: size of the radius
* responses:
* 200:
* description: Return a list of bars and coffee in city in geoJSON format
* 400:
* description: Missing Argument Error
* 401:
* description: Missing OTM tocken
*/
app.get("/otm/radius", getRadius)
/**
* @openapi
* /otm/poidetaill:
* get:
* summary: detail of a POI
* description: detail of a POI link name, kind, rate,...
* parameters:
* - name: id
* in: query
* required: true
* description: Unique identifier of the object in OpenTripMap
* schema:
* type: string
* responses:
* 200:
* description: Return the detaill of an POI in otm
* 400:
* description: Missing Argument Error
* 401:
* description: Missing OTM tocken
*/
app.get("/otm/poidetaill", getPoiId)
/**
* @openapi
* /otm/box:
* get:
* summary: return the drinks in a box
* description: return the drinks in a defined box
* parameters:
* - name: lon1
* in: query
* required: true
* description: longitude 1 of the box
* schema:
* type: number
* minimum: -180
* maximum: 180
* - name: lat1
* in: query
* required: true
* description: latitude 1 of the box
* schema:
* type: number
* minimum: -90
* maximum: 90
* - name: lon2
* in: query
* required: true
* description: longitude 2 of the box
* schema:
* type: number
* minimum: -180
* maximum: 180
* - name: lat2
* in: query
* required: true
* description: latitude 2 of the box
* schema:
* type: number
* minimum: -90
* maximum: 90
* responses:
* 200:
* description: Return a list of bars and coffee in city in geoJSON format
* 400:
* description: Missing Argument Error
* 401:
* description: Missing OTM tocken
*/
app.get("/otm/box", getBox)
export default app

View File

@ -0,0 +1,198 @@
import axios from 'axios'
import express from "express"
import * as dotenv from "dotenv"
dotenv.config({path: '../../.env'})
const key = process.env.OPEN_TRIP_MAPS_KEY
/**
* make a GET request to the OTM for a rectangle search.
* @param {string} lon1 Longitude of the 1st point of the box
* @param {string} lat1 Latitude of the 1st point of the box
* @param {string} lon2 Longitude of the 2nd point of the box
* @param {string} lat2 Latitude of the 2nd point of the box
* @returns {FeatureCollection} a list of POIs with their type, id, etc. (cf: [opentripmap](https://dev.opentripmap.org/docs#))
*/
async function callBox(lon1:string, lat1:string, lon2: string, lat2: string, rate: string) {
const lonMin = Math.min(parseFloat(lon1), parseFloat(lon2))
const lonMax = Math.max(parseFloat(lon1), parseFloat(lon2))
const latMin = Math.min(parseFloat(lat1), parseFloat(lat2))
const latMax = Math.max(parseFloat(lat1), parseFloat(lat2))
const options = {
method: 'GET',
url: 'https://api.opentripmap.com/0.1/en/places/bbox',
params: {
lon_min: lonMin,
lon_max: lonMax,
lat_min: latMin,
lat_max: latMax,
rate: rate,
apikey: key,
kinds: 'bars,cafes,pubs,biergartens'
},
headers: {'Content-Type': 'application/json'}
}
try {
const { data } = await axios.request(options)
return data
} catch (error) {
console.error(error)
}
}
/**
* make GET request to the OTM for radius search.
* @param {string} lon Longitude of radius center point
* @param {string} lat Latitude du point central du rayon
* @param {string} radius search radius size in meters
* @returns {FeatureCollection} a list of POIs with their type, id, etc. (cf: [opentripmap](https://dev.opentripmap.org/docs#))
*/
async function callRadius(lon: string, lat: string, radius = '1000') {
const optionsDrink = {
method: 'GET',
url: 'https://api.opentripmap.com/0.1/en/places/radius',
params: {
radius: radius,
lon: lon,
lat: lat,
apikey: key,
kinds: 'bars,cafes,pubs,biergartens'
},
headers: {'Content-Type': 'application/json'}
}
try {
const { data } = await axios.request(optionsDrink)
return data
} catch (error) {
console.error(error)
}
}
/**
* make GET request to the OTM for city info.
* @param {string} name Name of a city
* @returns {Geoname} info of a search city their name, country, lat, lon, etc. (cf: [opentripmap](https://dev.opentripmap.org/docs#))
*/
async function callCity(name:string) {
const optionsCity = {
method: 'GET',
url: 'https://api.opentripmap.com/0.1/en/places/geoname',
params: {
name: name,
apikey: key
},
headers: {'Content-Type': 'application/json'}
}
try {
const { data } = await axios.request(optionsCity)
return data
} catch (error) {
console.error(error)
}
}
/**
* make GET request to the OTM for city info.
* @param {string} name Name of a city
* @returns {Geoname} info of a search city their name, country, lat, lon, etc. (cf: [opentripmap](https://dev.opentripmap.org/docs#))
*/
async function callId(id:string) {
const optionsId = {
method: 'GET',
url: 'https://api.opentripmap.com/0.1/en/places/xid/' + id,
params: {
apikey: key
},
headers: {'Content-Type': 'application/json'}
}
try {
const { data } = await axios.request(optionsId)
return data
} catch (error) {
console.error(error)
}
}
/**
* Handle GET request for city search route ('/otm/city').
* @param {express.Request} req - HTTP Request object.
* @param {express.Response} res - HTTP Response object.
*/
export async function getCity(req: express.Request, res: express.Response) {
const cityName = req.query["name"]
let radius = req.query["radius"]
if(!cityName){
res.status(400).send("Missing Argument name")
return
}
if(!radius){
radius = "1000"
}
const cityPose = await callCity(cityName as string)
res.send( await callRadius(cityPose.lon,cityPose.lat, radius as string))
}
/**
* Handle GET request for radius search route ('/otm/radius').
* @param {express.Request} req - HTTP Request object.
* @param {express.Response} res - HTTP Response object.
*/
export async function getRadius(req:express.Request, res: express.Response) {
const lon = req.query["lon"] as string
const lat = req.query["lat"] as string
let radius = req.query["radius"]
if(!lon || !lat){
res.status(400).send("Missing Argument")
return
}
if(!radius){
radius = "1000"
}
res.send( await callRadius(lon,lat,radius as string))
}
/**
* Handle GET request for radius search route ('/otm/radius').
* @param {express.Request} req - HTTP Request object.
* @param {express.Response} res - HTTP Response object.
*/
export async function getPoiId(req: express.Request, res: express.Response){
const id = req.query["id"] as string
if(!id){
res.status(400).send("Missing Argument name")
return
}
res.send( await callId( id as string))
}
/**
* Handle GET request for radius search route ('/otm/box').
* @param {express.Request} req - HTTP Request object.
* @param {express.Response} res - HTTP Response object.
*/
export async function getBox(req:express.Request, res: express.Response) {
const lon1 = req.query["lon1"] as string
const lat1 = req.query["lat1"] as string
const lon2 = req.query["lon2"] as string
const lat2 = req.query["lat2"] as string
let rate = req.query["rate"] as string
if(!lon1 || !lat1 || !lon2 || !lat2){
res.status(400).send("Missing Argument")
return
}
if(!rate){
rate = "1";
}
res.send( await callBox(lon1, lat1, lon2, lat2, rate))
}

View File

@ -1,10 +1,16 @@
import * as dotenv from "dotenv";
import app from "./app";
import options from "./swaggerDef";
const swaggerJsdoc = require("swagger-jsdoc"),
swaggerUi = require("swagger-ui-express");
dotenv.config({path: '../../.env'})
const port = parseInt(process.env.BAR_PORT || '3000')
const specs = swaggerJsdoc(options)
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs))
app.listen(port, () =>{
console.log(`serveur running in ${port}`)
})

View File

@ -0,0 +1,29 @@
import * as dotenv from "dotenv";
dotenv.config({path: '../../.env'})
const port = parseInt(process.env.BAR_PORT || '3000')
const options = {
definition: {
openapi: "3.1.0",
info: {
title: "LogRocket Express API with Swagger",
version: "0.1.0",
description:
"This is a simple CRUD API application made with Express and documented with Swagger",
},
servers: [
{
url: "http://localhost:"+port.toString(),
},
{
url: "https://drink-tweb.cb85.fr"
}
],
},
apis: ['./src/*.ts','./dist/src/*.js'],
explorer: true
}
export default options

View File

@ -1,37 +1,30 @@
import request from "supertest";
import app from "../src/app";
import { Server, IncomingMessage, ServerResponse } from "http";
import * as dotenv from "dotenv";
const port = 3000;
dotenv.config({path: '../../.env'})
const port = parseInt(process.env.BAR_PORT || '3000')
let serveur : Server<typeof IncomingMessage, typeof ServerResponse>
describe("Test the root path", () => {
describe("Test the welcome path", () => {
beforeEach(() => {
serveur = app.listen(port, () =>{
console.log(`serveur running in ${port}`)
})
})
afterEach((done) => {
serveur.close(done)
})
test("It should response the GET method", done => {
test("It should response the 200 code for GET method", done => {
request(app)
.get("/")
.get("/welcome")
.then(response => {
expect(response.statusCode).toBe(200);
done();
});
});
test("It should response the GET method", done => {
test("It should response the GET method with content", done => {
const out = {hello:"world"};
request(app)
.get("/")
.get("/welcome")
.then(response => {
expect(response.text).toEqual("hello world from dev")
expect(response.text).toEqual(JSON.stringify(out))
done();
});
});

View File

@ -0,0 +1,280 @@
import request from "supertest"
import app from "../src/app"
import { Server, IncomingMessage, ServerResponse } from "http"
import * as dotenv from "dotenv"
dotenv.config({path: '../../.env'})
const port = parseInt(process.env.BAR_PORT || '3000')
let serveur : Server<typeof IncomingMessage, typeof ServerResponse>
describe("Test the otm city path", () => {
test("It should response the 200 code for GET method", done => {
request(app)
.get("/otm/city")
.query({'name':'La roche sur yon'})
.then(response => {
expect(response.statusCode).toBe(200)
done()
})
})
test("It should response the GET method with content", done => {
const out = {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"id": "562635",
"geometry": {
"type": "Point",
"coordinates": [
-1.4344594,
46.6686478
]
},
"properties": {
"xid": "N4032296324",
"name": "Le 27 point carré",
"dist": 236.40360026,
"rate": 1,
"osm": "node/4032296324",
"kinds": "foods,bars,tourist_facilities"
}
}]
}
request(app)
.get("/otm/city")
.query({'name':'La roche sur yon', "radius": 240})
.then(response => {
console.log(response.text)
expect(response.text).toEqual(JSON.stringify(out))
done()
})
})
test("It should response the 400 code for GET method", done => {
request(app)
.get("/otm/city")
.then(response => {
console.log(response.text)
expect(response.statusCode).toBe(400)
done()
})
})
test("It should response the 200 code for GET method", done => {
request(app)
.get("/otm/radius")
.query({'lon':'-1.4344594', 'lat' : '46.6686478', 'radius': '10'})
.then(response => {
expect(response.statusCode).toBe(200)
done()
})
})
test("It should response the 200 code for GET method (default radius)", done => {
request(app)
.get("/otm/radius")
.query({'lon':'-1.4344594', 'lat' : '46.6686478'})
.then(response => {
expect(response.statusCode).toBe(200)
done()
})
})
test("It should response the 400 code for GET method (missing lon and lat)", done => {
request(app)
.get("/otm/radius")
.then(response => {
expect(response.statusCode).toBe(400)
done()
})
})
test("Get bar in radius from API", done => {
const out = {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"id": "562635",
"geometry": {
"type": "Point",
"coordinates": [
-1.4344594,
46.6686478
]
},
"properties": {
"xid": "N4032296324",
"name": "Le 27 point carré",
"dist": 0.17652159,
"rate": 1,
"osm": "node/4032296324",
"kinds": "foods,bars,tourist_facilities"
}
}]
}
request(app)
.get("/otm/radius")
.query({'lon':'-1.4344594', 'lat' : '46.6686478', 'radius': '10'})
.then(response => {
expect(response.text).toEqual(JSON.stringify(out))
done()
})
})
test("It should response the 200 code for GET method", done => {
request(app)
.get("/otm/poidetaill")
.query({'id':'562635'})
.then(response => {
expect(response.statusCode).toBe(200)
done()
})
})
test("Get bar in radius from API", done => {
const out = {
"xid": "N4032296324",
"name": "Le 27 point carré",
"address": {
"road": "Rue Raymond Poincaré",
"town": "La Roche-sur-Yon",
"state": "Pays de la Loire",
"county": "La Roche-sur-Yon",
"suburb": "Zola",
"country": "France",
"postcode": "85000",
"country_code": "fr",
"house_number": "27",
"neighbourhood": "Cité des Forges"
},
"rate": "1",
"osm": "node/4032296324",
"kinds": "foods,bars,tourist_facilities",
"sources": {
"geometry": "osm",
"attributes": [
"osm"
]
},
"otm": "https://opentripmap.com/en/card/N4032296324",
"point": {
"lon": -1.4344594478607178,
"lat": 46.66864776611328
}
}
request(app)
.get("/otm/poidetaill")
.query({'id':'562635'})
.then(response => {
expect(response.text).toEqual(JSON.stringify(out))
done()
})
})
test("It should response the 400 code for GET method (no id)", done => {
request(app)
.get("/otm/poidetaill")
.then(response => {
expect(response.statusCode).toBe(400)
done()
})
})
test("It should response the 200 code for GET method", done => {
request(app)
.get("/otm/box")
.query({'lon1':'-1.435199','lon2':'-1.43519', 'lat1':'46.668460', 'lat2':'46.668461'})
.then(response => {
expect(response.statusCode).toBe(200)
done()
})
})
test("Get bar in box from API", done => {
const out = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "562633",
"geometry": {
"type": "Point",
"coordinates": [
-1.435197,
46.6684608
]
},
"properties": {
"xid": "N4032296323",
"name": "Le Cube",
"rate": 1,
"osm": "node/4032296323",
"kinds": "foods,cafes,tourist_facilities"
}
}
]
}
function sleep(milliseconds: number) {
const date = Date.now();
let currentDate = null;
do {
currentDate = Date.now();
} while (currentDate - date < milliseconds);
}
sleep(2000);
request(app)
.get("/otm/box")
.query({'lon1':'-1.435199','lon2':'-1.43519', 'lat1':'46.668460', 'lat2':'46.668461'})
.then(response => {
expect(response.text).toEqual(JSON.stringify(out))
done()
})
})
test("Get bar in box from API lat reversed", done => {
const out = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "562633",
"geometry": {
"type": "Point",
"coordinates": [
-1.435197,
46.6684608
]
},
"properties": {
"xid": "N4032296323",
"name": "Le Cube",
"rate": 1,
"osm": "node/4032296323",
"kinds": "foods,cafes,tourist_facilities"
}
}
]
}
request(app)
.get("/otm/box")
.query({'lon1':'-1.435199','lon2':'-1.43519', 'lat2':'46.668460', 'lat1':'46.668461'})
.then(response => {
expect(response.text).toEqual(JSON.stringify(out))
done()
})
})
test("It should response the 400 code for GET method (no coordonaite)", done => {
request(app)
.get("/otm/box")
.then(response => {
expect(response.statusCode).toBe(400)
done()
})
})
})

View File

@ -0,0 +1,18 @@
{
"folders": [
{
"name": "Ratrapage_WEB",
"path": "."
},
{
"name": "barAndCafe",
"path": "Express/barAndCafe"
},
{
"path": "front"
}
],
"settings": {
"typescript.tsdk": "node_modules/typescript/lib"
}
}

View File

@ -0,0 +1,30 @@
meta {
name: Google API
type: http
seq: 3
}
post {
url: https://places.googleapis.com/v1/places:searchNearby
body: json
auth: none
}
headers {
X-Goog-Api-Key: {{GOOGLE_API_KEY}}
}
body:json {
{
"includedTypes": ["restaurant"],
"maxResultCount": 10,
"locationRestriction": {
"circle": {
"center": {
"latitude": 37.7937,
"longitude": -122.3965},
"radius": 500.0
}
}
}
}

View File

@ -1,3 +1,9 @@
vars {
TRIPMAP_URL: https://api.opentripmap.com/0.1
EXPRESS_API: http://localhost:3001
}
vars:secret [
app_key
app_key,
OTM_KEY,
GOOGLE_API_KEY
]

View File

@ -0,0 +1,8 @@
vars {
EXPRESS_API: https://drink-tweb.cb85.fr
PB_URL: https://pb-tweb.cb85.fr
TRIPMAP_URL: https://api.opentripmap.com/0.1/
}
vars:secret [
OTM_KEY
]

View File

@ -1,7 +1,7 @@
meta {
name: express test
name: express Drink OTM
type: http
seq: 3
seq: 4
}
get {

View File

@ -0,0 +1,20 @@
meta {
name: otm_ex_box
type: http
seq: 4
}
get {
url: http://localhost:3001/otm/box?lon1=-1.435199&lon2=-1.43519&lat1=46.668460&lat2=46.668461&apikey={{OTM_KEY}}&kinds=bars,cafes,pubs,biergartens
body: none
auth: none
}
query {
lon1: -1.435199
lon2: -1.43519
lat1: 46.668460
lat2: 46.668461
apikey: {{OTM_KEY}}
kinds: bars,cafes,pubs,biergartens
}

View File

@ -0,0 +1,16 @@
meta {
name: otm_ex_city
type: http
seq: 1
}
get {
url: {{EXPRESS_API}}/otm/city?name=La roche sur yon&radius=300
body: none
auth: none
}
query {
name: La roche sur yon
radius: 300
}

View File

@ -0,0 +1,15 @@
meta {
name: otm_ex_drink_id
type: http
seq: 2
}
get {
url: https://api.opentripmap.com/0.1/en/places/xid/562635?apikey={{OTM_KEY}}
body: none
auth: none
}
query {
apikey: {{OTM_KEY}}
}

View File

@ -0,0 +1,16 @@
meta {
name: otm_ex_radius
type: http
seq: 3
}
get {
url: http://localhost:3001/otm/city?name=La roche sur yon&radius=300
body: none
auth: none
}
query {
name: La roche sur yon
radius: 300
}

View File

@ -0,0 +1,11 @@
meta {
name: list oaut methode
type: http
seq: 5
}
get {
url: https://pb-tweb.cb85.fr/api/collections/users/auth-methods
body: none
auth: none
}

View File

@ -0,0 +1,18 @@
meta {
name: oauth test
type: http
seq: 4
}
post {
url: https://pb-tweb.cb85.fr/api/collections/users/auth-with-oauth2
body: json
auth: none
}
body:json {
{
provider: "google"
}
}

View File

@ -0,0 +1,16 @@
meta {
name: otm_city
type: http
seq: 5
}
get {
url: {{TRIPMAP_URL}}/en/places/geoname?name=paris&apikey={{OTM_KEY}}
body: none
auth: none
}
query {
name: paris
apikey: {{OTM_KEY}}
}

View File

@ -0,0 +1,19 @@
meta {
name: otm_drink
type: http
seq: 1
}
get {
url: {{TRIPMAP_URL}}/en/places/radius?radius=10&lon=-1.4344594&lat=46.6686478&apikey={{OTM_KEY}}&kinds=bars,cafes,pubs,biergartens
body: none
auth: none
}
query {
radius: 10
lon: -1.4344594
lat: 46.6686478
apikey: {{OTM_KEY}}
kinds: bars,cafes,pubs,biergartens
}

View File

@ -0,0 +1,19 @@
meta {
name: otm_drink_bbox
type: http
seq: 4
}
get {
url: {{TRIPMAP_URL}}/en/places/radius?radius=10&lon=-1.4344594&lat=46.6686478&apikey={{OTM_KEY}}&kinds=bars,cafes,pubs,biergartens
body: none
auth: none
}
query {
radius: 10
lon: -1.4344594
lat: 46.6686478
apikey: {{OTM_KEY}}
kinds: bars,cafes,pubs,biergartens
}

View File

@ -0,0 +1,15 @@
meta {
name: otm_drink_id
type: http
seq: 3
}
get {
url: https://api.opentripmap.com/0.1/en/places/xid/562635?apikey={{OTM_KEY}}
body: none
auth: none
}
query {
apikey: {{OTM_KEY}}
}

View File

@ -0,0 +1,20 @@
meta {
name: otm_location
type: http
seq: 2
}
get {
url: https://api.opentripmap.com/0.1/en/places/bbox?lon_min=-1.435199&lon_max=-1.43519&lat_min=46.668460&lat_max=46.668461&apikey={{OTM_KEY}}&kinds=bars,cafes,pubs,biergartens
body: none
auth: none
}
query {
lon_min: -1.435199
lon_max: -1.43519
lat_min: 46.668460
lat_max: 46.668461
apikey: {{OTM_KEY}}
kinds: bars,cafes,pubs,biergartens
}

View File

@ -0,0 +1,18 @@
meta {
name: connexion
type: http
seq: 1
}
post {
url: {{PB_URL}}/api/collections/users/auth-with-password
body: json
auth: none
}
body:json {
{
"identity": "michel.biche@aptatio.com",
"password": "123456789"
}
}

View File

@ -0,0 +1,27 @@
meta {
name: get all poi
type: http
seq: 5
}
get {
url: {{PB_URL}}/api/collections/user_poi/records?filter=owner='vvy93m1hoaeshwy'&&poi_list='cabkkovmjsfoapa'
body: json
auth: bearer
}
query {
filter: owner
poi_list: 'cabkkovmjsfoapa'
}
auth:bearer {
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3MTg4Mjg0NzEsImlkIjoidnZ5OTNtMWhvYWVzaHd5IiwidHlwZSI6ImF1dGhSZWNvcmQifQ.pC7u-QaZ_BYqWA5wG8wu1lRbbd4mKuKeAveWe_IBnfU
}
body:json {
{
"owner": "vvy93m1hoaeshwy",
"poi_list": "sh430u0im37cxm5"
}
}

View File

@ -0,0 +1,22 @@
meta {
name: link user to poi
type: http
seq: 3
}
post {
url: {{PB_URL}}/api/collections/user_poi/records
body: json
auth: bearer
}
auth:bearer {
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3MTg4Mjg0NzEsImlkIjoidnZ5OTNtMWhvYWVzaHd5IiwidHlwZSI6ImF1dGhSZWNvcmQifQ.pC7u-QaZ_BYqWA5wG8wu1lRbbd4mKuKeAveWe_IBnfU
}
body:json {
{
"owner": "vvy93m1hoaeshwy",
"poi_list": "sh430u0im37cxm5"
}
}

View File

@ -0,0 +1,40 @@
meta {
name: send a poi
type: http
seq: 2
}
post {
url: {{PB_URL}}/api/collections/POI/records
body: json
auth: bearer
}
auth:bearer {
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3MTg4Mjg0NzEsImlkIjoidnZ5OTNtMWhvYWVzaHd5IiwidHlwZSI6ImF1dGhSZWNvcmQifQ.pC7u-QaZ_BYqWA5wG8wu1lRbbd4mKuKeAveWe_IBnfU
}
body:json {
{
"Poi_id": "11472887",
"Poi": {
"type": "Feature",
"id": "11472887",
"geometry": {
"type": "Point",
"coordinates": [
-0.0626024,
51.4924088
]
},
"properties": {
"xid": "W544344833",
"name": "The Blue Anchor",
"rate": 2,
"osm": "way/544344833",
"wikidata": "Q7718716",
"kinds": "pubs,foods,shops,marketplaces,tourist_facilities"
}
}
}
}

View File

@ -0,0 +1,37 @@
meta {
name: send fav to astro
type: http
seq: 4
}
post {
url: http://localhost:3000/maps/save_poi
body: json
auth: bearer
}
auth:bearer {
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3MTg4ODY2OTIsImlkIjoidnZ5OTNtMWhvYWVzaHd5IiwidHlwZSI6ImF1dGhSZWNvcmQifQ.R9PBGlHn6aBHt89g6G0NykMh_Vye24OpKEUYtz6R6Og
}
body:json {
{
"type": "Feature",
"id": "11472888",
"geometry": {
"type": "Point",
"coordinates": [
-0.0626024,
51.4924088
]
},
"properties": {
"xid": "W544344833",
"name": "The Blue Anchor",
"rate": 2,
"osm": "way/544344833",
"wikidata": "Q7718716",
"kinds": "pubs,foods,shops,marketplaces,tourist_facilities"
}
}
}

View File

@ -0,0 +1,22 @@
meta {
name: test graphQL
type: graphql
seq: 3
}
post {
url: https://datatour-tweb.cb85.fr
body: graphql
auth: none
}
body:graphql {
{
poi {
total
results {
_uri
}
}
}
}

@ -1 +1 @@
Subproject commit fb594bfd7c715a891f076750403e3a439b804468
Subproject commit b3bc1fdc68a325905f6d466e860732466d8e750a

View File

@ -1,12 +1,14 @@
version: "3.8"
services:
pocketbase:
image: ghcr.io/coollabsio/pocketbase:latest
environment:
- SERVICE_FQDN_POCKETBASE_8080
volumes:
- ./.pb/pocketbase-data:/app/pb_data
- ./.pb/pocketbase-hooks:/app/pb_hooks
- ./pb/pocketbase-data:/app/pb_data
- ./pb/pocketbase-hooks:/app/pb_hooks
labels:
- traefik.enable=true
- traefik.http.routers.poketBaseTweb.rule=Host(`${POCKET_BASE_URL}`)
@ -14,8 +16,11 @@ services:
- traefik.http.routers.poketBaseTweb.tls.certresolver=le
networks:
- public
front:
image: git.lab-ouest.org/epitech/ratrapage_t-web_front:pr-7-head
image: git.lab-ouest.org/epitech/ratrapage_t-web_front:pr-16-head
environment:
- POCKETBASE_URL=https://${POCKET_BASE_URL}
depends_on:
- pocketbase
labels:
@ -25,10 +30,69 @@ services:
- traefik.http.routers.astroTweb.tls.certresolver=le
networks:
- public
back_drink:
image: git.lab-ouest.org/epitech/ratrapage_t-web_back:master
environment:
- port=${BACK_BASE_PORT}
- OPEN_TRIP_MAPS_KEY=${OPEN_TRIP_MAPS_KEY}
labels:
- traefik.enable=true
- traefik.http.routers.expressDrinkTweb.rule=Host(`${DRINK_URL}`)
- traefik.http.services.expressDrinkTweb.loadbalancer.server.port=${BACK_BASE_PORT}
- traefik.http.routers.expressDrinkTweb.tls.certresolver=le
networks:
- public
docs_drink:
image: git.lab-ouest.org/epitech/ratrapage_t-web_drink_jsdocs:pr-6-head
labels:
- traefik.enable=true
- traefik.http.routers.docsDrinkTweb.rule=Host(`${DOCS_DRINK_URL}`)
- traefik.http.services.docsDrinkTweb.loadbalancer.server.port=${DOCS_BACK_BASE_PORT}
- traefik.http.routers.docsDrinkTweb.tls.certresolver=le
networks:
- public
blazegraph:
image: conjecto/blazegraph:2.1.5
# ports:
# - 9999:9999
environment:
JAVA_OPTS: -Xms6G -Xmx6G
volumes:
- ./datatourisme/dataset:/docker-entrypoint-initdb.d
ulimits:
nofile:
soft: 65536
hard: 65536
networks:
- graphQL
restart: unless-stopped
datatourisme:
image: git.lab-ouest.org/epitech/fork-open-data-tourism:master
depends_on:
- blazegraph
networks:
- graphQL
- public
restart: unless-stopped
labels:
- traefik.enable=true
- traefik.http.routers.dataTourisme.rule=Host(`${DATA_TOURISME_URL}`)
- traefik.http.services.dataTourisme.loadbalancer.server.port=${DATA_TOURISME_BASE_PORT}
- traefik.http.routers.dataTourisme.tls.certresolver=le
networks:
graphQL: {}
public:
external: true
x-dockge:
urls:
- https://${POCKET_BASE_URL}/
- https://${FRONT_URL}/
- https://${DOCS_DRINK_URL}/
- https://${DRINK_URL}/
- https://${DATA_TOURISME_URL}/

12
front/.editorconfig Normal file
View File

@ -0,0 +1,12 @@
# https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = false

8
front/.eslintignore Normal file
View File

@ -0,0 +1,8 @@
node_modules
out
.next
next-env.d.ts
*.js
__tests__

324
front/.eslintrc.json Normal file
View File

@ -0,0 +1,324 @@
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:astro/recommended",
"plugin:@typescript-eslint/strict",
"plugin:@typescript-eslint/stylistic"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"overrides": [
{
"files": [
"*.astro"
],
"parser": "astro-eslint-parser",
"parserOptions": {
"extraFileExtensions": [
".astro"
],
"parser": "@typescript-eslint/parser"
}
},
{
"files": [
"*.ts",
"*.tsx",
"*.astro"
],
"rules": {
"no-extra-parens": "off",
"no-unused-expressions": "off",
"no-shadow": "off",
"quotes": "off",
"semi": "off",
"space-before-function-paren": "off"
}
}
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true,
"project": "tsconfig.json",
"sourceType": "module",
"ecmaVersion": 2018
}
},
"plugins": [
"@typescript-eslint"
],
"root": true,
"rules": {
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/space-before-function-paren": [
"error",
{
"anonymous": "never",
"asyncArrow": "always",
"named": "never"
}
],
"@typescript-eslint/triple-slash-reference": "error",
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unified-signatures": "error",
"@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/array-type": [
"error",
{
"default": "generic"
}
],
"arrow-body-style": "error",
"arrow-parens": [
"error",
"always"
],
"@typescript-eslint/ban-types": [
"error",
{
"types": {
"{}": false
}
}
],
"complexity": [
"warn",
10
],
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"constructor-super": "error",
"curly": "error",
"dot-notation": "error",
"eol-last": "error",
"eqeqeq": [
"error",
"smart"
],
"@typescript-eslint/explicit-member-accessibility": [
"error",
{
"accessibility": "explicit"
}
],
"for-direction": "error",
"getter-return": "error",
"guard-for-in": "error",
"id-blacklist": [
"error",
"any",
"Number",
"number",
"String",
"string",
"Boolean",
"boolean",
"Undefined"
],
"id-length": [
"warn",
{
"exceptions": [
"_"
]
}
],
"id-match": "error",
"indent": [
"error",
"tab",
{
"SwitchCase": 1
}
],
"linebreak-style": [
"error",
"unix"
],
"max-classes-per-file": [
"error",
1
],
"max-depth": [
"warn",
2
],
"max-len": [
"warn",
{
"code": 256
}
],
"@typescript-eslint/member-delimiter-style": [
"error",
{
"multiline": {
"delimiter": "none",
"requireLast": true
},
"singleline": {
"delimiter": "comma",
"requireLast": false
}
}
],
"@typescript-eslint/member-ordering": "error",
"new-parens": "error",
"no-async-promise-executor": "error",
"no-await-in-loop": "warn",
"no-bitwise": "error",
"no-caller": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-console": "off",
"no-constant-condition": "error",
"no-control-regex": "warn",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-args": "error",
"no-dupe-else-if": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty": [
"error",
{
"allowEmptyCatch": true
}
],
"no-empty-character-class": "error",
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-interface": "error",
"no-eval": "error",
"no-ex-assign": "error",
"@typescript-eslint/no-explicit-any": "off",
"no-extra-boolean-cast": "error",
"no-extra-parens": "off",
"@typescript-eslint/no-extra-parens": [
"error",
"all",
{
"ignoreJSX": "all"
}
],
"no-extra-semi": "error",
"no-fallthrough": "off",
"no-func-assign": "error",
"no-import-assign": "error",
"no-inner-declarations": "error",
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-label-var": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"@typescript-eslint/no-misused-new": "error",
"no-multiple-empty-lines": "error",
"@typescript-eslint/no-namespace": "error",
"no-new-wrappers": "error",
"no-obj-calls": "error",
"no-promise-executor-return": "error",
"@typescript-eslint/no-parameter-properties": "off",
"no-prototype-builtins": "error",
"no-regex-spaces": "error",
"no-setter-return": "error",
"@typescript-eslint/no-shadow": "error",
"no-shadow": [
"error",
{
"builtinGlobals": false,
"hoist": "all"
}
],
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-template-curly-in-string": "warn",
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef": "error",
"no-undef-init": "error",
"no-underscore-dangle": "off",
"no-unexpected-multiline": "error",
"no-unreachable": "warn",
"no-unreachable-loop": "warn",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unsafe-optional-chaining": "error",
"no-unused-expressions": [
"error",
{
"allowTernary": true
}
],
"@typescript-eslint/no-unused-expressions": [
"error",
{
"allowTernary": true
}
],
"no-unused-labels": "error",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", {
"args": "all",
"argsIgnorePattern": "^_",
"caughtErrors": "all",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"ignoreRestSiblings": true
}],
"@typescript-eslint/no-non-null-assertion": ["warn"],
"no-var": "error",
"object-shorthand": [
"warn",
"methods"
],
"one-var": [
"error",
"never"
],
"prefer-const": "error",
"quote-props": [
"error",
"consistent-as-needed"
],
"quotes": "off",
"@typescript-eslint/quotes": [
"error",
"single",
{
"avoidEscape": true
}
],
"radix": "error",
"require-atomic-updates": "warn",
"semi": "off",
"@typescript-eslint/semi": [
"error",
"never"
],
"space-before-function-paren": [
"error",
{
"anonymous": "never",
"asyncArrow": "always",
"named": "never"
}
],
"spaced-comment": ["error", "always", { "block": { "exceptions": ["*"] } }],
"use-isnan": "error",
"valid-typeof": "warn"
}
}

10
front/.gitignore vendored
View File

@ -1,8 +1,6 @@
# build output
dist/
# generated types
.astro/
.output/
# dependencies
node_modules/
@ -13,6 +11,7 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
@ -20,5 +19,6 @@ pnpm-debug.log*
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/
pnpm-lock.yaml
.astro

2
front/.npmrc Normal file
View File

@ -0,0 +1,2 @@
# Expose Astro dependencies for `pnpm` users
shamefully-hoist=true

4
front/.prettierignore Normal file
View File

@ -0,0 +1,4 @@
dist
node_modules
.github
.changeset

13
front/.prettierrc.cjs Normal file
View File

@ -0,0 +1,13 @@
/** @type {import('prettier').Config} */
module.exports = {
printWidth: 120,
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'es5',
useTabs: false,
plugins: [require.resolve('prettier-plugin-astro')],
overrides: [{ files: '*.astro', options: { parser: 'astro' } }],
};

6
front/.stackblitzrc Normal file
View File

@ -0,0 +1,6 @@
{
"startCommand": "npm start",
"env": {
"ENABLE_CJS_IMPORTS": true
}
}

View File

@ -0,0 +1,275 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"site": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"site": {
"type": "string"
},
"base": {
"type": "string"
},
"trailingSlash": {
"type": "boolean"
},
"googleSiteVerificationId": {
"type": "string"
}
},
"required": ["name", "site", "base", "trailingSlash"],
"additionalProperties": false
},
"metadata": {
"type": "object",
"properties": {
"title": {
"type": "object",
"properties": {
"default": {
"type": "string"
},
"template": {
"type": "string"
}
},
"required": ["default", "template"]
},
"description": {
"type": "string"
},
"robots": {
"type": "object",
"properties": {
"index": {
"type": "boolean"
},
"follow": {
"type": "boolean"
}
},
"required": ["index", "follow"]
},
"openGraph": {
"type": "object",
"properties": {
"site_name": {
"type": "string"
},
"images": {
"type": "array",
"items": [
{
"type": "object",
"properties": {
"url": {
"type": "string"
},
"width": {
"type": "integer"
},
"height": {
"type": "integer"
}
},
"required": ["url", "width", "height"]
}
]
},
"type": {
"type": "string"
}
},
"required": ["site_name", "images", "type"]
},
"twitter": {
"type": "object",
"properties": {
"handle": {
"type": "string"
},
"site": {
"type": "string"
},
"cardType": {
"type": "string"
}
},
"required": ["handle", "site", "cardType"]
}
},
"required": ["title", "description", "robots", "openGraph", "twitter"]
},
"i18n": {
"type": "object",
"properties": {
"language": {
"type": "string"
},
"textDirection": {
"type": "string"
}
},
"required": ["language", "textDirection"]
},
"apps": {
"type": "object",
"properties": {
"blog": {
"type": "object",
"properties": {
"isEnabled": {
"type": "boolean"
},
"postsPerPage": {
"type": "integer"
},
"isRelatedPostsEnabled": {
"type": "boolean"
},
"relatedPostsCount": {
"type": "integer"
},
"post": {
"type": "object",
"properties": {
"isEnabled": {
"type": "boolean"
},
"permalink": {
"type": "string"
},
"robots": {
"type": "object",
"properties": {
"index": {
"type": "boolean"
},
"follow": {
"type": "boolean"
}
},
"required": ["index"]
}
},
"required": ["isEnabled", "permalink", "robots"]
},
"list": {
"type": "object",
"properties": {
"isEnabled": {
"type": "boolean"
},
"pathname": {
"type": "string"
},
"robots": {
"type": "object",
"properties": {
"index": {
"type": "boolean"
},
"follow": {
"type": "boolean"
}
},
"required": ["index"]
}
},
"required": ["isEnabled", "pathname", "robots"]
},
"category": {
"type": "object",
"properties": {
"isEnabled": {
"type": "boolean"
},
"pathname": {
"type": "string"
},
"robots": {
"type": "object",
"properties": {
"index": {
"type": "boolean"
},
"follow": {
"type": "boolean"
}
},
"required": ["index"]
}
},
"required": ["isEnabled", "pathname", "robots"]
},
"tag": {
"type": "object",
"properties": {
"isEnabled": {
"type": "boolean"
},
"pathname": {
"type": "string"
},
"robots": {
"type": "object",
"properties": {
"index": {
"type": "boolean"
},
"follow": {
"type": "boolean"
}
},
"required": ["index"]
}
},
"required": ["isEnabled", "pathname", "robots"]
}
},
"required": ["isEnabled", "postsPerPage", "post", "list", "category", "tag"]
}
},
"required": ["blog"]
},
"analytics": {
"type": "object",
"properties": {
"vendors": {
"type": "object",
"properties": {
"googleAnalytics": {
"type": "object",
"properties": {
"id": {
"type": ["string", "null"]
},
"partytown": {
"type": "boolean",
"default": true
}
},
"required": ["id"]
}
},
"required": ["googleAnalytics"]
}
},
"required": ["vendors"]
},
"ui": {
"type": "object",
"properties": {
"theme": {
"type": "string"
}
},
"required": ["theme"]
}
},
"required": ["site", "metadata", "i18n", "apps", "analytics", "ui"]
}

View File

@ -1,4 +1,10 @@
{
"recommendations": ["astro-build.astro-vscode"],
"recommendations": [
"astro-build.astro-vscode",
"bradlc.vscode-tailwindcss",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"unifiedjs.vscode-mdx"
],
"unwantedRecommendations": []
}

22
front/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,22 @@
{
"prettier.documentSelectors": ["**/*.astro"],
"[astro]": {
"editor.defaultFormatter": "astro-build.astro-vscode"
},
"css.customData": ["./vscode.tailwind.json"],
"eslint.validate": ["javascript", "javascriptreact", "astro", "typescript", "typescriptreact"],
"files.associations": {
"*.mdx": "markdown"
},
"editor.quickSuggestions": {
"strings": "on"
},
"tailwindCSS.includeLanguages": {
"astro": "html"
},
"typescript.tsdk": "node_modules/typescript/lib",
"yaml.schemas": {
"./.vscode/astrowind/config-schema.json": "/src/config.yaml"
},
"eslint.experimental.useFlatConfig": true
}

21
front/LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 onWidget
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,54 +1,295 @@
# Astro Starter Kit: Basics
# 🚀 AstroWind
<img src="https://raw.githubusercontent.com/onwidget/.github/main/resources/astrowind/lighthouse-score.png" align="right"
alt="AstroWind Lighthouse Score" width="100" height="358">
🌟 _Most *starred* & *forked* Astro theme in 2022 & 2023_. 🌟
**AstroWind** is a free and open-source template to make your website using **[Astro 4.0](https://astro.build/) + [Tailwind CSS](https://tailwindcss.com/)**. Ready to start a new project and designed taking into account web best practices.
- ✅ **Production-ready** scores in **PageSpeed Insights** reports.
- ✅ Integration with **Tailwind CSS** supporting **Dark mode** and **_RTL_**.
- ✅ **Fast and SEO friendly blog** with automatic **RSS feed**, **MDX** support, **Categories & Tags**, **Social Share**, ...
- ✅ **Image Optimization** (using new **Astro Assets** and **Unpic** for Universal image CDN).
- ✅ Generation of **project sitemap** based on your routes.
- ✅ **Open Graph tags** for social media sharing.
- ✅ **Analytics** built-in Google Analytics, and Splitbee integration.
<br>
<img src="https://raw.githubusercontent.com/onwidget/.github/main/resources/astrowind/screenshot-astrowind-1.png" alt="AstroWind Theme Screenshot">
[![onWidget](https://custom-icon-badges.demolab.com/badge/made%20by%20-onWidget-556bf2?style=flat-square&logo=onwidget&logoColor=white&labelColor=101827)](https://onwidget.com)
[![License](https://img.shields.io/github/license/onwidget/astrowind?style=flat-square&color=dddddd&labelColor=000000)](https://github.com/onwidget/astrowind/blob/main/LICENSE.md)
[![Maintained](https://img.shields.io/badge/maintained%3F-yes-brightgreen.svg?style=flat-square)](https://github.com/onwidget)
[![Contributions Welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/onwidget/astrowind#contributing)
[![Known Vulnerabilities](https://snyk.io/test/github/onwidget/astrowind/badge.svg?style=flat-square)](https://snyk.io/test/github/onwidget/astrowind)
[![Stars](https://img.shields.io/github/stars/onwidget/astrowind.svg?style=social&label=stars&maxAge=86400&color=ff69b4)](https://github.com/onwidget/astrowind)
[![Forks](https://img.shields.io/github/forks/onwidget/astrowind.svg?style=social&label=forks&maxAge=86400&color=ff69b4)](https://github.com/onwidget/astrowind)
<br>
<details open>
<summary>Table of Contents</summary>
- [Demo](#demo)
- [Upcoming: AstroWind 2.0 We Need Your Vision!](#-upcoming-astrowind-20--we-need-your-vision)
- [Getting started](#getting-started)
- [Project structure](#project-structure)
- [Commands](#commands)
- [Configuration](#configuration)
- [Deploy](#deploy)
- [Frequently Asked Questions](#frequently-asked-questions)
- [Related Projects](#related-projects)
- [Contributing](#contributing)
- [Acknowledgements](#acknowledgements)
- [License](#license)
</details>
<br>
## Demo
📌 [https://astrowind.vercel.app/](https://astrowind.vercel.app/)
<br>
## 🔔 Upcoming: AstroWind 2.0 We Need Your Vision!
We're embarking on an exciting journey with **AstroWind 2.0**, and we want you to be a part of it! We're currently taking the first steps in developing this new version and your insights are invaluable. Join the discussion and share your feedback, ideas, and suggestions to help shape the future of **AstroWind**. Let's make **AstroWind 2.0** even better, together!
[Share Your Feedback in Our Discussion!](https://github.com/onwidget/astrowind/discussions/392)
<br>
## Getting started
**AstroWind** tries to give you quick access to creating a website using [Astro 4.0](https://astro.build/) + [Tailwind CSS](https://tailwindcss.com/). It's a free theme which focuses on simplicity, good practices and high performance.
Very little vanilla javascript is used only to provide basic functionality so that each developer decides which framework (React, Vue, Svelte, Solid JS...) to use and how to approach their goals.
In this version the template supports all the options in the `output` configuration, `static`, `hybrid` and `server`, but the blog only works with `prerender = true`. We are working on the next version and aim to make it fully compatible with SSR.
### Project structure
Inside **AstroWind** template, you'll see the following folders and files:
```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
│ ├── _headers
│ └── robots.txt
├── src/
│ ├── assets/
│ │ ├── favicons/
│ │ ├── images/
│ │ └── styles/
│ │ └── tailwind.css
│ ├── components/
│ │ └── Card.astro
│ │ ├── blog/
│ │ ├── common/
│ │ ├── ui/
│ │ ├── widgets/
│ │ │ ├── Header.astro
│ │ │ └── ...
│ │ ├── CustomStyles.astro
│ │ ├── Favicons.astro
│ │ └── Logo.astro
│ ├── content/
│ │ ├── post/
│ │ │ ├── post-slug-1.md
│ │ │ ├── post-slug-2.mdx
│ │ │ └── ...
│ │ └-- config.ts
│ ├── layouts/
│ │ └── Layout.astro
│ └── pages/
│ └── index.astro
└── package.json
│ │ ├── Layout.astro
│ │ ├── MarkdownLayout.astro
│ │ └── PageLayout.astro
│ ├── pages/
│ │ ├── [...blog]/
│ │ │ ├── [category]/
│ │ │ ├── [tag]/
│ │ │ ├── [...page].astro
│ │ │ └── index.astro
│ │ ├── index.astro
│ │ ├── 404.astro
│ │ ├-- rss.xml.ts
│ │ └── ...
│ ├── utils/
│ ├── config.yaml
│ └── navigation.js
├── package.json
├── astro.config.mjs
└── ...
```
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.
Any static assets, like images, can be placed in the `public/` directory if they do not require any transformation or in the `assets/` directory if they are imported directly.
## 🧞 Commands
[![Edit AstroWind on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://githubbox.com/onwidget/astrowind/tree/main) [![Open in Gitpod](https://svgshare.com/i/xdi.svg)](https://gitpod.io/?on=gitpod#https://github.com/onwidget/astrowind) [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/onwidget/astrowind)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file `README.md`. Update `src/config.yaml` and contents. Have fun!
<br>
### 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 dev` | Starts local dev server at `localhost:3000` |
| `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 |
| `npm run format` | Format codes with Prettier |
| `npm run lint:eslint` | Run Eslint |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro preview` |
## 👀 Want to learn more?
<br>
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
### Configuration
Basic configuration file: `./src/config.yaml`
```yaml
site:
name: 'Example'
site: 'https://example.com'
base: '/' # Change this if you need to deploy to Github Pages, for example
trailingSlash: false # Generate permalinks with or without "/" at the end
googleSiteVerificationId: false # Or some value,
# Default SEO metadata
metadata:
title:
default: 'Example'
template: '%s — Example'
description: 'This is the default meta description of Example website'
robots:
index: true
follow: true
openGraph:
site_name: 'Example'
images:
- url: '~/assets/images/default.png'
width: 1200
height: 628
type: website
twitter:
handle: '@twitter_user'
site: '@twitter_user'
cardType: summary_large_image
i18n:
language: en
textDirection: ltr
apps:
blog:
isEnabled: true # If the blog will be enabled
postsPerPage: 6 # Number of posts per page
post:
isEnabled: true
permalink: '/blog/%slug%' # Variables: %slug%, %year%, %month%, %day%, %hour%, %minute%, %second%, %category%
robots:
index: true
list:
isEnabled: true
pathname: 'blog' # Blog main path, you can change this to "articles" (/articles)
robots:
index: true
category:
isEnabled: true
pathname: 'category' # Category main path /category/some-category, you can change this to "group" (/group/some-category)
robots:
index: true
tag:
isEnabled: true
pathname: 'tag' # Tag main path /tag/some-tag, you can change this to "topics" (/topics/some-category)
robots:
index: false
isRelatedPostsEnabled: true # If a widget with related posts is to be displayed below each post
relatedPostsCount: 4 # Number of related posts to display
analytics:
vendors:
googleAnalytics:
id: null # or "G-XXXXXXXXXX"
ui:
theme: 'system' # Values: "system" | "light" | "dark" | "light:only" | "dark:only"
```
<br>
#### Customize Design
To customize Font families, Colors or more Elements refer to the following files:
- `src/components/CustomStyles.astro`
- `src/assets/styles/tailwind.css`
### Deploy
#### Deploy to production (manual)
You can create an optimized production build with:
```shell
npm run build
```
Now, your website is ready to be deployed. All generated files are located at
`dist` folder, which you can deploy the folder to any hosting service you
prefer.
#### Deploy to Netlify
Clone this repository on own GitHub account and deploy to Netlify:
[![Netlify Deploy button](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/onwidget/astrowind)
#### Deploy to Vercel
Clone this repository on own GitHub account and deploy to Vercel:
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fonwidget%2Fastrowind)
<br>
## Frequently Asked Questions
- Why?
-
-
<br>
## Related projects
- [TailNext](https://tailnext.vercel.app/) - Free template using Next.js 14 and Tailwind CSS with the new App Router.
- [Qwind](https://qwind.pages.dev/) - Free template to make your website using Qwik + Tailwind CSS.
## Contributing
If you have any idea, suggestions or find any bugs, feel free to open a discussion, an issue or create a pull request.
That would be very useful for all of us and we would be happy to listen and take action.
## Acknowledgements
Initially created by [onWidget](https://onwidget.com) and maintained by a community of [contributors](https://github.com/onwidget/astrowind/graphs/contributors).
## License
**AstroWind** is licensed under the MIT license — see the [LICENSE](./LICENSE.md) file for details.

View File

@ -1,12 +1,33 @@
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
import tailwind from "@astrojs/tailwind";
import path from 'path';
import { fileURLToPath } from 'url';
import node from '@astrojs/node'
import tailwind from '@astrojs/tailwind'
import { defineConfig, squooshImageService } from 'astro/config';
import react from "@astrojs/react";
import sitemap from '@astrojs/sitemap';
import tailwind from '@astrojs/tailwind';
import mdx from '@astrojs/mdx';
import partytown from '@astrojs/partytown';
import icon from 'astro-icon';
import compress from '@playform/compress';
import astrowind from './vendor/integration';
import {
readingTimeRemarkPlugin,
responsiveTablesRehypePlugin,
lazyImagesRehypePlugin,
} from './src/utils/frontmatter.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const hasExternalScripts = false;
const whenExternalScripts = (items = []) =>
hasExternalScripts ? (Array.isArray(items) ? items.map((item) => item()) : [items()]) : [];
// https://astro.build/config
export default defineConfig({
// integrations: [tailwind(), test, routing(), version(), buildInfos()],
output: 'server',
compressHTML: true,
build: {
assets: 'assets',
@ -17,14 +38,61 @@ export default defineConfig({
port: 3000
},
trailingSlash: 'never',
output: 'server',
adapter: node({
mode: 'standalone'
}),
integrations: [tailwind(), react()],
integrations: [
tailwind({
applyBaseStyles: false,
}),
sitemap(),
mdx(),
icon({
include: {
tabler: ['*'],
'flat-color-icons': [
'template',
'gallery',
'approval',
'document',
'advertising',
'currency-exchange',
'voice-presentation',
'business-contact',
'database',
],
},
}),
...whenExternalScripts(() =>
partytown({
config: { forward: ['dataLayer.push'] },
})
),
astrowind({
config: './src/config.yaml',
}),
],
image: {
service: squooshImageService(),
domains: ['cdn.pixabay.com'],
},
markdown: {
remarkPlugins: [readingTimeRemarkPlugin],
rehypePlugins: [responsiveTablesRehypePlugin, lazyImagesRehypePlugin],
},
vite: {
optimizeDeps: {
include: ['leaflet']
}
}
},
resolve: {
alias: {
'~': path.resolve(__dirname, './src'),
},
},
},
});

59
front/eslint.config.js Normal file
View File

@ -0,0 +1,59 @@
import astroEslintParser from 'astro-eslint-parser';
import eslintPluginAstro from 'eslint-plugin-astro';
import globals from 'globals';
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import typescriptParser from '@typescript-eslint/parser';
export default [
js.configs.recommended,
...eslintPluginAstro.configs['flat/recommended'],
...tseslint.configs.recommended,
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
{
files: ['**/*.astro'],
languageOptions: {
parser: astroEslintParser,
parserOptions: {
parser: '@typescript-eslint/parser',
extraFileExtensions: ['.astro'],
},
},
},
{
files: ['**/*.{js,jsx,astro}'],
rules: {
'no-mixed-spaces-and-tabs': ['error', 'smart-tabs'],
},
},
{
// Define the configuration for `<script>` tag.
// Script in `<script>` is assigned a virtual file name with the `.js` extension.
files: ['**/*.{ts,tsx}', '**/*.astro/*.js'],
languageOptions: {
parser: typescriptParser,
},
rules: {
// Note: you must disable the base rule as it can report incorrect errors
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
},
],
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
{
ignores: ['dist', 'node_modules', '.github', 'types.generated.d.ts', '.astro'],
},
];

9
front/netlify.toml Normal file
View File

@ -0,0 +1,9 @@
[build]
publish = "dist"
command = "npm run build"
[build.processing.html]
pretty_urls = false
[[headers]]
for = "/_astro/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"

8377
front/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,11 @@
{
"name": "front",
"version": "1.0.0-beta.34",
"type": "module",
"version": "0.0.1",
"private": "true",
"private": true,
"engines": {
"node": "^18.17.1 || ^20.3.0 || >= 21.0.0"
},
"scripts": {
"dev": "astro dev",
"start": "node ./dist/server/entry.mjs",
@ -13,32 +16,64 @@
},
"dependencies": {
"@astrojs/node": "^8.2.5",
"@astrojs/react": "^3.3.1",
"@astrojs/rss": "^4.0.5",
"@astrojs/sitemap": "^3.1.4",
"@astrojs/tailwind": "^5.1.0",
"@astrolib/analytics": "^0.5.0",
"@astrolib/seo": "^1.0.0-beta.5",
"@fontsource-variable/inter": "^5.0.18",
"@tailwindcss/typography": "^0.5.12",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
"astro": "4.5.12",
"astro": "^4.8.3",
"astro-embed": "^0.7.2",
"astro-icon": "^1.1.0",
"leaflet": "^1.9.4",
"leaflet-control-geocoder": "^2.4.0",
"leaflet-geosearch": "^4.0.0",
"leaflet-routing-machine": "^3.2.12",
"limax": "4.1.0",
"lodash.merge": "^4.6.2",
"lucide-astro": "^0.372.0",
"pocketbase": "^0.21.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-leaflet": "^4.2.1",
"simple-icons-astro": "^11.12.0",
"tailwind-merge": "^2.3.0",
"tailwindcss": "^3.4.3",
"typescript": "^5.2.2"
"unpic": "^3.18.0"
},
"devDependencies": {
"@astrojs/check": "^0",
"@astrojs/mdx": "^3.0.0",
"@astrojs/partytown": "^2.1.0",
"@astrojs/tailwind": "5.1.0",
"@eslint/js": "^9.2.0",
"@iconify-json/flat-color-icons": "^1.1.10",
"@iconify-json/tabler": "^1.1.111",
"@playform/compress": "0.0.4",
"@tailwindcss/typography": "^0.5.13",
"@types/eslint__js": "^8.42.3",
"@types/js-yaml": "^4.0.9",
"@types/leaflet": "^1.9.12",
"@types/leaflet-routing-machine": "^3.2.8",
"@types/lodash.merge": "^4.6.9",
"@types/node": "^20",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitest/coverage-v8": "^1",
"astro-eslint-parser": "^1.0.2",
"eslint": "^8.57.0",
"eslint-plugin-astro": "^0.31.4",
"eslint-plugin-jsx-a11y": "^6.8.0",
"globals": "^15.2.0",
"js-yaml": "^4.1.0",
"mdast-util-to-string": "^4.0.0",
"prettier": "^3.2.5",
"prettier-plugin-astro": "^0.13.0",
"reading-time": "^1.5.0",
"rehype-plugin-image-native-lazy-loading": "^1.2.0",
"sharp": "0.33.3",
"tailwindcss": "^3.4.3",
"typescript": "^5",
"typescript-eslint": "^7.9.0",
"vitest": "^1"
}
}

2
front/public/_headers Normal file
View File

@ -0,0 +1,2 @@
/_astro/*
Cache-Control: public, max-age=31536000, immutable

View File

@ -0,0 +1,29 @@
backend:
name: git-gateway
branch: main
media_folder: 'src/assets/images'
public_folder: '/_astro'
collections:
- name: 'post'
label: 'Post'
folder: 'src/content/post'
create: true
fields:
- { label: 'Title', name: 'title', widget: 'string' }
- { label: 'Excerpt', name: 'excerpt', widget: 'string' }
- { label: 'Category', name: 'category', widget: 'string' }
- {
label: 'Tags',
name: 'tags',
widget: 'list',
allow_add: true,
allow_delete: true,
collapsed: false,
field: { label: 'Tag', name: 'tag', widget: 'string' },
}
- { label: 'Image', name: 'image', widget: 'string' }
- { label: 'Publish Date', name: 'publishDate', widget: 'datetime', required: false }
- { label: 'Author', name: 'author', widget: 'string' }
- { label: 'Content', name: 'body', widget: 'markdown' }

View File

@ -0,0 +1,14 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex" />
<title>Content Manager</title>
<script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
</head>
<body>
<!-- Include the script that builds the page and powers Decap CMS -->
<script src="https://unpkg.com/netlify-cms@^2.0.0/dist/netlify-cms.js"></script>
</body>
</html>

2
front/public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow:

11
front/sandbox.config.json Normal file
View File

@ -0,0 +1,11 @@
{
"infiniteLoopProtection": true,
"hardReloadOnChange": false,
"view": "browser",
"template": "node",
"container": {
"port": 3000,
"startScript": "start",
"node": "18"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB

View File

@ -0,0 +1,92 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.bg-page {
background-color: var(--aw-color-bg-page);
}
.bg-dark {
background-color: var(--aw-color-bg-page-dark);
}
.bg-light {
background-color: var(--aw-color-bg-page);
}
.text-page {
color: var(--aw-color-text-page);
}
.text-muted {
color: var(--aw-color-text-muted);
}
}
@layer components {
.btn {
@apply inline-flex items-center justify-center rounded-full border-gray-400 border bg-transparent font-medium text-center text-base text-page leading-snug transition py-3.5 px-6 md:px-8 ease-in duration-200 focus:ring-blue-500 focus:ring-offset-blue-200 focus:ring-2 focus:ring-offset-2 hover:bg-gray-100 hover:border-gray-600 dark:text-slate-300 dark:border-slate-500 dark:hover:bg-slate-800 dark:hover:border-slate-800 cursor-pointer;
}
.btn-primary {
@apply btn font-semibold bg-primary text-white border-primary hover:bg-secondary hover:border-secondary hover:text-white dark:text-white dark:bg-primary dark:border-primary dark:hover:border-secondary dark:hover:bg-secondary;
}
.btn-secondary {
@apply btn;
}
.btn-tertiary {
@apply btn border-none shadow-none text-muted hover:text-gray-900 dark:text-gray-400 dark:hover:text-white;
}
}
#header.scroll > div:first-child {
@apply bg-page md:bg-white/90 md:backdrop-blur-md;
box-shadow: 0 0.375rem 1.5rem 0 rgb(140 152 164 / 13%);
}
.dark #header.scroll > div:first-child,
#header.scroll.dark > div:first-child {
@apply bg-page md:bg-[#030621e6] border-b border-gray-500/20;
box-shadow: none;
}
/* #header.scroll > div:last-child {
@apply py-3;
} */
#header.expanded nav {
position: fixed;
top: 70px;
left: 0;
right: 0;
bottom: 70px !important;
padding: 0 5px;
}
.dropdown:focus .dropdown-menu,
.dropdown:focus-within .dropdown-menu,
.dropdown:hover .dropdown-menu {
display: block;
}
[astro-icon].icon-light > * {
stroke-width: 1.2;
}
[astro-icon].icon-bold > * {
stroke-width: 2.4;
}
[data-aw-toggle-menu] path {
@apply transition;
}
[data-aw-toggle-menu].expanded g > path:first-child {
@apply -rotate-45 translate-y-[15px] translate-x-[-3px];
}
[data-aw-toggle-menu].expanded g > path:last-child {
@apply rotate-45 translate-y-[-8px] translate-x-[14px];
}
/* To deprecated */
.dd *:first-child {
margin-top: 0;
}

View File

@ -0,0 +1,30 @@
---
import type { Input as Props } from 'types';
const {value, checked ,name, label, autocomplete, placeholder, divClass, inputClass} = Astro.props;
---
<>
{
name && (
<div class={"flex flex-row items-center " + divClass}>
<input
type="checkbox"
name={name}
id={name}
checked={checked}
value={value}
autocomplete={autocomplete}
placeholder={placeholder}
class={"mr-2 size-5 " + inputClass}
/>
{label && (
<label for={name} class="block text-lg font-medium">
{label}
</label>
)}
</div>
)
}
</>

View File

@ -0,0 +1,63 @@
---
import '@fontsource-variable/inter';
// 'DM Sans'
// Nunito
// Dosis
// Outfit
// Roboto
// Literata
// 'IBM Plex Sans'
// Karla
// Poppins
// 'Fira Sans'
// 'Libre Franklin'
// Inconsolata
// Raleway
// Oswald
// 'Space Grotesk'
// Urbanist
---
<style is:inline>
:root {
--aw-font-sans: 'InterVariable';
--aw-font-serif: 'InterVariable';
--aw-font-heading: 'InterVariable';
--aw-color-primary: rgb(1 97 239);
--aw-color-secondary: rgb(1 84 207);
--aw-color-accent: rgb(109 40 217);
--aw-color-text-heading: rgb(0 0 0);
--aw-color-text-default: rgb(16 16 16);
--aw-color-text-muted: rgb(16 16 16 / 66%);
--aw-color-bg-page: rgb(255 255 255);
--aw-color-bg-page-dark: rgb(3 6 32);
::selection {
background-color: lavender;
}
}
.dark {
--aw-font-sans: 'InterVariable';
--aw-font-serif: 'InterVariable';
--aw-font-heading: 'InterVariable';
--aw-color-primary: rgb(1 97 239);
--aw-color-secondary: rgb(1 84 207);
--aw-color-accent: rgb(109 40 217);
--aw-color-text-heading: rgb(247, 248, 248);
--aw-color-text-default: rgb(229 236 246);
--aw-color-text-muted: rgb(229 236 246 / 66%);
--aw-color-bg-page: rgb(3 6 32);
::selection {
background-color: black;
color: snow;
}
}
</style>

View File

@ -0,0 +1,10 @@
---
import favIcon from 'assets/favicons/favicon.ico';
import favIconSvg from 'assets/favicons/favicon.svg';
import appleTouchIcon from 'assets/favicons/apple-touch-icon.png';
---
<link rel="shortcut icon" href={favIcon} />
<link rel="icon" type="image/svg+xml" href={favIconSvg.src} />
<link rel="mask-icon" href={favIconSvg.src} color="#8D46E7" />
<link rel="apple-touch-icon" sizes="180x180" href={appleTouchIcon.src} />

View File

@ -0,0 +1,28 @@
---
import type { Input as Props } from 'types';
const { type, name, label, autocomplete, placeholder, divClass, inputClass} = Astro.props;
---
<>
{
name && (
<div class={divClass}>
{label && (
<label for={name} class="block text-sm font-medium">
{label}
</label>
)}
<input
type={type}
name={name}
id={name}
autocomplete={autocomplete}
placeholder={placeholder}
class={"py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900" + inputClass}
/>
</div>
)
}
</>

View File

@ -0,0 +1,9 @@
---
import { SITE } from 'astrowind:config';
---
<span
class="self-center ml-2 rtl:ml-0 rtl:mr-2 text-2xl md:text-xl font-bold text-gray-900 whitespace-nowrap dark:text-white"
>
🚀 {SITE?.name}
</span>

View File

@ -0,0 +1,53 @@
---
import ContactUs from 'components/widgets/Contact.astro';
import CallToAction from 'components/widgets/CallToAction.astro';
const pb = Astro.locals.pb
const oauths = (await pb.collection('users').listAuthMethods()).authProviders;
const discordProvider = oauths.find(item => item.name === 'discord');
const googleProvider = oauths.find(item => item.name === 'google');
---
<>
<CallToAction
actions={[
{
variant: 'primary',
text: 'Discord',
href: discordProvider!.authUrl + Astro.url.protocol + "//" + Astro.url.host + '/account/oauth',
icon: 'tabler:brand-discord',
class: "oauth-btn",
"data-cookie": encodeURIComponent(JSON.stringify(discordProvider))
},
{
variant: 'primary',
text: 'Google',
href: googleProvider!.authUrl + Astro.url.protocol + "//" + Astro.url.host + '/account/oauth',
icon: 'tabler:brand-google',
class: "oauth-btn",
"data-cookie": encodeURIComponent(JSON.stringify(googleProvider))
}
]}
>
<Fragment slot="title">
Oauth
</Fragment>
<Fragment slot="subtitle">
Connecter Vous aussi avec
</Fragment>
</CallToAction>
</>
<script>
// import { date } from "astro/zod";
document.cookie = "provider" + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
// console.log(date.toString)
const btn = document.querySelectorAll('.oauth-btn')
btn.forEach((item: Element) =>(item.addEventListener('click', (ev) =>{
document.cookie = "provider" + "=" + item.getAttribute('data-cookie') + "; path=/;"// expires=" + ;
})))
</script>

View File

@ -0,0 +1,19 @@
---
export interface Props {
values: Array<{label:String, name:string, checked?:boolean | undefined}>
}
const fields = Astro.props.values
---
<>
<div class="flex, flex-col">
{fields.map(value => (
<input type="radio" id={value.name} name="note-min" value={value.name} checked={value.checked}>
<label for={value.name} class="ml-1">{value.label}</label><br>
))}
</div>
</>

View File

@ -0,0 +1,14 @@
---
import Item from 'components/blog/GridItem.astro';
import type { Post } from 'types';
export interface Props {
posts: Array<Post>;
}
const { posts } = Astro.props;
---
<div class="grid gap-6 row-gap-5 md:grid-cols-2 lg:grid-cols-4 -mb-6">
{posts.map((post) => <Item post={post} />)}
</div>

View File

@ -0,0 +1,69 @@
---
import { APP_BLOG } from 'astrowind:config';
import type { Post } from 'types';
import Image from 'components/common/Image.astro';
import { findImage } from 'utils/images';
import { getPermalink } from 'utils/permalinks';
export interface Props {
post: Post;
}
const { post } = Astro.props;
const image = await findImage(post.image);
const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : '';
---
<article class="mb-6 transition">
<div class="relative md:h-64 bg-gray-400 dark:bg-slate-700 rounded shadow-lg mb-6">
{
image &&
(link ? (
<a href={link}>
<Image
src={image}
class="w-full md:h-full rounded shadow-lg bg-gray-400 dark:bg-slate-700"
widths={[400, 900]}
width={400}
sizes="(max-width: 900px) 400px, 900px"
alt={post.title}
aspectRatio="16:9"
layout="cover"
loading="lazy"
decoding="async"
/>
</a>
) : (
<Image
src={image}
class="w-full md:h-full rounded shadow-lg bg-gray-400 dark:bg-slate-700"
widths={[400, 900]}
width={400}
sizes="(max-width: 900px) 400px, 900px"
alt={post.title}
aspectRatio="16:9"
layout="cover"
loading="lazy"
decoding="async"
/>
))
}
</div>
<h3 class="text-xl sm:text-2xl font-bold leading-tight mb-2 font-heading dark:text-slate-300">
{
link ? (
<a class="inline-block hover:text-primary dark:hover:text-blue-700 transition ease-in duration-200" href={link}>
{post.title}
</a>
) : (
post.title
)
}
</h3>
<p class="text-muted dark:text-slate-400 text-lg">{post.excerpt}</p>
</article>

View File

@ -0,0 +1,12 @@
---
const { title = await Astro.slots.render('default'), subtitle = await Astro.slots.render('subtitle') } = Astro.props;
---
<header class="mb-8 md:mb-16 text-center max-w-3xl mx-auto">
<h1 class="text-4xl md:text-5xl font-bold leading-tighter tracking-tighter font-heading" set:html={title} />
{
subtitle && (
<div class="mt-2 md:mt-3 mx-auto text-xl text-gray-500 dark:text-slate-400 font-medium" set:html={subtitle} />
)
}
</header>

View File

@ -0,0 +1,20 @@
---
import Item from 'components/blog/ListItem.astro';
import type { Post } from 'types';
export interface Props {
posts: Array<Post>;
}
const { posts } = Astro.props;
---
<ul>
{
posts.map((post) => (
<li class="mb-12 md:mb-20">
<Item post={post} />
</li>
))
}
</ul>

View File

@ -0,0 +1,118 @@
---
import type { ImageMetadata } from 'astro';
import { Icon } from 'astro-icon/components';
import Image from 'components/common/Image.astro';
import PostTags from 'components/blog/Tags.astro';
import { APP_BLOG } from 'astrowind:config';
import type { Post } from 'types';
import { getPermalink } from 'utils/permalinks';
import { findImage } from 'utils/images';
import { getFormattedDate } from 'utils/utils';
export interface Props {
post: Post;
}
const { post } = Astro.props;
const image = (await findImage(post.image)) as ImageMetadata | undefined;
const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : '';
---
<article class={`max-w-md mx-auto md:max-w-none grid gap-6 md:gap-8 ${image ? 'md:grid-cols-2' : ''}`}>
{
image &&
(link ? (
<a class="relative block group" href={link ?? 'javascript:void(0)'}>
<div class="relative h-0 pb-[56.25%] md:pb-[75%] md:h-72 lg:pb-[56.25%] overflow-hidden bg-gray-400 dark:bg-slate-700 rounded shadow-lg">
{image && (
<Image
src={image}
class="absolute inset-0 object-cover w-full h-full mb-6 rounded shadow-lg bg-gray-400 dark:bg-slate-700"
widths={[400, 900]}
width={900}
sizes="(max-width: 900px) 400px, 900px"
alt={post.title}
aspectRatio="16:9"
loading="lazy"
decoding="async"
/>
)}
</div>
</a>
) : (
<div class="relative h-0 pb-[56.25%] md:pb-[75%] md:h-72 lg:pb-[56.25%] overflow-hidden bg-gray-400 dark:bg-slate-700 rounded shadow-lg">
{image && (
<Image
src={image}
class="absolute inset-0 object-cover w-full h-full mb-6 rounded shadow-lg bg-gray-400 dark:bg-slate-700"
widths={[400, 900]}
width={900}
sizes="(max-width: 900px) 400px, 900px"
alt={post.title}
aspectRatio="16:9"
loading="lazy"
decoding="async"
/>
)}
</div>
))
}
<div class="mt-2">
<header>
<div class="mb-1">
<span class="text-sm">
<Icon name="tabler:clock" class="w-3.5 h-3.5 inline-block -mt-0.5 dark:text-gray-400" />
<time datetime={String(post.publishDate)} class="inline-block">{getFormattedDate(post.publishDate)}</time>
{
post.author && (
<>
{' '}
· <Icon name="tabler:user" class="w-3.5 h-3.5 inline-block -mt-0.5 dark:text-gray-400" />
<span>{post.author.replaceAll('-', ' ')}</span>
</>
)
}
{
post.category && (
<>
{' '}
·{' '}
<a class="hover:underline" href={getPermalink(post.category.slug, 'category')}>
{post.category.title}
</a>
</>
)
}
</span>
</div>
<h2 class="text-xl sm:text-2xl font-bold leading-tight mb-2 font-heading dark:text-slate-300">
{
link ? (
<a
class="inline-block hover:text-primary dark:hover:text-blue-700 transition ease-in duration-200"
href={link}
>
{post.title}
</a>
) : (
post.title
)
}
</h2>
</header>
{post.excerpt && <p class="flex-grow text-muted dark:text-slate-400 text-lg">{post.excerpt}</p>}
{
post.tags && Array.isArray(post.tags) ? (
<footer class="mt-5">
<PostTags tags={post.tags} />
</footer>
) : (
<Fragment />
)
}
</div>
</article>

View File

@ -0,0 +1,36 @@
---
import { Icon } from 'astro-icon/components';
import { getPermalink } from 'utils/permalinks';
import Button from 'components/ui/Button.astro';
export interface Props {
prevUrl?: string;
nextUrl?: string;
prevText?: string;
nextText?: string;
}
const { prevUrl, nextUrl, prevText = 'Newer posts', nextText = 'Older posts' } = Astro.props;
---
{
(prevUrl || nextUrl) && (
<div class="container flex">
<div class="flex flex-row mx-auto container justify-between">
<Button
variant="tertiary"
class={`md:px-3 px-3 mr-2 ${!prevUrl ? 'invisible' : ''}`}
href={getPermalink(prevUrl)}
>
<Icon name="tabler:chevron-left" class="w-6 h-6" />
<p class="ml-2">{prevText}</p>
</Button>
<Button variant="tertiary" class={`md:px-3 px-3 ${!nextUrl ? 'invisible' : ''}`} href={getPermalink(nextUrl)}>
<span class="mr-2">{nextText}</span>
<Icon name="tabler:chevron-right" class="w-6 h-6" />
</Button>
</div>
</div>
)
}

View File

@ -0,0 +1,28 @@
---
import { APP_BLOG } from 'astrowind:config';
import { getRelatedPosts } from 'utils/blog';
import BlogHighlightedPosts from '../widgets/BlogHighlightedPosts.astro';
import type { Post } from 'types';
import { getBlogPermalink } from 'utils/permalinks';
export interface Props {
post: Post;
}
const { post } = Astro.props;
const relatedPosts = post.tags ? await getRelatedPosts(post, 4) : [];
---
{
APP_BLOG.isRelatedPostsEnabled ? (
<BlogHighlightedPosts
classes={{ container: 'pt-0 lg:pt-0 md:pt-0' }}
title="Related Posts"
linkText="View All Posts"
linkUrl={getBlogPermalink()}
postIds={relatedPosts.map((post) => post.id)}
/>
) : null
}

View File

@ -0,0 +1,99 @@
---
import { Icon } from 'astro-icon/components';
import Image from 'components/common/Image.astro';
import PostTags from 'components/blog/Tags.astro';
import SocialShare from 'components/common/SocialShare.astro';
import { getPermalink } from 'utils/permalinks';
import { getFormattedDate } from 'utils/utils';
import type { Post } from 'types';
export interface Props {
post: Post;
url: string | URL;
}
const { post, url } = Astro.props;
---
<section class="py-8 sm:py-16 lg:py-20 mx-auto">
<article>
<header class={post.image ? '' : ''}>
<div class="flex justify-between flex-col sm:flex-row max-w-3xl mx-auto mt-0 mb-2 px-4 sm:px-6 sm:items-center">
<p>
<Icon name="tabler:clock" class="w-4 h-4 inline-block -mt-0.5 dark:text-gray-400" />
<time datetime={String(post.publishDate)} class="inline-block">{getFormattedDate(post.publishDate)}</time>
{
post.author && (
<>
{' '}
· <Icon name="tabler:user" class="w-4 h-4 inline-block -mt-0.5 dark:text-gray-400" />
<span class="inline-block">{post.author}</span>
</>
)
}
{
post.category && (
<>
{' '}
·{' '}
<a class="hover:underline inline-block" href={getPermalink(post.category.slug, 'category')}>
{post.category.title}
</a>
</>
)
}
{
post.readingTime && (
<>
&nbsp;· <span>{post.readingTime}</span> min read
</>
)
}
</p>
</div>
<h1
class="px-4 sm:px-6 max-w-3xl mx-auto text-4xl md:text-5xl font-bold leading-tighter tracking-tighter font-heading"
>
{post.title}
</h1>
<p
class="max-w-3xl mx-auto mt-4 mb-8 px-4 sm:px-6 text-xl md:text-2xl text-muted dark:text-slate-400 text-justify"
>
{post.excerpt}
</p>
{
post.image ? (
<Image
src={post.image}
class="max-w-full lg:max-w-[900px] mx-auto mb-6 sm:rounded-md bg-gray-400 dark:bg-slate-700"
widths={[400, 900]}
sizes="(max-width: 900px) 400px, 900px"
alt={post?.excerpt || ''}
width={900}
height={506}
loading="eager"
decoding="async"
/>
) : (
<div class="max-w-3xl mx-auto px-4 sm:px-6">
<div class="border-t dark:border-slate-700" />
</div>
)
}
</header>
<div
class="mx-auto px-6 sm:px-6 max-w-3xl prose prose-md lg:prose-xl dark:prose-invert dark:prose-headings:text-slate-300 prose-headings:font-heading prose-headings:leading-tighter prose-headings:tracking-tighter prose-headings:font-bold prose-a:text-primary dark:prose-a:text-blue-400 prose-img:rounded-md prose-img:shadow-lg mt-8 prose-headings:scroll-mt-[80px] prose-li:my-0"
>
<slot />
</div>
<div class="mx-auto px-6 sm:px-6 max-w-3xl mt-8 flex justify-between flex-col sm:flex-row">
<PostTags tags={post.tags} class="mr-5 rtl:mr-0 rtl:ml-5" />
<SocialShare url={url} text={post.title} class="mt-5 sm:mt-1 align-middle text-gray-500 dark:text-slate-600" />
</div>
</article>
</section>

View File

@ -0,0 +1,45 @@
---
import { getPermalink } from 'utils/permalinks';
import { APP_BLOG } from 'astrowind:config';
import type { Post } from 'types';
export interface Props {
tags: Post['tags'];
class?: string;
title?: string | undefined;
isCategory?: boolean;
}
const { tags, class: className = 'text-sm', title = undefined, isCategory = false } = Astro.props;
---
{
tags && Array.isArray(tags) && (
<>
<>
{title !== undefined && (
<span class="align-super font-normal underline underline-offset-4 decoration-2 dark:text-slate-400">
{title}
</span>
)}
</>
<ul class={className}>
{tags.map((tag) => (
<li class="bg-gray-100 dark:bg-slate-700 inline-block mr-2 rtl:mr-0 rtl:ml-2 mb-2 py-0.5 px-2 lowercase font-medium">
{!APP_BLOG?.tag?.isEnabled ? (
tag.title
) : (
<a
href={getPermalink(tag.slug, isCategory ? 'category' : 'tag')}
class="text-muted dark:text-slate-300 hover:text-primary dark:hover:text-gray-200"
>
{tag.title}
</a>
)}
</li>
))}
</ul>
</>
)
}

View File

@ -0,0 +1,20 @@
---
import { Icon } from 'astro-icon/components';
import { getBlogPermalink } from 'utils/permalinks';
import { I18N } from 'astrowind:config';
import Button from 'components/ui/Button.astro';
const { textDirection } = I18N;
---
<div class="mx-auto px-6 sm:px-6 max-w-3xl pt-8 md:pt-4 pb-12 md:pb-20">
<Button variant="tertiary" class="px-3 md:px-3" href={getBlogPermalink()}>
{
textDirection === 'rtl' ? (
<Icon name="tabler:chevron-right" class="w-5 h-5 mr-1 -ml-1.5 rtl:-mr-1.5 rtl:ml-1" />
) : (
<Icon name="tabler:chevron-left" class="w-5 h-5 mr-1 -ml-1.5 rtl:-mr-1.5 rtl:ml-1" />
)
} Back to Blog
</Button>
</div>

View File

@ -0,0 +1,13 @@
---
import { GoogleAnalytics } from '@astrolib/analytics';
import { ANALYTICS } from 'astrowind:config';
---
{
ANALYTICS?.vendors?.googleAnalytics?.id ? (
<GoogleAnalytics
id={String(ANALYTICS.vendors.googleAnalytics.id)}
partytown={ANALYTICS?.vendors?.googleAnalytics?.partytown}
/>
) : null
}

View File

@ -0,0 +1,33 @@
---
import { UI } from 'astrowind:config';
// TODO: This code is temporary
---
<script is:inline define:vars={{ defaultTheme: UI.theme || 'system' }}>
function applyTheme(theme) {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
const matches = document.querySelectorAll('[data-aw-toggle-color-scheme] > input');
if (matches && matches.length) {
matches.forEach((elem) => {
elem.checked = theme !== 'dark';
});
}
}
if ((defaultTheme && defaultTheme.endsWith(':only')) || (!localStorage.theme && defaultTheme !== 'system')) {
applyTheme(defaultTheme.replace(':only', ''));
} else if (
localStorage.theme === 'dark' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
applyTheme('dark');
} else {
applyTheme('light');
}
</script>

View File

@ -0,0 +1,162 @@
---
import { UI } from 'astrowind:config';
---
<script is:inline define:vars={{ defaultTheme: UI.theme }}>
if (window.basic_script) {
return;
}
window.basic_script = true;
function applyTheme(theme) {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
const initTheme = function () {
if ((defaultTheme && defaultTheme.endsWith(':only')) || (!localStorage.theme && defaultTheme !== 'system')) {
applyTheme(defaultTheme.replace(':only', ''));
} else if (
localStorage.theme === 'dark' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
applyTheme('dark');
} else {
applyTheme('light');
}
};
initTheme();
function attachEvent(selector, event, fn) {
const matches = typeof selector === 'string' ? document.querySelectorAll(selector) : selector;
if (matches && matches.length) {
matches.forEach((elem) => {
elem.addEventListener(event, (e) => fn(e, elem), false);
});
}
}
const onLoad = function () {
let lastKnownScrollPosition = window.scrollY;
let ticking = true;
attachEvent('#header nav', 'click', function () {
document.querySelector('[data-aw-toggle-menu]')?.classList.remove('expanded');
document.body.classList.remove('overflow-hidden');
document.getElementById('header')?.classList.remove('h-screen');
document.getElementById('header')?.classList.remove('expanded');
document.getElementById('header')?.classList.remove('bg-page');
document.querySelector('#header nav')?.classList.add('hidden');
document.querySelector('#header > div > div:last-child')?.classList.add('hidden');
});
attachEvent('[data-aw-toggle-menu]', 'click', function (_, elem) {
elem.classList.toggle('expanded');
document.body.classList.toggle('overflow-hidden');
document.getElementById('header')?.classList.toggle('h-screen');
document.getElementById('header')?.classList.toggle('expanded');
document.getElementById('header')?.classList.toggle('bg-page');
document.querySelector('#header nav')?.classList.toggle('hidden');
document.querySelector('#header > div > div:last-child')?.classList.toggle('hidden');
});
attachEvent('[data-aw-toggle-color-scheme]', 'click', function () {
if (defaultTheme.endsWith(':only')) {
return;
}
document.documentElement.classList.toggle('dark');
localStorage.theme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
});
attachEvent('[data-aw-social-share]', 'click', function (_, elem) {
const network = elem.getAttribute('data-aw-social-share');
const url = encodeURIComponent(elem.getAttribute('data-aw-url'));
const text = encodeURIComponent(elem.getAttribute('data-aw-text'));
let href;
switch (network) {
case 'facebook':
href = `https://www.facebook.com/sharer.php?u=${url}`;
break;
case 'twitter':
href = `https://twitter.com/intent/tweet?url=${url}&text=${text}`;
break;
case 'linkedin':
href = `https://www.linkedin.com/shareArticle?mini=true&url=${url}&title=${text}`;
break;
case 'whatsapp':
href = `https://wa.me/?text=${text}%20${url}`;
break;
case 'mail':
href = `mailto:?subject=%22${text}%22&body=${text}%20${url}`;
break;
default:
return;
}
const newlink = document.createElement('a');
newlink.target = '_blank';
newlink.href = href;
newlink.click();
});
const screenSize = window.matchMedia('(max-width: 767px)');
screenSize.addEventListener('change', function () {
document.querySelector('[data-aw-toggle-menu]')?.classList.remove('expanded');
document.body.classList.remove('overflow-hidden');
document.getElementById('header')?.classList.remove('h-screen');
document.getElementById('header')?.classList.remove('expanded');
document.getElementById('header')?.classList.remove('bg-page');
document.querySelector('#header nav')?.classList.add('hidden');
document.querySelector('#header > div > div:last-child')?.classList.add('hidden');
});
function applyHeaderStylesOnScroll() {
const header = document.querySelector('#header[data-aw-sticky-header]');
if (!header) return;
if (lastKnownScrollPosition > 60 && !header.classList.contains('scroll')) {
header.classList.add('scroll');
} else if (lastKnownScrollPosition <= 60 && header.classList.contains('scroll')) {
header.classList.remove('scroll');
}
ticking = false;
}
applyHeaderStylesOnScroll();
attachEvent([document], 'scroll', function () {
lastKnownScrollPosition = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(() => {
applyHeaderStylesOnScroll();
});
ticking = true;
}
});
};
const onPageShow = function () {
document.documentElement.classList.add('motion-safe:scroll-smooth');
const elem = document.querySelector('[data-aw-toggle-menu]');
if (elem) {
elem.classList.remove('expanded');
}
document.body.classList.remove('overflow-hidden');
document.getElementById('header')?.classList.remove('h-screen');
document.getElementById('header')?.classList.remove('expanded');
document.querySelector('#header nav')?.classList.add('hidden');
};
window.onload = onLoad;
window.onpageshow = onPageShow;
document.addEventListener('astro:after-swap', () => {
initTheme();
onLoad();
onPageShow();
});
</script>

View File

@ -0,0 +1,8 @@
---
import { getAsset } from 'utils/permalinks';
---
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="sitemap" href={getAsset('/sitemap-index.xml')} />

View File

@ -0,0 +1,61 @@
---
import { findImage } from 'utils/images';
import {
getImagesOptimized,
astroAsseetsOptimizer,
unpicOptimizer,
isUnpicCompatible,
type ImageProps,
type AttributesProps,
} from 'utils/images-optimization';
type Props = ImageProps;
type ImageType = {
src: string;
attributes: AttributesProps;
};
const props = Astro.props;
if (props.alt === undefined || props.alt === null) {
throw new Error();
}
if (typeof props.width === 'string') {
props.width = parseInt(props.width);
}
if (typeof props.height === 'string') {
props.height = parseInt(props.height);
}
if (!props.loading) {
props.loading = 'lazy';
}
if (!props.decoding) {
props.decoding = 'async';
}
const _image = await findImage(props.src);
let image: ImageType | undefined = undefined;
if (
typeof _image === 'string' &&
(_image.startsWith('http://') || _image.startsWith('https://')) &&
isUnpicCompatible(_image)
) {
image = await getImagesOptimized(_image, props, unpicOptimizer);
} else if (_image) {
image = await getImagesOptimized(_image, props, astroAsseetsOptimizer);
}
---
{
!image ? (
<Fragment />
) : (
<img src={image.src} crossorigin="anonymous" referrerpolicy="no-referrer" {...image.attributes} />
)
}

View File

@ -0,0 +1,68 @@
---
import merge from 'lodash.merge';
import { AstroSeo } from '@astrolib/seo';
import type { Props as AstroSeoProps } from '@astrolib/seo';
import { SITE, METADATA, I18N } from 'astrowind:config';
import type { MetaData } from 'types';
import { getCanonical } from 'utils/permalinks';
import { adaptOpenGraphImages } from 'utils/images';
export interface Props extends MetaData {
dontUseTitleTemplate?: boolean;
}
const {
title,
ignoreTitleTemplate = false,
canonical = String(getCanonical(String(Astro.url.pathname))),
robots = {},
description,
openGraph = {},
twitter = {},
} = Astro.props;
const seoProps: AstroSeoProps = merge(
{
title: '',
titleTemplate: '%s',
canonical: canonical,
noindex: true,
nofollow: true,
description: undefined,
openGraph: {
url: canonical,
site_name: SITE?.name,
images: [],
locale: I18N?.language || 'en',
type: 'website',
},
twitter: {
cardType: openGraph?.images?.length ? 'summary_large_image' : 'summary',
},
},
{
title: METADATA?.title?.default,
titleTemplate: METADATA?.title?.template,
noindex: typeof METADATA?.robots?.index !== 'undefined' ? !METADATA.robots.index : undefined,
nofollow: typeof METADATA?.robots?.follow !== 'undefined' ? !METADATA.robots.follow : undefined,
description: METADATA?.description,
openGraph: METADATA?.openGraph,
twitter: METADATA?.twitter,
},
{
title: title,
titleTemplate: ignoreTitleTemplate ? '%s' : undefined,
canonical: canonical,
noindex: typeof robots?.index !== 'undefined' ? !robots.index : undefined,
nofollow: typeof robots?.follow !== 'undefined' ? !robots.follow : undefined,
description: description,
openGraph: { url: canonical, ...openGraph },
twitter: twitter,
}
);
---
<AstroSeo {...{ ...seoProps, openGraph: await adaptOpenGraphImages(seoProps?.openGraph, Astro.site) }} />

View File

@ -0,0 +1,5 @@
---
import { SITE } from 'astrowind:config';
---
{SITE.googleSiteVerificationId && <meta name="google-site-verification" content={SITE.googleSiteVerificationId} />}

View File

@ -0,0 +1,65 @@
---
import { Icon } from 'astro-icon/components';
export interface Props {
text: string;
url: string | URL;
class?: string;
}
const { text, url, class: className = 'inline-block' } = Astro.props;
---
<div class={className}>
<span class="align-super font-bold text-slate-500 dark:text-slate-400">Share:</span>
<button
class="ml-2 rtl:ml-0 rtl:mr-2"
title="Twitter Share"
data-aw-social-share="twitter"
data-aw-url={url}
data-aw-text={text}
><Icon
name="tabler:brand-x"
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
/>
</button>
<button class="ml-2 rtl:ml-0 rtl:mr-2" title="Facebook Share" data-aw-social-share="facebook" data-aw-url={url}
><Icon
name="tabler:brand-facebook"
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
/>
</button>
<button
class="ml-2 rtl:ml-0 rtl:mr-2"
title="Linkedin Share"
data-aw-social-share="linkedin"
data-aw-url={url}
data-aw-text={text}
><Icon
name="tabler:brand-linkedin"
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
/>
</button>
<button
class="ml-2 rtl:ml-0 rtl:mr-2"
title="Whatsapp Share"
data-aw-social-share="whatsapp"
data-aw-url={url}
data-aw-text={text}
><Icon
name="tabler:brand-whatsapp"
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
/>
</button>
<button
class="ml-2 rtl:ml-0 rtl:mr-2"
title="Email Share"
data-aw-social-share="mail"
data-aw-url={url}
data-aw-text={text}
><Icon
name="tabler:mail"
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
/>
</button>
</div>

View File

@ -0,0 +1,6 @@
---
const { doNotTrack = true, noCookieMode = false, url = 'https://cdn.splitbee.io/sb.js' } = Astro.props;
---
<!-- Splitbee Analytics -->
<script is:inline data-respect-dnt={doNotTrack} data-no-cookie={noCookieMode} async src={url}></script>

View File

@ -0,0 +1,29 @@
---
export interface Props {
label?: string;
class?: string;
}
const {
label = 'Toggle Menu',
class: className = 'flex flex-col h-12 w-12 rounded justify-center items-center cursor-pointer group',
} = Astro.props;
---
<button type="button" class={className} aria-label={label} data-aw-toggle-menu>
<span class="sr-only">{label}</span>
<slot>
<span
aria-hidden="true"
class="h-0.5 w-6 my-1 rounded-full bg-black dark:bg-white transition ease transform duration-200 opacity-80 group-[.expanded]:rotate-45 group-[.expanded]:translate-y-2.5"
></span>
<span
aria-hidden="true"
class="h-0.5 w-6 my-1 rounded-full bg-black dark:bg-white transition ease transform duration-200 opacity-80 group-[.expanded]:opacity-0"
></span>
<span
aria-hidden="true"
class="h-0.5 w-6 my-1 rounded-full bg-black dark:bg-white transition ease transform duration-200 opacity-80 group-[.expanded]:-rotate-45 group-[.expanded]:-translate-y-2.5"
></span>
</slot>
</button>

View File

@ -0,0 +1,28 @@
---
import { Icon } from 'astro-icon/components';
import { UI } from 'astrowind:config';
export interface Props {
label?: string;
class?: string;
iconClass?: string;
iconName?: string;
}
const {
label = 'Toggle between Dark and Light mode',
class:
className = 'text-muted dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5 inline-flex items-center',
iconClass = 'w-6 h-6',
iconName = 'tabler:sun',
} = Astro.props;
---
{
!(UI.theme && UI.theme.endsWith(':only')) && (
<button type="button" class={className} aria-label={label} data-aw-toggle-color-scheme>
<Icon name={iconName} class={iconClass} />
</button>
)
}

View File

@ -0,0 +1,11 @@
---
export interface Props {
isDark?: boolean;
}
const { isDark = false } = Astro.props;
---
<div class:list={['absolute inset-0', { 'bg-dark dark:bg-transparent': isDark }]}>
<slot />
</div>

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