Compare commits
3 Commits
6346077eea
...
v0.0.1
Author | SHA1 | Date | |
---|---|---|---|
3cb5a30270 | |||
2cb2166093 | |||
1593fa3493 |
@ -47,7 +47,8 @@ WORKDIR /home/node
|
|||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
# Add Healthcheck
|
# 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 from build image
|
||||||
COPY --chown=node:node --from=BUILD_IMAGE /home/node/node_modules ./node_modules
|
COPY --chown=node:node --from=BUILD_IMAGE /home/node/node_modules ./node_modules
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
testPathIgnorePatterns: [
|
||||||
|
"<rootDir>/dist/"
|
||||||
|
],
|
||||||
testTimeout: 10000,
|
testTimeout: 10000,
|
||||||
preset: 'ts-jest',
|
preset: 'ts-jest',
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
|
24
Express/barAndCafe/package-lock.json
generated
@ -8,6 +8,7 @@
|
|||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/node": "^20.12.7",
|
"@types/node": "^20.12.7",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
@ -15,6 +16,7 @@
|
|||||||
"swagger-ui-express": "^5.0.0"
|
"swagger-ui-express": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/rewire": "^2.5.30",
|
"@types/rewire": "^2.5.30",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
@ -1968,6 +1970,15 @@
|
|||||||
"integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
|
"integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/cors": {
|
||||||
|
"version": "2.8.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
|
||||||
|
"integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/express": {
|
"node_modules/@types/express": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
|
||||||
@ -4745,6 +4756,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/cors": {
|
||||||
|
"version": "2.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||||
|
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||||
|
"dependencies": {
|
||||||
|
"object-assign": "^4",
|
||||||
|
"vary": "^1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/create-jest": {
|
"node_modules/create-jest": {
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
|
||||||
@ -9248,7 +9271,6 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/node": "^20.12.7",
|
"@types/node": "^20.12.7",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
@ -21,6 +22,7 @@
|
|||||||
"swagger-ui-express": "^5.0.0"
|
"swagger-ui-express": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/rewire": "^2.5.30",
|
"@types/rewire": "^2.5.30",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { getCity, getRadius, getPoiId, getBox } from "./openTripMaps"
|
import { getCity, getRadius, getPoiId, getBox } from "./openTripMaps"
|
||||||
import express from "express"
|
import express from "express"
|
||||||
|
import cors from "cors"
|
||||||
/**
|
/**
|
||||||
* Initialize Express application instance.
|
* Initialize Express application instance.
|
||||||
* @returns An initialized Express application object.
|
* @returns An initialized Express application object.
|
||||||
*/
|
*/
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
|
app.use(cors())
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,7 +13,7 @@ const key = process.env.OPEN_TRIP_MAPS_KEY
|
|||||||
* @param {string} lat2 Latitude 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#))
|
* @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) {
|
async function callBox(lon1:string, lat1:string, lon2: string, lat2: string, rate: string) {
|
||||||
const lonMin = Math.min(parseFloat(lon1), parseFloat(lon2))
|
const lonMin = Math.min(parseFloat(lon1), parseFloat(lon2))
|
||||||
const lonMax = Math.max(parseFloat(lon1), parseFloat(lon2))
|
const lonMax = Math.max(parseFloat(lon1), parseFloat(lon2))
|
||||||
const latMin = Math.min(parseFloat(lat1), parseFloat(lat2))
|
const latMin = Math.min(parseFloat(lat1), parseFloat(lat2))
|
||||||
@ -27,6 +27,7 @@ async function callBox(lon1:string, lat1:string, lon2: string, lat2: string) {
|
|||||||
lon_max: lonMax,
|
lon_max: lonMax,
|
||||||
lat_min: latMin,
|
lat_min: latMin,
|
||||||
lat_max: latMax,
|
lat_max: latMax,
|
||||||
|
rate: rate,
|
||||||
apikey: key,
|
apikey: key,
|
||||||
kinds: 'bars,cafes,pubs,biergartens'
|
kinds: 'bars,cafes,pubs,biergartens'
|
||||||
},
|
},
|
||||||
@ -182,12 +183,16 @@ export async function getBox(req:express.Request, res: express.Response) {
|
|||||||
const lat1 = req.query["lat1"] as string
|
const lat1 = req.query["lat1"] as string
|
||||||
const lon2 = req.query["lon2"] as string
|
const lon2 = req.query["lon2"] as string
|
||||||
const lat2 = req.query["lat2"] as string
|
const lat2 = req.query["lat2"] as string
|
||||||
|
let rate = req.query["rate"] as string
|
||||||
if(!lon1 || !lat1 || !lon2 || !lat2){
|
if(!lon1 || !lat1 || !lon2 || !lat2){
|
||||||
res.status(400).send("Missing Argument")
|
res.status(400).send("Missing Argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send( await callBox(lon1, lat1, lon2, lat2))
|
if(!rate){
|
||||||
|
rate = "1";
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send( await callBox(lon1, lat1, lon2, lat2, rate))
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: fair une route ou l'on donne 2 coordonée
|
|
||||||
|
@ -21,7 +21,7 @@ const options = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
apis: ['./src/*.ts'],
|
apis: ['./src/*.ts','./dist/src/*.js'],
|
||||||
explorer: true
|
explorer: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,6 +219,14 @@ describe("Test the otm city path", () => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
function sleep(milliseconds: number) {
|
||||||
|
const date = Date.now();
|
||||||
|
let currentDate = null;
|
||||||
|
do {
|
||||||
|
currentDate = Date.now();
|
||||||
|
} while (currentDate - date < milliseconds);
|
||||||
|
}
|
||||||
|
sleep(2000);
|
||||||
request(app)
|
request(app)
|
||||||
.get("/otm/box")
|
.get("/otm/box")
|
||||||
.query({'lon1':'-1.435199','lon2':'-1.43519', 'lat1':'46.668460', 'lat2':'46.668461'})
|
.query({'lon1':'-1.435199','lon2':'-1.43519', 'lat1':'46.668460', 'lat2':'46.668461'})
|
||||||
|
18
Ratrapage_WEB.code-workspace
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"name": "Ratrapage_WEB",
|
||||||
|
"path": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "barAndCafe",
|
||||||
|
"path": "Express/barAndCafe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "front"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
vars {
|
vars {
|
||||||
TRIPMAP_URL: https://api.opentripmap.com/0.1
|
TRIPMAP_URL: https://api.opentripmap.com/0.1
|
||||||
|
EXPRESS_API: http://localhost:3001
|
||||||
}
|
}
|
||||||
vars:secret [
|
vars:secret [
|
||||||
app_key,
|
app_key,
|
||||||
|
8
bruno/OpenData datatourisme/environments/prod.bru
Normal 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
|
||||||
|
]
|
@ -5,7 +5,7 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
url: http://localhost:3001/otm/city?name=La roche sur yon&radius=300
|
url: {{EXPRESS_API}}/otm/city?name=La roche sur yon&radius=300
|
||||||
body: none
|
body: none
|
||||||
auth: none
|
auth: none
|
||||||
}
|
}
|
||||||
|
16
bruno/OpenData datatourisme/otm/otm_city.bru
Normal 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}}
|
||||||
|
}
|
18
bruno/OpenData datatourisme/poketBase_api/connexion.bru
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
27
bruno/OpenData datatourisme/poketBase_api/get all poi.bru
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
40
bruno/OpenData datatourisme/poketBase_api/send a poi.bru
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -18,7 +18,9 @@ services:
|
|||||||
- public
|
- public
|
||||||
|
|
||||||
front:
|
front:
|
||||||
image: git.lab-ouest.org/epitech/ratrapage_t-web_front:pr-1-head
|
image: git.lab-ouest.org/epitech/ratrapage_t-web_front:pr-16-head
|
||||||
|
environment:
|
||||||
|
- POCKETBASE_URL=https://${POCKET_BASE_URL}
|
||||||
depends_on:
|
depends_on:
|
||||||
- pocketbase
|
- pocketbase
|
||||||
labels:
|
labels:
|
||||||
@ -30,9 +32,10 @@ services:
|
|||||||
- public
|
- public
|
||||||
|
|
||||||
back_drink:
|
back_drink:
|
||||||
image: git.lab-ouest.org/epitech/ratrapage_t-web_back:pr-6-head
|
image: git.lab-ouest.org/epitech/ratrapage_t-web_back:master
|
||||||
environment:
|
environment:
|
||||||
- port=${BACK_BASE_PORT}
|
- port=${BACK_BASE_PORT}
|
||||||
|
- OPEN_TRIP_MAPS_KEY=${OPEN_TRIP_MAPS_KEY}
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.http.routers.expressDrinkTweb.rule=Host(`${DRINK_URL}`)
|
- traefik.http.routers.expressDrinkTweb.rule=Host(`${DRINK_URL}`)
|
||||||
@ -92,4 +95,4 @@ x-dockge:
|
|||||||
- https://${FRONT_URL}/
|
- https://${FRONT_URL}/
|
||||||
- https://${DOCS_DRINK_URL}/
|
- https://${DOCS_DRINK_URL}/
|
||||||
- https://${DRINK_URL}/
|
- https://${DRINK_URL}/
|
||||||
- https://${DATA_TOURISME_URL}/
|
- https://${DATA_TOURISME_URL}/
|
||||||
|
12
front/.editorconfig
Normal 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
|
10
front/.gitignore
vendored
@ -1,8 +1,6 @@
|
|||||||
# build output
|
# build output
|
||||||
dist/
|
dist/
|
||||||
|
.output/
|
||||||
# generated types
|
|
||||||
.astro/
|
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
@ -13,6 +11,7 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
# environment variables
|
# environment variables
|
||||||
.env
|
.env
|
||||||
.env.production
|
.env.production
|
||||||
@ -20,5 +19,6 @@ pnpm-debug.log*
|
|||||||
# macOS-specific files
|
# macOS-specific files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# jetbrains setting folder
|
pnpm-lock.yaml
|
||||||
.idea/
|
|
||||||
|
.astro
|
2
front/.npmrc
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Expose Astro dependencies for `pnpm` users
|
||||||
|
shamefully-hoist=true
|
4
front/.prettierignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
.github
|
||||||
|
.changeset
|
13
front/.prettierrc.cjs
Normal 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
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"startCommand": "npm start",
|
||||||
|
"env": {
|
||||||
|
"ENABLE_CJS_IMPORTS": true
|
||||||
|
}
|
||||||
|
}
|
275
front/.vscode/astrowind/config-schema.json
vendored
Normal 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"]
|
||||||
|
}
|
8
front/.vscode/extensions.json
vendored
@ -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": []
|
"unwantedRecommendations": []
|
||||||
}
|
}
|
||||||
|
15
front/.vscode/settings.json
vendored
@ -1,9 +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": {
|
"editor.quickSuggestions": {
|
||||||
"strings": "on"
|
"strings": "on"
|
||||||
},
|
},
|
||||||
"tailwindCSS.includeLanguages": {
|
"tailwindCSS.includeLanguages": {
|
||||||
"astro": "html"
|
"astro": "html"
|
||||||
},
|
},
|
||||||
"typescript.tsdk": "node_modules/typescript/lib"
|
"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
@ -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.
|
311
front/README.md
@ -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">
|
||||||
|
|
||||||
|
[](https://onwidget.com)
|
||||||
|
[](https://github.com/onwidget/astrowind/blob/main/LICENSE.md)
|
||||||
|
[](https://github.com/onwidget)
|
||||||
|
[](https://github.com/onwidget/astrowind#contributing)
|
||||||
|
[](https://snyk.io/test/github/onwidget/astrowind)
|
||||||
|
[](https://github.com/onwidget/astrowind)
|
||||||
|
[](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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
|
|
||||||
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
|
|
||||||
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
|
|
||||||
|
|
||||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 🚀 Project Structure
|
|
||||||
|
|
||||||
Inside of your Astro project, you'll see the following folders and files:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/
|
/
|
||||||
├── public/
|
├── public/
|
||||||
│ └── favicon.svg
|
│ ├── _headers
|
||||||
|
│ └── robots.txt
|
||||||
├── src/
|
├── src/
|
||||||
|
│ ├── assets/
|
||||||
|
│ │ ├── favicons/
|
||||||
|
│ │ ├── images/
|
||||||
|
│ │ └── styles/
|
||||||
|
│ │ └── tailwind.css
|
||||||
│ ├── components/
|
│ ├── 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/
|
│ ├── layouts/
|
||||||
│ │ └── Layout.astro
|
│ │ ├── Layout.astro
|
||||||
│ └── pages/
|
│ │ ├── MarkdownLayout.astro
|
||||||
│ └── index.astro
|
│ │ └── PageLayout.astro
|
||||||
└── package.json
|
│ ├── 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.
|
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.
|
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
|
[](https://githubbox.com/onwidget/astrowind/tree/main) [](https://gitpod.io/?on=gitpod#https://github.com/onwidget/astrowind) [](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:
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
| Command | Action |
|
| Command | Action |
|
||||||
| :------------------------ | :----------------------------------------------- |
|
| :-------------------- | :------------------------------------------------- |
|
||||||
| `npm install` | Installs dependencies |
|
| `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 build` | Build your production site to `./dist/` |
|
||||||
| `npm run preview` | Preview your build locally, before deploying |
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
| `npm run format` | Format codes with Prettier |
|
||||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
| `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:
|
||||||
|
|
||||||
|
[](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:
|
||||||
|
|
||||||
|
[](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.
|
||||||
|
@ -1,12 +1,34 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
import node from '@astrojs/node'
|
import node from '@astrojs/node'
|
||||||
import tailwind from '@astrojs/tailwind'
|
import tailwind from '@astrojs/tailwind'
|
||||||
import { defineConfig } from 'astro/config'
|
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({
|
export default defineConfig({
|
||||||
compressHTML: true,
|
|
||||||
|
output: 'server',
|
||||||
|
compressHTML: true,
|
||||||
build: {
|
build: {
|
||||||
assets: 'assets',
|
assets: 'assets',
|
||||||
inlineStylesheets: 'auto'
|
inlineStylesheets: 'auto'
|
||||||
@ -16,14 +38,61 @@ export default defineConfig({
|
|||||||
port: 3000
|
port: 3000
|
||||||
},
|
},
|
||||||
trailingSlash: 'never',
|
trailingSlash: 'never',
|
||||||
output: 'server',
|
|
||||||
adapter: node({
|
adapter: node({
|
||||||
mode: 'standalone'
|
mode: 'standalone'
|
||||||
}),
|
}),
|
||||||
integrations: [tailwind(), react()],
|
integrations: [
|
||||||
vite: {
|
tailwind({
|
||||||
optimizeDeps: {
|
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']
|
include: ['leaflet']
|
||||||
}
|
},
|
||||||
}
|
resolve: {
|
||||||
})
|
alias: {
|
||||||
|
'~': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
59
front/eslint.config.js
Normal 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
@ -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"
|
6672
front/package-lock.json
generated
@ -1,8 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "front",
|
"name": "front",
|
||||||
|
"version": "1.0.0-beta.34",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.0.1",
|
"private": true,
|
||||||
"private": "true",
|
"engines": {
|
||||||
|
"node": "^18.17.1 || ^20.3.0 || >= 21.0.0"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"start": "node ./dist/server/entry.mjs",
|
"start": "node ./dist/server/entry.mjs",
|
||||||
@ -13,33 +16,64 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/node": "^8.2.5",
|
"@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",
|
"@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",
|
"@tailwindcss/typography": "^0.5.12",
|
||||||
"@types/react": "^18.2.79",
|
"astro": "^4.8.3",
|
||||||
"@types/react-dom": "^18.2.25",
|
"astro-embed": "^0.7.2",
|
||||||
"astro": "4.5.12",
|
"astro-icon": "^1.1.0",
|
||||||
"leaflet": "^1.9.4",
|
"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",
|
"lucide-astro": "^0.372.0",
|
||||||
"pocketbase": "^0.21.1",
|
"pocketbase": "^0.21.1",
|
||||||
"react": "^18.2.0",
|
"react-leaflet": "^4.2.1",
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"simple-icons-astro": "^11.12.0",
|
"simple-icons-astro": "^11.12.0",
|
||||||
|
"tailwind-merge": "^2.3.0",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript": "^5"
|
"unpic": "^3.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/check": "^0",
|
"@astrojs/check": "^0",
|
||||||
"@astrojs/ts-plugin": "^1.6.1",
|
"@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": "^1.9.12",
|
||||||
|
"@types/leaflet-routing-machine": "^3.2.8",
|
||||||
|
"@types/lodash.merge": "^4.6.9",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"@vitest/coverage-v8": "^1",
|
"@vitest/coverage-v8": "^1",
|
||||||
|
"astro-eslint-parser": "^1.0.2",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-astro": "^0.31.4",
|
"eslint-plugin-astro": "^0.31.4",
|
||||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
"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": "^5",
|
||||||
|
"typescript-eslint": "^7.9.0",
|
||||||
"vitest": "^1"
|
"vitest": "^1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
front/public/_headers
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/_astro/*
|
||||||
|
Cache-Control: public, max-age=31536000, immutable
|
29
front/public/decapcms/config.yml
Normal 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' }
|
14
front/public/decapcms/index.html
Normal 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
@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow:
|
11
front/sandbox.config.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"infiniteLoopProtection": true,
|
||||||
|
"hardReloadOnChange": false,
|
||||||
|
"view": "browser",
|
||||||
|
"template": "node",
|
||||||
|
"container": {
|
||||||
|
"port": 3000,
|
||||||
|
"startScript": "start",
|
||||||
|
"node": "18"
|
||||||
|
}
|
||||||
|
}
|
BIN
front/src/assets/favicons/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
front/src/assets/favicons/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
9
front/src/assets/favicons/favicon.svg
Normal 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 |
BIN
front/src/assets/images/app-store.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
front/src/assets/images/default.png
Normal file
After Width: | Height: | Size: 563 KiB |
BIN
front/src/assets/images/google-play.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
front/src/assets/images/hero-image.png
Normal file
After Width: | Height: | Size: 539 KiB |
92
front/src/assets/styles/tailwind.css
Normal 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;
|
||||||
|
}
|
30
front/src/components/CheckBox.astro
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
63
front/src/components/CustomStyles.astro
Normal 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>
|
10
front/src/components/Favicons.astro
Normal 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} />
|
28
front/src/components/Input.astro
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
9
front/src/components/Logo.astro
Normal 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>
|
53
front/src/components/Oauth.astro
Normal 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>
|
19
front/src/components/Radios.astro
Normal 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>
|
||||||
|
</>
|
14
front/src/components/blog/Grid.astro
Normal 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>
|
69
front/src/components/blog/GridItem.astro
Normal 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>
|
12
front/src/components/blog/Headline.astro
Normal 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>
|
20
front/src/components/blog/List.astro
Normal 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>
|
118
front/src/components/blog/ListItem.astro
Normal 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>
|
36
front/src/components/blog/Pagination.astro
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
28
front/src/components/blog/RelatedPosts.astro
Normal 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
|
||||||
|
}
|
99
front/src/components/blog/SinglePost.astro
Normal 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 && (
|
||||||
|
<>
|
||||||
|
· <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>
|
45
front/src/components/blog/Tags.astro
Normal 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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
20
front/src/components/blog/ToBlogLink.astro
Normal 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>
|
13
front/src/components/common/Analytics.astro
Normal 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
|
||||||
|
}
|
33
front/src/components/common/ApplyColorMode.astro
Normal 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>
|
162
front/src/components/common/BasicScripts.astro
Normal 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>
|
8
front/src/components/common/CommonMeta.astro
Normal 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')} />
|
61
front/src/components/common/Image.astro
Normal 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} />
|
||||||
|
)
|
||||||
|
}
|
68
front/src/components/common/Metadata.astro
Normal 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) }} />
|
5
front/src/components/common/SiteVerification.astro
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
import { SITE } from 'astrowind:config';
|
||||||
|
---
|
||||||
|
|
||||||
|
{SITE.googleSiteVerificationId && <meta name="google-site-verification" content={SITE.googleSiteVerificationId} />}
|
65
front/src/components/common/SocialShare.astro
Normal 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>
|
6
front/src/components/common/SplitbeeAnalytics.astro
Normal 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>
|
29
front/src/components/common/ToggleMenu.astro
Normal 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>
|
28
front/src/components/common/ToggleTheme.astro
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
11
front/src/components/ui/Background.astro
Normal 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>
|
40
front/src/components/ui/Button.astro
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import type { CallToAction as Props } from 'types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
variant = 'secondary',
|
||||||
|
target,
|
||||||
|
text = Astro.slots.render('default'),
|
||||||
|
icon = '',
|
||||||
|
class: className = '',
|
||||||
|
type,
|
||||||
|
...rest
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const variants = {
|
||||||
|
primary: 'btn-primary',
|
||||||
|
secondary: 'btn-secondary',
|
||||||
|
tertiary: 'btn btn-tertiary',
|
||||||
|
link: 'cursor-pointer hover:text-primary',
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
type === 'button' || type === 'submit' || type === 'reset' ? (
|
||||||
|
<button type={type} class={twMerge(variants[variant] || '', className)} {...rest}>
|
||||||
|
<Fragment set:html={text} />
|
||||||
|
{icon && <Icon name={icon} class="w-5 h-5 ml-1 -mr-1.5 rtl:mr-1 rtl:-ml-1.5 inline-block" />}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
class={twMerge(variants[variant] || '', className)}
|
||||||
|
{...(target ? { target: target, rel: 'noopener noreferrer' } : {})}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<Fragment set:html={text} />
|
||||||
|
{icon && <Icon name={icon} class="w-5 h-5 ml-1 -mr-1.5 rtl:mr-1 rtl:-ml-1.5 inline-block" />}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
22
front/src/components/ui/DListItem.astro
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
// component: DListItem
|
||||||
|
//
|
||||||
|
// Mimics the html 'dl' (description list)
|
||||||
|
//
|
||||||
|
// The 'dt' item is the item 'term' and is inserted into an 'h6' tag.
|
||||||
|
// Caller needs to style the 'h6' tag appropriately.
|
||||||
|
//
|
||||||
|
// You can put pretty much any content you want between the open and
|
||||||
|
// closing tags - it's simply contained in an enclosing div with a
|
||||||
|
// margin left. No need for 'dd' items.
|
||||||
|
//
|
||||||
|
const { dt } = Astro.props;
|
||||||
|
interface Props {
|
||||||
|
dt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content: string = await Astro.slots.render('default');
|
||||||
|
---
|
||||||
|
|
||||||
|
<h6 set:html={dt} />
|
||||||
|
<div class="dd ml-8" set:html={content} />
|
87
front/src/components/ui/Form.astro
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
import type { Form as Props } from 'types';
|
||||||
|
import Button from 'components/ui/Button.astro';
|
||||||
|
|
||||||
|
const { inputs, textarea, disclaimer, button = 'Contact us', description = '', id, method, enctype } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<form id={id} method={method} enctype={enctype}>
|
||||||
|
{
|
||||||
|
inputs &&
|
||||||
|
inputs.map(
|
||||||
|
({ type = 'text', name, label = '', autocomplete = 'on', placeholder = '' }) =>
|
||||||
|
name && (
|
||||||
|
<div class="mb-6">
|
||||||
|
{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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
textarea && (
|
||||||
|
<div>
|
||||||
|
<label for="textarea" class="block text-sm font-medium">
|
||||||
|
{textarea.label}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="textarea"
|
||||||
|
name={textarea.name ? textarea.name : 'message'}
|
||||||
|
rows={textarea.rows ? textarea.rows : 4}
|
||||||
|
placeholder={textarea.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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
disclaimer && (
|
||||||
|
<div class="mt-3 flex items-start">
|
||||||
|
<div class="flex mt-0.5">
|
||||||
|
<input
|
||||||
|
id="disclaimer"
|
||||||
|
name="disclaimer"
|
||||||
|
type="checkbox"
|
||||||
|
class="cursor-pointer mt-1 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<label for="disclaimer" class="cursor-pointer select-none text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{disclaimer.label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
button && (
|
||||||
|
<div class="mt-10 grid">
|
||||||
|
<Button variant="primary" type="submit">
|
||||||
|
{button}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
description && (
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">{description}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</form>
|
35
front/src/components/ui/Headline.astro
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
import type { Headline as Props } from 'types';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
classes = {},
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
container: containerClass = 'max-w-3xl',
|
||||||
|
title: titleClass = 'text-3xl md:text-4xl ',
|
||||||
|
subtitle: subtitleClass = 'text-xl',
|
||||||
|
} = classes;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
(title || subtitle || tagline) && (
|
||||||
|
<div class={twMerge('mb-8 md:mx-auto md:mb-12 text-center', containerClass)}>
|
||||||
|
{tagline && (
|
||||||
|
<p class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase" set:html={tagline} />
|
||||||
|
)}
|
||||||
|
{title && (
|
||||||
|
<h2
|
||||||
|
class={twMerge('font-bold leading-tighter tracking-tighter font-heading text-heading text-3xl', titleClass)}
|
||||||
|
set:html={title}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{subtitle && <p class={twMerge('mt-4 text-muted', subtitleClass)} set:html={subtitle} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
65
front/src/components/ui/ItemGrid.astro
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
import type { ItemGrid as Props } from 'types';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import Button from './Button.astro';
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
const { items = [], columns, defaultIcon = '', classes = {} } = Astro.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
container: containerClass = '',
|
||||||
|
panel: panelClass = '',
|
||||||
|
title: titleClass = '',
|
||||||
|
description: descriptionClass = '',
|
||||||
|
icon: defaultIconClass = 'text-primary',
|
||||||
|
action: actionClass = '',
|
||||||
|
} = classes;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
items && (
|
||||||
|
<div
|
||||||
|
class={twMerge(
|
||||||
|
`grid mx-auto gap-8 md:gap-y-12 ${
|
||||||
|
columns === 4
|
||||||
|
? 'lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2'
|
||||||
|
: columns === 3
|
||||||
|
? 'lg:grid-cols-3 sm:grid-cols-2'
|
||||||
|
: columns === 2
|
||||||
|
? 'sm:grid-cols-2 '
|
||||||
|
: ''
|
||||||
|
}`,
|
||||||
|
containerClass
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{items.map(({ title, description, icon, callToAction, classes: itemClasses = {} }) => (
|
||||||
|
<div>
|
||||||
|
<div class={twMerge('flex flex-row max-w-md', panelClass, itemClasses?.panel)}>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
{(icon || defaultIcon) && (
|
||||||
|
<Icon
|
||||||
|
name={icon || defaultIcon}
|
||||||
|
class={twMerge('w-7 h-7 mr-2 rtl:mr-0 rtl:ml-2', defaultIconClass, itemClasses?.icon)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="mt-0.5">
|
||||||
|
{title && <h3 class={twMerge('text-xl font-bold', titleClass, itemClasses?.title)}>{title}</h3>}
|
||||||
|
{description && (
|
||||||
|
<p
|
||||||
|
class={twMerge(`${title ? 'mt-3' : ''} text-muted`, descriptionClass, itemClasses?.description)}
|
||||||
|
set:html={description}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{callToAction && (
|
||||||
|
<div class={twMerge(`${title || description ? 'mt-3' : ''}`, actionClass, itemClasses?.actionClass)}>
|
||||||
|
<Button variant="link" {...callToAction} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
53
front/src/components/ui/ItemGrid2.astro
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
import type { ItemGrid as Props } from 'types';
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import Button from './Button.astro';
|
||||||
|
|
||||||
|
const { items = [], columns, defaultIcon = '', classes = {} } = Astro.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
container: containerClass = '',
|
||||||
|
// container: containerClass = "sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
|
||||||
|
panel: panelClass = '',
|
||||||
|
title: titleClass = '',
|
||||||
|
description: descriptionClass = '',
|
||||||
|
icon: defaultIconClass = 'text-primary',
|
||||||
|
} = classes;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
items && (
|
||||||
|
<div
|
||||||
|
class={twMerge(
|
||||||
|
`grid gap-8 gap-x-12 sm:gap-y-8 ${
|
||||||
|
columns === 4
|
||||||
|
? 'lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2'
|
||||||
|
: columns === 3
|
||||||
|
? 'lg:grid-cols-3 sm:grid-cols-2'
|
||||||
|
: columns === 2
|
||||||
|
? 'sm:grid-cols-2 '
|
||||||
|
: ''
|
||||||
|
}`,
|
||||||
|
containerClass
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{items.map(({ title, description, icon, callToAction, classes: itemClasses = {} }) => (
|
||||||
|
<div class={twMerge('relative flex flex-col', panelClass, itemClasses?.panel)}>
|
||||||
|
{(icon || defaultIcon) && (
|
||||||
|
<Icon name={icon || defaultIcon} class={twMerge('mb-2 w-10 h-10', defaultIconClass, itemClasses?.icon)} />
|
||||||
|
)}
|
||||||
|
<div class={twMerge('text-xl font-bold', titleClass, itemClasses?.title)}>{title}</div>
|
||||||
|
{description && (
|
||||||
|
<p class={twMerge('text-muted mt-2', descriptionClass, itemClasses?.description)} set:html={description} />
|
||||||
|
)}
|
||||||
|
{callToAction && (
|
||||||
|
<div class="mt-2">
|
||||||
|
<Button {...callToAction} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
54
front/src/components/ui/Timeline.astro
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import type { Item } from 'types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
items?: Array<Item>;
|
||||||
|
defaultIcon?: string;
|
||||||
|
classes?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { items = [], classes = {}, defaultIcon } = Astro.props as Props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
container: containerClass = '',
|
||||||
|
panel: panelClass = '',
|
||||||
|
title: titleClass = '',
|
||||||
|
description: descriptionClass = '',
|
||||||
|
icon: defaultIconClass = 'text-primary dark:text-slate-200 border-primary dark:border-blue-700',
|
||||||
|
} = classes;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
items && items.length && (
|
||||||
|
<div class={containerClass}>
|
||||||
|
{items.map(({ title, description, icon, classes: itemClasses = {} }, index = 0) => (
|
||||||
|
<div class={twMerge('flex', panelClass, itemClasses?.panel)}>
|
||||||
|
<div class="flex flex-col items-center mr-4 rtl:mr-0 rtl:ml-4">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
{(icon || defaultIcon) && (
|
||||||
|
<Icon
|
||||||
|
name={icon || defaultIcon}
|
||||||
|
class={twMerge('w-10 h-10 p-2 rounded-full border-2', defaultIconClass, itemClasses?.icon)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{index !== items.length - 1 && <div class="w-px h-full bg-black/10 dark:bg-slate-400/50" />}
|
||||||
|
</div>
|
||||||
|
<div class={`pt-1 ${index !== items.length - 1 ? 'pb-8' : ''}`}>
|
||||||
|
{title && <p class={twMerge('text-xl font-bold', titleClass, itemClasses?.title)} set:html={title} />}
|
||||||
|
{description && (
|
||||||
|
<p
|
||||||
|
class={twMerge('text-muted mt-2', descriptionClass, itemClasses?.description)}
|
||||||
|
set:html={description}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
31
front/src/components/ui/WidgetWrapper.astro
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
import type { HTMLTag } from 'astro/types';
|
||||||
|
import type { Widget } from 'types';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import Background from './Background.astro';
|
||||||
|
|
||||||
|
export interface Props extends Widget {
|
||||||
|
containerClass?: string;
|
||||||
|
['as']?: HTMLTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, isDark = false, containerClass = '', bg, as = 'section' } = Astro.props;
|
||||||
|
|
||||||
|
const WrapperTag = as;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WrapperTag class="relative not-prose scroll-mt-[72px]" {...id ? { id } : {}}>
|
||||||
|
<div class="absolute inset-0 pointer-events-none -z-[1]" aria-hidden="true">
|
||||||
|
<slot name="bg">
|
||||||
|
{bg ? <Fragment set:html={bg} /> : <Background isDark={isDark} />}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class:list={[
|
||||||
|
twMerge('relative mx-auto max-w-7xl px-4 md:px-6 py-12 md:py-16 lg:py-20 text-default', containerClass),
|
||||||
|
{ dark: isDark },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</WrapperTag>
|
23
front/src/components/widgets/Announcement.astro
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="dark text-muted text-sm bg-black dark:bg-transparent dark:border-b dark:border-slate-800 dark:text-slate-400 hidden md:flex gap-1 overflow-hidden px-3 py-2 relative text-ellipsis whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="dark:bg-slate-700 bg-white/40 dark:text-slate-300 font-semibold px-1 py-0.5 text-xs mr-0.5 rtl:mr-0 rtl:ml-0.5 inline-block"
|
||||||
|
>NEW</span
|
||||||
|
>
|
||||||
|
<a href="https://astro.build/blog/astro-480/" class="text-muted hover:underline dark:text-slate-400 font-medium"
|
||||||
|
>Astro 4.8 is now available! »</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="ltr:ml-auto rtl:mr-auto w-[5.6rem] h-[1.25rem] ml-auto bg-contain inline-block bg-[url(https://img.shields.io/github/stars/onwidget/astrowind.svg?style=social&label=Stars&maxAge=86400)]"
|
||||||
|
title="If you like AstroWind, give us a star."
|
||||||
|
href="https://github.com/onwidget/astrowind"
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</div>
|
64
front/src/components/widgets/BlogHighlightedPosts.astro
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
import { APP_BLOG } from 'astrowind:config';
|
||||||
|
|
||||||
|
import Grid from 'components/blog/Grid.astro';
|
||||||
|
|
||||||
|
import { getBlogPermalink } from 'utils/permalinks';
|
||||||
|
import { findPostsByIds } from 'utils/blog';
|
||||||
|
import WidgetWrapper from 'components/ui/WidgetWrapper.astro';
|
||||||
|
import type { Widget } from 'types';
|
||||||
|
|
||||||
|
export interface Props extends Widget {
|
||||||
|
title?: string;
|
||||||
|
linkText?: string;
|
||||||
|
linkUrl?: string | URL;
|
||||||
|
information?: string;
|
||||||
|
postIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
linkText = 'View all posts',
|
||||||
|
linkUrl = getBlogPermalink(),
|
||||||
|
information = await Astro.slots.render('information'),
|
||||||
|
postIds = [],
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const posts = APP_BLOG.isEnabled ? await findPostsByIds(postIds) : [];
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
APP_BLOG.isEnabled ? (
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={classes?.container as string} bg={bg}>
|
||||||
|
<div class="flex flex-col lg:justify-between lg:flex-row mb-8">
|
||||||
|
{title && (
|
||||||
|
<div class="md:max-w-sm">
|
||||||
|
<h2
|
||||||
|
class="text-3xl font-bold tracking-tight sm:text-4xl sm:leading-none group font-heading mb-2"
|
||||||
|
set:html={title}
|
||||||
|
/>
|
||||||
|
{APP_BLOG.list.isEnabled && linkText && linkUrl && (
|
||||||
|
<a
|
||||||
|
class="text-muted dark:text-slate-400 hover:text-primary transition ease-in duration-200 block mb-6 lg:mb-0"
|
||||||
|
href={linkUrl}
|
||||||
|
>
|
||||||
|
{linkText} »
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{information && <p class="text-muted dark:text-slate-400 lg:text-sm lg:max-w-md" set:html={information} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Grid posts={posts} />
|
||||||
|
</WidgetWrapper>
|
||||||
|
) : (
|
||||||
|
<Fragment />
|
||||||
|
)
|
||||||
|
}
|
63
front/src/components/widgets/BlogLatestPosts.astro
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
import { APP_BLOG } from 'astrowind:config';
|
||||||
|
|
||||||
|
import Grid from 'components/blog/Grid.astro';
|
||||||
|
|
||||||
|
import { getBlogPermalink } from 'utils/permalinks';
|
||||||
|
import { findLatestPosts } from 'utils/blog';
|
||||||
|
import WidgetWrapper from 'components/ui/WidgetWrapper.astro';
|
||||||
|
import type { Widget } from 'types';
|
||||||
|
import Button from '../ui/Button.astro';
|
||||||
|
|
||||||
|
export interface Props extends Widget {
|
||||||
|
title?: string;
|
||||||
|
linkText?: string;
|
||||||
|
linkUrl?: string | URL;
|
||||||
|
information?: string;
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
linkText = 'View all posts',
|
||||||
|
linkUrl = getBlogPermalink(),
|
||||||
|
information = await Astro.slots.render('information'),
|
||||||
|
count = 4,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const posts = APP_BLOG.isEnabled ? await findLatestPosts({ count }) : [];
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
APP_BLOG.isEnabled ? (
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={classes?.container as string} bg={bg}>
|
||||||
|
<div class="flex flex-col lg:justify-between lg:flex-row mb-8">
|
||||||
|
{title && (
|
||||||
|
<div class="md:max-w-sm">
|
||||||
|
<h2
|
||||||
|
class="text-3xl font-bold tracking-tight sm:text-4xl sm:leading-none group font-heading mb-2"
|
||||||
|
set:html={title}
|
||||||
|
/>
|
||||||
|
{APP_BLOG.list.isEnabled && linkText && linkUrl && (
|
||||||
|
<Button variant="link" href={linkUrl}>
|
||||||
|
{' '}
|
||||||
|
{linkText} »
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{information && <p class="text-muted dark:text-slate-400 lg:text-sm lg:max-w-md" set:html={information} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Grid posts={posts} />
|
||||||
|
</WidgetWrapper>
|
||||||
|
) : (
|
||||||
|
<Fragment />
|
||||||
|
)
|
||||||
|
}
|
38
front/src/components/widgets/Brands.astro
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import type { Brands as Props } from 'types';
|
||||||
|
|
||||||
|
import Image from 'components/common/Image.astro';
|
||||||
|
import Headline from 'components/ui/Headline.astro';
|
||||||
|
import WidgetWrapper from 'components/ui/WidgetWrapper.astro';
|
||||||
|
const {
|
||||||
|
title = '',
|
||||||
|
subtitle = '',
|
||||||
|
tagline = '',
|
||||||
|
icons = [],
|
||||||
|
images = [],
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} />
|
||||||
|
|
||||||
|
<div class="flex flex-wrap justify-center gap-x-6 sm:gap-x-12 lg:gap-x-24">
|
||||||
|
{icons && icons.map((icon) => <Icon name={icon} class="py-3 lg:py-5 w-12 h-auto mx-auto sm:mx-0 text-gray-500" />)}
|
||||||
|
{
|
||||||
|
images &&
|
||||||
|
images.map(
|
||||||
|
(image) =>
|
||||||
|
image.src && (
|
||||||
|
<div class="flex justify-center col-span-1 my-2 lg:my-4 py-1 px-3 rounded-md dark:bg-gray-200">
|
||||||
|
<Image src={image.src} alt={image.alt || ''} class="max-h-12" width={120} height={48} layout="fixed" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
58
front/src/components/widgets/CallToAction.astro
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
import WidgetWrapper from '../ui/WidgetWrapper.astro';
|
||||||
|
import type { CallToAction, Widget } from 'types';
|
||||||
|
import Headline from 'components/ui/Headline.astro';
|
||||||
|
import Button from 'components/ui/Button.astro';
|
||||||
|
|
||||||
|
interface Props extends Widget {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
tagline?: string;
|
||||||
|
callToAction?: CallToAction;
|
||||||
|
actions?: string | CallToAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline = await Astro.slots.render('tagline'),
|
||||||
|
actions = await Astro.slots.render('actions'),
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<div
|
||||||
|
class="max-w-3xl mx-auto text-center p-6 rounded-md shadow-xl dark:shadow-none dark:border dark:border-slate-600"
|
||||||
|
>
|
||||||
|
<Headline
|
||||||
|
title={title}
|
||||||
|
subtitle={subtitle}
|
||||||
|
tagline={tagline}
|
||||||
|
classes={{
|
||||||
|
container: 'mb-0 md:mb-0',
|
||||||
|
title: 'text-4xl md:text-4xl font-bold tracking-tighter mb-4 font-heading',
|
||||||
|
subtitle: 'text-xl text-muted dark:text-slate-400',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
actions && (
|
||||||
|
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 mt-6">
|
||||||
|
{Array.isArray(actions) ? (
|
||||||
|
actions.map((action) => (
|
||||||
|
<div class="flex w-full sm:w-auto">
|
||||||
|
<Button {...(action || {})} class:list={["w-full", "sm:mb-0", action.class]} />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Fragment set:html={actions} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
46
front/src/components/widgets/Contact.astro
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
import FormContainer from 'components/ui/Form.astro';
|
||||||
|
import Headline from 'components/ui/Headline.astro';
|
||||||
|
import WidgetWrapper from 'components/ui/WidgetWrapper.astro';
|
||||||
|
import type { Contact as Props } from 'types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline = await Astro.slots.render('tagline'),
|
||||||
|
inputs,
|
||||||
|
textarea,
|
||||||
|
disclaimer,
|
||||||
|
button,
|
||||||
|
description,
|
||||||
|
formid,
|
||||||
|
method,
|
||||||
|
enctype,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} />
|
||||||
|
|
||||||
|
{
|
||||||
|
inputs && (
|
||||||
|
<div class="flex flex-col max-w-xl mx-auto rounded-lg backdrop-blur border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900 shadow p-4 sm:p-6 lg:p-8 w-full">
|
||||||
|
<FormContainer
|
||||||
|
inputs={inputs}
|
||||||
|
textarea={textarea}
|
||||||
|
disclaimer={disclaimer}
|
||||||
|
button={button}
|
||||||
|
description={description}
|
||||||
|
id={formid}
|
||||||
|
method={method}
|
||||||
|
enctype={enctype}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</WidgetWrapper>
|
94
front/src/components/widgets/Content.astro
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
import type { Content as Props } from 'types';
|
||||||
|
import Headline from '../ui/Headline.astro';
|
||||||
|
import WidgetWrapper from '../ui/WidgetWrapper.astro';
|
||||||
|
import Image from 'components/common/Image.astro';
|
||||||
|
import Button from 'components/ui/Button.astro';
|
||||||
|
import ItemGrid from '../ui/ItemGrid.astro';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
content = await Astro.slots.render('content'),
|
||||||
|
callToAction,
|
||||||
|
items = [],
|
||||||
|
columns,
|
||||||
|
image = await Astro.slots.render('image'),
|
||||||
|
isReversed = false,
|
||||||
|
isAfterContent = false,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper
|
||||||
|
id={id}
|
||||||
|
isDark={isDark}
|
||||||
|
containerClass={`max-w-7xl mx-auto ${isAfterContent ? 'pt-0 md:pt-0 lg:pt-0' : ''} ${classes?.container ?? ''}`}
|
||||||
|
bg={bg}
|
||||||
|
>
|
||||||
|
<Headline
|
||||||
|
title={title}
|
||||||
|
subtitle={subtitle}
|
||||||
|
tagline={tagline}
|
||||||
|
classes={{
|
||||||
|
container: 'max-w-xl sm:mx-auto lg:max-w-2xl',
|
||||||
|
title: 'text-4xl md:text-5xl font-bold tracking-tighter mb-4 font-heading',
|
||||||
|
subtitle: 'max-w-3xl mx-auto sm:text-center text-xl text-muted dark:text-slate-400',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div class="mx-auto max-w-7xl p-4 md:px-8">
|
||||||
|
<div class={`md:flex ${isReversed ? 'md:flex-row-reverse' : ''} md:gap-16`}>
|
||||||
|
<div class="md:basis-1/2 self-center">
|
||||||
|
{content && <div class="mb-12 text-lg dark:text-slate-400" set:html={content} />}
|
||||||
|
|
||||||
|
{
|
||||||
|
callToAction && (
|
||||||
|
<div class="mt-[-40px] mb-8 text-primary">
|
||||||
|
<Button variant="link" {...callToAction} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<ItemGrid
|
||||||
|
items={items}
|
||||||
|
columns={columns}
|
||||||
|
defaultIcon="tabler:check"
|
||||||
|
classes={{
|
||||||
|
container: `gap-y-4 md:gap-y-8`,
|
||||||
|
panel: 'max-w-none',
|
||||||
|
title: 'text-lg font-medium leading-6 dark:text-white ml-2 rtl:ml-0 rtl:mr-2',
|
||||||
|
description: 'text-muted dark:text-slate-400 ml-2 rtl:ml-0 rtl:mr-2',
|
||||||
|
icon: 'flex h-7 w-7 items-center justify-center rounded-full bg-green-600 dark:bg-green-700 text-gray-50 p-1',
|
||||||
|
action: 'text-lg font-medium leading-6 dark:text-white ml-2 rtl:ml-0 rtl:mr-2',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div aria-hidden="true" class="mt-10 md:mt-0 md:basis-1/2">
|
||||||
|
{
|
||||||
|
image && (
|
||||||
|
<div class="relative m-auto max-w-4xl">
|
||||||
|
{typeof image === 'string' ? (
|
||||||
|
<Fragment set:html={image} />
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
class="mx-auto w-full rounded-lg bg-gray-500 shadow-lg"
|
||||||
|
width={500}
|
||||||
|
height={500}
|
||||||
|
widths={[400, 768]}
|
||||||
|
sizes="(max-width: 768px) 100vw, 432px"
|
||||||
|
layout="responsive"
|
||||||
|
{...image}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
33
front/src/components/widgets/FAQs.astro
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
import Headline from 'components/ui/Headline.astro';
|
||||||
|
import ItemGrid from 'components/ui/ItemGrid.astro';
|
||||||
|
import WidgetWrapper from 'components/ui/WidgetWrapper.astro';
|
||||||
|
import type { Faqs as Props } from 'types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = '',
|
||||||
|
subtitle = '',
|
||||||
|
tagline = '',
|
||||||
|
items = [],
|
||||||
|
columns = 2,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} />
|
||||||
|
<ItemGrid
|
||||||
|
items={items}
|
||||||
|
columns={columns}
|
||||||
|
defaultIcon="tabler:chevron-right"
|
||||||
|
classes={{
|
||||||
|
container: `${columns === 1 ? 'max-w-4xl' : ''} gap-y-8 md:gap-y-12`,
|
||||||
|
panel: 'max-w-none',
|
||||||
|
icon: 'flex-shrink-0 mt-1 w-6 h-6 text-primary',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</WidgetWrapper>
|
36
front/src/components/widgets/Features.astro
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
import WidgetWrapper from 'components/ui/WidgetWrapper.astro';
|
||||||
|
import ItemGrid from 'components/ui/ItemGrid.astro';
|
||||||
|
import Headline from 'components/ui/Headline.astro';
|
||||||
|
import type { Features as Props } from 'types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline = await Astro.slots.render('tagline'),
|
||||||
|
items = [],
|
||||||
|
columns = 2,
|
||||||
|
|
||||||
|
defaultIcon,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-5xl ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} classes={classes?.headline as Record<string, string>} />
|
||||||
|
<ItemGrid
|
||||||
|
items={items}
|
||||||
|
columns={columns}
|
||||||
|
defaultIcon={defaultIcon}
|
||||||
|
classes={{
|
||||||
|
container: '',
|
||||||
|
title: 'md:text-[1.3rem]',
|
||||||
|
icon: 'text-white bg-primary rounded-full w-10 h-10 p-2 md:w-12 md:h-12 md:p-3 mr-4 rtl:ml-4 rtl:mr-0',
|
||||||
|
...((classes?.items as Record<string, never>) ?? {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</WidgetWrapper>
|
38
front/src/components/widgets/Features2.astro
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
import WidgetWrapper from 'components/ui/WidgetWrapper.astro';
|
||||||
|
import Headline from 'components/ui/Headline.astro';
|
||||||
|
import ItemGrid2 from 'components/ui/ItemGrid2.astro';
|
||||||
|
import type { Features as Props } from 'types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline = await Astro.slots.render('tagline'),
|
||||||
|
items = [],
|
||||||
|
columns = 3,
|
||||||
|
defaultIcon,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} classes={classes?.headline as Record<string, string>} />
|
||||||
|
<ItemGrid2
|
||||||
|
items={items}
|
||||||
|
columns={columns}
|
||||||
|
defaultIcon={defaultIcon}
|
||||||
|
classes={{
|
||||||
|
container: 'gap-4 md:gap-6',
|
||||||
|
panel:
|
||||||
|
'rounded-lg shadow-[0_4px_30px_rgba(0,0,0,0.1)] dark:shadow-[0_4px_30px_rgba(0,0,0,0.1)] backdrop-blur border border-[#ffffff29] bg-white dark:bg-slate-900 p-6',
|
||||||
|
// panel:
|
||||||
|
// "text-center bg-page items-center md:text-left rtl:md:text-right md:items-start p-6 p-6 rounded-md shadow-xl dark:shadow-none dark:border dark:border-slate-800",
|
||||||
|
icon: 'w-12 h-12 mb-6 text-primary',
|
||||||
|
...((classes?.items as Record<string, never>) ?? {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</WidgetWrapper>
|
70
front/src/components/widgets/Features3.astro
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
import Headline from 'components/ui/Headline.astro';
|
||||||
|
import ItemGrid from 'components/ui/ItemGrid.astro';
|
||||||
|
import WidgetWrapper from 'components/ui/WidgetWrapper.astro';
|
||||||
|
import Image from 'components/common/Image.astro';
|
||||||
|
import type { Features as Props } from 'types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline = await Astro.slots.render('tagline'),
|
||||||
|
image,
|
||||||
|
items = [],
|
||||||
|
columns,
|
||||||
|
defaultIcon,
|
||||||
|
isBeforeContent,
|
||||||
|
isAfterContent,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper
|
||||||
|
id={id}
|
||||||
|
isDark={isDark}
|
||||||
|
containerClass={`${isBeforeContent ? 'md:pb-8 lg:pb-12' : ''} ${isAfterContent ? 'pt-0 md:pt-0 lg:pt-0' : ''} ${
|
||||||
|
classes?.container ?? ''
|
||||||
|
}`}
|
||||||
|
bg={bg}
|
||||||
|
>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} classes={classes?.headline as Record<string, string>} />
|
||||||
|
|
||||||
|
<div aria-hidden="true" class="aspect-w-16 aspect-h-7">
|
||||||
|
{
|
||||||
|
image && (
|
||||||
|
<div class="w-full h-80 object-cover rounded-xl mx-auto bg-gray-500 shadow-lg">
|
||||||
|
{typeof image === 'string' ? (
|
||||||
|
<Fragment set:html={image} />
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
class="w-full h-80 object-cover rounded-xl mx-auto bg-gray-500 shadow-lg"
|
||||||
|
width="auto"
|
||||||
|
height={320}
|
||||||
|
widths={[400, 768]}
|
||||||
|
layout="fullWidth"
|
||||||
|
{...image}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ItemGrid
|
||||||
|
items={items}
|
||||||
|
columns={columns}
|
||||||
|
defaultIcon={defaultIcon}
|
||||||
|
classes={{
|
||||||
|
container: 'mt-12',
|
||||||
|
panel: 'max-w-full sm:max-w-md',
|
||||||
|
title: 'text-lg font-semibold',
|
||||||
|
description: 'mt-0.5',
|
||||||
|
icon: 'flex-shrink-0 mt-1 text-primary w-6 h-6',
|
||||||
|
...((classes?.items as object) ?? {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</WidgetWrapper>
|
102
front/src/components/widgets/Footer.astro
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { SITE } from 'astrowind:config';
|
||||||
|
import { getHomePermalink } from 'utils/permalinks';
|
||||||
|
|
||||||
|
interface Link {
|
||||||
|
text?: string;
|
||||||
|
href?: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Links {
|
||||||
|
title?: string;
|
||||||
|
links: Array<Link>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
links: Array<Links>;
|
||||||
|
secondaryLinks: Array<Link>;
|
||||||
|
socialLinks: Array<Link>;
|
||||||
|
footNote?: string;
|
||||||
|
theme?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { socialLinks = [], secondaryLinks = [], links = [], footNote = '', theme = 'light' } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<footer class:list={[{ dark: theme === 'dark' }, 'relative border-t border-gray-200 dark:border-slate-800 not-prose, mt-auto, h-{11rem}']}>
|
||||||
|
<div class="dark:bg-dark absolute inset-0 pointer-events-none" aria-hidden="true"></div>
|
||||||
|
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 dark:text-slate-300">
|
||||||
|
<div class="grid grid-cols-12 gap-4 gap-y-8 sm:gap-8 py-8 md:py-12">
|
||||||
|
<div class="col-span-12 lg:col-span-4">
|
||||||
|
<div class="mb-2">
|
||||||
|
<a class="inline-block font-bold text-xl" href={getHomePermalink()}>{SITE?.name}</a>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-muted flex gap-1">
|
||||||
|
{
|
||||||
|
secondaryLinks.map(({ text, href }, index) => (
|
||||||
|
<>
|
||||||
|
{index !== 0 ? ' · ' : ''}
|
||||||
|
<a
|
||||||
|
class="text-muted hover:text-gray-700 dark:text-gray-400 hover:underline transition duration-150 ease-in-out"
|
||||||
|
href={href}
|
||||||
|
set:html={text}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
links.map(({ title, links }) => (
|
||||||
|
<div class="col-span-6 md:col-span-3 lg:col-span-2">
|
||||||
|
<div class="dark:text-gray-300 font-medium mb-2">{title}</div>
|
||||||
|
{links && Array.isArray(links) && links.length > 0 && (
|
||||||
|
<ul class="text-sm">
|
||||||
|
{links.map(({ text, href, ariaLabel }) => (
|
||||||
|
<li class="mb-2">
|
||||||
|
<a
|
||||||
|
class="text-muted hover:text-gray-700 hover:underline dark:text-gray-400 transition duration-150 ease-in-out"
|
||||||
|
href={href}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
>
|
||||||
|
<Fragment set:html={text} />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="md:flex md:items-center md:justify-between py-6 md:py-8">
|
||||||
|
{
|
||||||
|
socialLinks?.length ? (
|
||||||
|
<ul class="flex mb-4 md:order-1 -ml-2 md:ml-4 md:mb-0 rtl:ml-0 rtl:-mr-2 rtl:md:ml-0 rtl:md:mr-4">
|
||||||
|
{socialLinks.map(({ ariaLabel, href, text, icon }) => (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="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"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
href={href}
|
||||||
|
>
|
||||||
|
{icon && <Icon name={icon} class="w-5 h-5" />}
|
||||||
|
<Fragment set:html={text} />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="text-sm mr-4 dark:text-muted">
|
||||||
|
<Fragment set:html={footNote} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
173
front/src/components/widgets/Header.astro
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components'
|
||||||
|
import Logo from 'components/Logo.astro'
|
||||||
|
import ToggleTheme from 'components/common/ToggleTheme.astro'
|
||||||
|
import ToggleMenu from 'components/common/ToggleMenu.astro'
|
||||||
|
import Button from 'components/ui/Button.astro'
|
||||||
|
|
||||||
|
import { getHomePermalink } from 'utils/permalinks'
|
||||||
|
import { trimSlash, getAsset } from 'utils/permalinks'
|
||||||
|
import type { CallToAction } from 'types'
|
||||||
|
|
||||||
|
const pb = Astro.locals.pb
|
||||||
|
|
||||||
|
const connected = pb.authStore.isValid
|
||||||
|
interface Link {
|
||||||
|
text?: string
|
||||||
|
href?: string
|
||||||
|
ariaLabel?: string
|
||||||
|
icon?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionLink extends CallToAction {}
|
||||||
|
|
||||||
|
interface MenuLink extends Link {
|
||||||
|
links?: Array<MenuLink>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
id?: string
|
||||||
|
links?: Array<MenuLink>
|
||||||
|
actions?: Array<ActionLink>
|
||||||
|
isSticky?: boolean
|
||||||
|
isDark?: boolean
|
||||||
|
isFullWidth?: boolean
|
||||||
|
showToggleTheme?: boolean
|
||||||
|
showRssFeed?: boolean
|
||||||
|
position?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
id = 'header',
|
||||||
|
links = [],
|
||||||
|
actions = [],
|
||||||
|
isSticky = false,
|
||||||
|
isDark = false,
|
||||||
|
isFullWidth = false,
|
||||||
|
showToggleTheme = false,
|
||||||
|
showRssFeed = false,
|
||||||
|
position = 'center',
|
||||||
|
} = Astro.props
|
||||||
|
|
||||||
|
const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`
|
||||||
|
---
|
||||||
|
|
||||||
|
<header
|
||||||
|
class:list={[
|
||||||
|
{ sticky: isSticky, relative: !isSticky, dark: isDark },
|
||||||
|
'top-0 z-[1001] h-[5rem] bg-page flex-none mx-auto w-full border-b border-gray-50/0 transition-[opacity] ease-in-out',
|
||||||
|
]}
|
||||||
|
{...isSticky ? { 'data-aw-sticky-header': true } : {}}
|
||||||
|
{...id ? { id } : {}}
|
||||||
|
>
|
||||||
|
<div class="absolute inset-0"></div>
|
||||||
|
<div
|
||||||
|
class:list={[
|
||||||
|
'relative text-default py-3 px-3 md:px-6 mx-auto w-full',
|
||||||
|
{
|
||||||
|
'md:flex md:justify-between': position !== 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'md:grid md:grid-cols-3 md:items-center': position === 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'max-w-7xl': !isFullWidth,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div class:list={[{ 'mr-auto rtl:mr-0 rtl:ml-auto': position === 'right' }, 'flex justify-between']}>
|
||||||
|
<a class="flex items-center" href={getHomePermalink()}>
|
||||||
|
<Logo />
|
||||||
|
</a>
|
||||||
|
<div class="flex items-center md:hidden">
|
||||||
|
<ToggleMenu />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav
|
||||||
|
class="items-center w-full md:w-auto hidden md:flex md:mx-5 text-default overflow-y-auto overflow-x-hidden md:overflow-y-visible md:overflow-x-auto md:justify-self-center"
|
||||||
|
aria-label="Main navigation"
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
class="flex flex-col bg-page md:flex-row md:self-center w-full md:w-auto text-xl md:text-[0.9375rem] tracking-[0.01rem] font-medium md:justify-center"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
links.map(({ text, href, links }) => (
|
||||||
|
<li class={links?.length ? 'dropdown' : ''}>
|
||||||
|
{links?.length ? (
|
||||||
|
<>
|
||||||
|
<button type="button" class="hover:text-link dark:hover:text-white px-4 py-3 flex items-center">
|
||||||
|
{text}{' '}
|
||||||
|
<Icon name="tabler:chevron-down" class="w-3.5 h-3.5 ml-0.5 rtl:ml-0 rtl:mr-0.5 hidden md:inline" />
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu md:backdrop-blur-md dark:md:bg-dark rounded md:absolute pl-4 md:pl-0 md:hidden font-medium md:bg-white/90 md:min-w-[200px] drop-shadow-xl">
|
||||||
|
{links.map(({ text: text2, href: href2 }) => (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class:list={[
|
||||||
|
'first:rounded-t last:rounded-b md:hover:bg-gray-100 hover:text-link dark:hover:text-white dark:hover:bg-gray-700 py-2 px-5 block whitespace-no-wrap',
|
||||||
|
{ 'aw-link-active': href2 === currentPath },
|
||||||
|
]}
|
||||||
|
href={href2}
|
||||||
|
>
|
||||||
|
{text2}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
class:list={[
|
||||||
|
'hover:text-link dark:hover:text-white px-4 py-3 flex items-center',
|
||||||
|
{ 'aw-link-active': href === currentPath },
|
||||||
|
]}
|
||||||
|
href={href}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<div
|
||||||
|
class:list={[
|
||||||
|
{ 'ml-auto rtl:ml-0 rtl:mr-auto': position === 'left' },
|
||||||
|
'hidden md:self-center md:flex items-center md:mb-0 fixed w-full md:w-auto md:static justify-end left-0 rtl:left-auto rtl:right-0 bottom-0 p-3 md:p-0 md:justify-self-end',
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div class="items-center flex justify-between w-full md:w-auto">
|
||||||
|
<div class="flex">
|
||||||
|
{showToggleTheme && <ToggleTheme iconClass="w-6 h-6 md:w-5 md:h-5 md:inline-block" />}
|
||||||
|
{
|
||||||
|
showRssFeed && (
|
||||||
|
<a
|
||||||
|
class="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"
|
||||||
|
aria-label="RSS Feed"
|
||||||
|
href={getAsset('/rss.xml')}
|
||||||
|
>
|
||||||
|
<Icon name="tabler:rss" class="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{ !connected && (
|
||||||
|
<span class="ml-4 rtl:ml-0 rtl:mr-4">
|
||||||
|
<Button href='/account/login' variant='primary' class="ml-2 py-2.5 px-5.5 md:px-6 font-semibold shadow-none text-sm w-auto">
|
||||||
|
Connexion
|
||||||
|
</Button>
|
||||||
|
<Button href='/account/register' variant='secondary' class="ml-2 py-2.5 px-5.5 md:px-6 font-semibold shadow-none text-sm w-auto">
|
||||||
|
Inscription
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{ connected && (
|
||||||
|
<Button href='/account/logout' variant='primary' class="ml-2 py-2.5 px-5.5 md:px-6 font-semibold shadow-none text-sm w-auto">
|
||||||
|
Déconnexion
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
92
front/src/components/widgets/Hero.astro
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
import Image from 'components/common/Image.astro';
|
||||||
|
import Button from 'components/ui/Button.astro';
|
||||||
|
import type { CallToAction } from 'types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
id?: string;
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
tagline?: string;
|
||||||
|
content?: string;
|
||||||
|
actions?: string | CallToAction[];
|
||||||
|
image?: string | unknown; // TODO: find HTMLElementProps
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
content = await Astro.slots.render('content'),
|
||||||
|
actions = await Astro.slots.render('actions'),
|
||||||
|
image = await Astro.slots.render('image'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="relative md:-mt-[76px] not-prose" {...id ? { id } : {}}>
|
||||||
|
<div class="absolute inset-0 pointer-events-none" aria-hidden="true"></div>
|
||||||
|
<div class="relative max-w-7xl mx-auto px-4 sm:px-6">
|
||||||
|
<div class="pt-0 md:pt-[76px] pointer-events-none"></div>
|
||||||
|
<div class="py-12 md:py-20">
|
||||||
|
<div class="text-center pb-10 md:pb-16 max-w-5xl mx-auto">
|
||||||
|
{
|
||||||
|
tagline && (
|
||||||
|
<p
|
||||||
|
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase"
|
||||||
|
set:html={tagline}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
title && (
|
||||||
|
<h1
|
||||||
|
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200"
|
||||||
|
set:html={title}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div class="max-w-3xl mx-auto">
|
||||||
|
{subtitle && <p class="text-xl text-muted mb-6 dark:text-slate-300" set:html={subtitle} />}
|
||||||
|
{
|
||||||
|
actions && (
|
||||||
|
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4">
|
||||||
|
{Array.isArray(actions) ? (
|
||||||
|
actions.map((action) => (
|
||||||
|
<div class="flex w-full sm:w-auto">
|
||||||
|
<Button {...(action || {})} class="w-full sm:mb-0" />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Fragment set:html={actions} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{content && <Fragment set:html={content} />}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
image && (
|
||||||
|
<div class="relative m-auto max-w-5xl">
|
||||||
|
{typeof image === 'string' ? (
|
||||||
|
<Fragment set:html={image} />
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
class="mx-auto rounded-md w-full"
|
||||||
|
widths={[400, 768, 1024, 2040]}
|
||||||
|
sizes="(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px"
|
||||||
|
loading="eager"
|
||||||
|
width={1024}
|
||||||
|
height={576}
|
||||||
|
{...image}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
91
front/src/components/widgets/Hero2.astro
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
import Image from 'components/common/Image.astro';
|
||||||
|
import type { CallToAction } from 'types';
|
||||||
|
import Button from 'components/ui/Button.astro';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
tagline?: string;
|
||||||
|
content?: string;
|
||||||
|
actions?: string | CallToAction[];
|
||||||
|
image?: string | unknown; // TODO: find HTMLElementProps
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
content = await Astro.slots.render('content'),
|
||||||
|
actions = await Astro.slots.render('actions'),
|
||||||
|
image = await Astro.slots.render('image'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="relative md:-mt-[76px] not-prose">
|
||||||
|
<div class="absolute inset-0 pointer-events-none" aria-hidden="true"></div>
|
||||||
|
<div class="relative max-w-7xl mx-auto px-4 sm:px-6">
|
||||||
|
<div class="pt-0 md:pt-[76px] pointer-events-none"></div>
|
||||||
|
<div class="py-12 md:py-20 lg:py-0 lg:flex lg:items-center lg:h-screen lg:gap-8">
|
||||||
|
<div class="basis-1/2 text-center lg:text-left pb-10 md:pb-16 mx-auto">
|
||||||
|
{
|
||||||
|
tagline && (
|
||||||
|
<p
|
||||||
|
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase"
|
||||||
|
set:html={tagline}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
title && (
|
||||||
|
<h1
|
||||||
|
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200"
|
||||||
|
set:html={title}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div class="max-w-3xl mx-auto lg:max-w-none">
|
||||||
|
{subtitle && <p class="text-xl text-muted mb-6 dark:text-slate-300" set:html={subtitle} />}
|
||||||
|
|
||||||
|
{
|
||||||
|
actions && (
|
||||||
|
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 lg:justify-start lg:m-0 lg:max-w-7xl">
|
||||||
|
{Array.isArray(actions) ? (
|
||||||
|
actions.map((action) => (
|
||||||
|
<div class="flex w-full sm:w-auto">
|
||||||
|
<Button {...(action || {})} class="w-full sm:mb-0" />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Fragment set:html={actions} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{content && <Fragment set:html={content} />}
|
||||||
|
</div>
|
||||||
|
<div class="basis-1/2">
|
||||||
|
{
|
||||||
|
image && (
|
||||||
|
<div class="relative m-auto max-w-5xl">
|
||||||
|
{typeof image === 'string' ? (
|
||||||
|
<Fragment set:html={image} />
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
class="mx-auto rounded-md w-full"
|
||||||
|
widths={[400, 768, 1024, 2040]}
|
||||||
|
sizes="(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px"
|
||||||
|
loading="eager"
|
||||||
|
width={600}
|
||||||
|
height={600}
|
||||||
|
{...image}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|