13 Commits

Author SHA1 Message Date
4113b77f81 add readme and fix save
All checks were successful
Build Docker Image Front / run (pull_request) Successful in 2m1s
Build Docker Image Back / run (pull_request) Successful in 24s
JsDocs / docs (pull_request) Successful in 26s
Test and coverage / coverage (pull_request) Successful in 1m15s
2024-06-09 13:12:32 +02:00
dba9f6ada9 feat: add pb export
All checks were successful
Build Docker Image Front / run (pull_request) Successful in 24s
Build Docker Image Back / run (pull_request) Successful in 24s
JsDocs / docs (pull_request) Successful in 24s
Test and coverage / coverage (pull_request) Successful in 1m16s
2024-06-09 12:50:36 +02:00
9591766e1f fix indent
All checks were successful
Build Docker Image Front / run (pull_request) Successful in 25s
Build Docker Image Back / run (pull_request) Successful in 23s
JsDocs / docs (pull_request) Successful in 27s
Test and coverage / coverage (pull_request) Successful in 1m10s
2024-06-09 12:47:55 +02:00
a7886495ec fix running condition
All checks were successful
Build Docker Image Front / run (pull_request) Successful in 25s
Build Docker Image Back / run (pull_request) Successful in 24s
Test and coverage / coverage (pull_request) Successful in 1m13s
2024-06-09 12:47:11 +02:00
2f92fff8c5 fix: jobs name
All checks were successful
Build Docker Image Front / run (pull_request) Successful in 25s
Build Docker Image Back / run (pull_request) Successful in 25s
Test and coverage / coverage (pull_request) Successful in 1m18s
2024-06-09 12:46:17 +02:00
93080ea315 add jsdoc CI
All checks were successful
Build Docker Image Front / run (pull_request) Successful in 28s
Build Docker Image Back / run (pull_request) Successful in 24s
Test and coverage / coverage (pull_request) Successful in 1m19s
2024-06-09 12:41:34 +02:00
e9ce2d1f9f feat/final commit
All checks were successful
Build Docker Image Front / run (push) Successful in 1m56s
Build Docker Image Back / run (push) Successful in 25s
2024-06-09 20:05:28 +02:00
3cb5a30270 feat: use drink api in front and more (#16)
All checks were successful
Build Docker Image Front / run (push) Successful in 28s
Build Docker Image Back / run (push) Successful in 27s
Reviewed-on: #16
Co-authored-by: Clement <c.boesmier@aptatio.com>
Co-committed-by: Clement <c.boesmier@aptatio.com>
2024-06-07 19:09:57 +02:00
2cb2166093 fix: correction prod docker (#15)
All checks were successful
Build Docker Image Front / run (push) Successful in 26s
Build Docker Image Back / run (push) Successful in 24s
Reviewed-on: #15
Co-authored-by: Clement <c.boesmier@aptatio.com>
Co-committed-by: Clement <c.boesmier@aptatio.com>
2024-05-21 10:40:13 +02:00
1593fa3493 feat: astro template for front and fix login(#13)
All checks were successful
Build Docker Image Front / run (push) Successful in 25s
Build Docker Image Back / run (push) Successful in 25s
Reviewed-on: #13
Co-authored-by: Clement <c.boesmier@aptatio.com>
Co-committed-by: Clement <c.boesmier@aptatio.com>
2024-05-21 10:07:56 +02:00
57a57c63ff feat: gestion-utilisateur (#1)
All checks were successful
Build Docker Image Front / run (push) Successful in 24s
Build Docker Image Back / run (push) Successful in 21s
Reviewed-on: #1
Co-authored-by: Clement <c.boesmier@aptatio.com>
Co-committed-by: Clement <c.boesmier@aptatio.com>
2024-05-20 12:48:34 +02:00
0c85df498e fix: rm port var in docker file (#12)
All checks were successful
Build Docker Image Front / run (push) Successful in 26s
Build Docker Image Back / run (push) Successful in 22s
Build Docker Image Front / run (pull_request) Successful in 23s
Build Docker Image Back / run (pull_request) Successful in 23s
JsDocs / coverage (pull_request) Successful in 23s
Test and coverage / coverage (pull_request) Successful in 1m38s
Reviewed-on: #12
Co-authored-by: Clement <c.boesmier@aptatio.com>
Co-committed-by: Clement <c.boesmier@aptatio.com>
2024-05-19 16:26:52 +02:00
e0c3580269 feat: open-trip-api-drink (#11)
All checks were successful
Build Docker Image Front / run (push) Successful in 28s
Build Docker Image Back / run (push) Successful in 23s
Reviewed-on: #11
Co-authored-by: Clement <c.boesmier@aptatio.com>
Co-committed-by: Clement <c.boesmier@aptatio.com>
2024-05-19 11:38:38 +02:00
183 changed files with 17729 additions and 2588 deletions

View File

@ -1,10 +1,16 @@
name: JsDocs
on:
pull_request:
push:
branches:
- '*'
tags:
- v*
pull_request:
branches:
- master
jobs:
coverage:
docs:
runs-on: ubuntu-latest
steps:
- name: Checkout # rapatrie le depot

View File

@ -47,7 +47,7 @@ WORKDIR /home/node
EXPOSE 3000
# Add Healthcheck
# FIXME: remttre check avec un port
# 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

View File

@ -1,5 +1,8 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
testPathIgnorePatterns: [
"<rootDir>/dist/"
],
testTimeout: 10000,
preset: 'ts-jest',
testEnvironment: 'node',

View File

@ -8,6 +8,7 @@
"@types/express": "^4.17.21",
"@types/node": "^20.12.7",
"axios": "^1.6.8",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"rimraf": "^5.0.5",
@ -15,6 +16,7 @@
"swagger-ui-express": "^5.0.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/jest": "^29.5.12",
"@types/rewire": "^2.5.30",
"@types/supertest": "^6.0.2",
@ -1968,6 +1970,15 @@
"integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
"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": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
@ -4745,6 +4756,18 @@
"dev": 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": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
@ -9248,7 +9271,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}

View File

@ -14,6 +14,7 @@
"@types/express": "^4.17.21",
"@types/node": "^20.12.7",
"axios": "^1.6.8",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"rimraf": "^5.0.5",
@ -21,6 +22,7 @@
"swagger-ui-express": "^5.0.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/jest": "^29.5.12",
"@types/rewire": "^2.5.30",
"@types/supertest": "^6.0.2",

View File

@ -1,11 +1,13 @@
import { getCity, getRadius, getPoiId, getBox } from "./openTripMaps"
import express from "express"
import cors from "cors"
/**
* Initialize Express application instance.
* @returns An initialized Express application object.
*/
const app = express()
app.use(cors())
/**

View File

@ -13,7 +13,7 @@ const key = process.env.OPEN_TRIP_MAPS_KEY
* @param {string} lat2 Latitude of the 2nd point of the box
* @returns {FeatureCollection} a list of POIs with their type, id, etc. (cf: [opentripmap](https://dev.opentripmap.org/docs#))
*/
async function callBox(lon1:string, lat1:string, lon2: string, lat2: string) {
async function callBox(lon1:string, lat1:string, lon2: string, lat2: string, rate: string) {
const lonMin = Math.min(parseFloat(lon1), parseFloat(lon2))
const lonMax = Math.max(parseFloat(lon1), parseFloat(lon2))
const latMin = Math.min(parseFloat(lat1), parseFloat(lat2))
@ -27,6 +27,7 @@ async function callBox(lon1:string, lat1:string, lon2: string, lat2: string) {
lon_max: lonMax,
lat_min: latMin,
lat_max: latMax,
rate: rate,
apikey: key,
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 lon2 = req.query["lon2"] as string
const lat2 = req.query["lat2"] as string
let rate = req.query["rate"] as string
if(!lon1 || !lat1 || !lon2 || !lat2){
res.status(400).send("Missing Argument")
return
}
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

View File

@ -21,7 +21,7 @@ const options = {
}
],
},
apis: ['./src/*.ts'],
apis: ['./src/*.ts','./dist/src/*.js'],
explorer: true
}

View File

@ -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)
.get("/otm/box")
.query({'lon1':'-1.435199','lon2':'-1.43519', 'lat1':'46.668460', 'lat2':'46.668461'})

155
PB_export.json Normal file
View File

@ -0,0 +1,155 @@
[
{
"id": "_pb_users_auth_",
"name": "users",
"type": "auth",
"system": false,
"schema": [
{
"system": false,
"id": "users_name",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "users_avatar",
"name": "avatar",
"type": "file",
"required": false,
"presentable": false,
"unique": false,
"options": {
"mimeTypes": [
"image/jpeg",
"image/png",
"image/svg+xml",
"image/gif",
"image/webp"
],
"thumbs": null,
"maxSelect": 1,
"maxSize": 5242880,
"protected": false
}
}
],
"indexes": [],
"listRule": "id = @request.auth.id",
"viewRule": "id = @request.auth.id",
"createRule": "",
"updateRule": "id = @request.auth.id",
"deleteRule": "id = @request.auth.id",
"options": {
"allowEmailAuth": true,
"allowOAuth2Auth": true,
"allowUsernameAuth": true,
"exceptEmailDomains": null,
"manageRule": null,
"minPasswordLength": 8,
"onlyEmailDomains": null,
"onlyVerified": false,
"requireEmail": false
}
},
{
"id": "okcdl5ouza7wpmh",
"name": "POI",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "tizn7ekv",
"name": "Poi",
"type": "json",
"required": true,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
},
{
"system": false,
"id": "sknwezy1",
"name": "Poi_id",
"type": "text",
"required": true,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
}
],
"indexes": [
"CREATE UNIQUE INDEX `idx_iGYiVpo` ON `POI` (`Poi_id`)"
],
"listRule": "@request.auth.id != \"\"",
"viewRule": "@request.auth.id != \"\"",
"createRule": "(@request.auth.id != \"\")",
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "iqpfqlcao9so0mj",
"name": "user_poi",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "skjq9wtr",
"name": "owner",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "_pb_users_auth_",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "buw92nch",
"name": "poi_list",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "okcdl5ouza7wpmh",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
}
],
"indexes": [
"CREATE UNIQUE INDEX `idx_PC3A5Pg` ON `user_poi` (\n `owner`,\n `poi_list`\n)"
],
"listRule": "",
"viewRule": "",
"createRule": "",
"updateRule": null,
"deleteRule": "",
"options": {}
}
]

View File

@ -1,4 +1,19 @@
Original repo : https://git.lab-ouest.org/Epitech/ratrapage_T-WEB
Submodule repo : https://git.lab-ouest.org/Epitech/fork-open-data-tourism/
Submodule repo : https://git.lab-ouest.org/Epitech/fork-open-data-tourism/
# Prod link :
JSDocs : https://drink-tweb.docs.cb85.fr/
Swagger : https://drink-tweb.cb85.fr/api-docs/
PB : https://pb-tweb.cb85.fr/
Datatourisme :
- https://datatour-tweb.cb85.fr/
- https://datatour-tweb.cb85.fr/graphiql
- https://datatour-tweb.cb85.fr/voyager

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ meta {
}
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
auth: none
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,9 @@ services:
- public
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:
- pocketbase
labels:
@ -30,7 +32,10 @@ services:
- public
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:
- port=${BACK_BASE_PORT}
- OPEN_TRIP_MAPS_KEY=${OPEN_TRIP_MAPS_KEY}
labels:
- traefik.enable=true
- traefik.http.routers.expressDrinkTweb.rule=Host(`${DRINK_URL}`)
@ -90,4 +95,4 @@ x-dockge:
- https://${FRONT_URL}/
- https://${DOCS_DRINK_URL}/
- https://${DRINK_URL}/
- https://${DATA_TOURISME_URL}/
- https://${DATA_TOURISME_URL}/

12
front/.editorconfig Normal file
View File

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

8
front/.eslintignore Normal file
View File

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

324
front/.eslintrc.json Normal file
View File

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

10
front/.gitignore vendored
View File

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

2
front/.npmrc Normal file
View File

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

4
front/.prettierignore Normal file
View File

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

13
front/.prettierrc.cjs Normal file
View File

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

6
front/.stackblitzrc Normal file
View File

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

View File

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

View File

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

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

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

21
front/LICENSE.md Normal file
View File

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

View File

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

View File

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

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

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

9
front/netlify.toml Normal file
View File

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

8391
front/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

2
front/public/_headers Normal file
View File

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

View File

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

View File

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

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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} />

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

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

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

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

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

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

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

View 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 />
)
}

View 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 />
)
}

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

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

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

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

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