Compare commits

..

1 Commits

Author SHA1 Message Date
Reinaldy Rafli 324a8b051f
Merge 9aedbc6648 into 823f93f556 2023-08-12 05:36:17 +00:00
47 changed files with 5653 additions and 11639 deletions

4
.github/FUNDING.yml vendored
View File

@ -1,3 +1 @@
github: aldy505
ko_fi: aldy505
liberapay: aldy505
ko_fi: aldy505

View File

@ -1,127 +1,125 @@
name: CI
on:
push:
branches: [ "master" ]
jobs:
api-build:
name: API
runs-on: ubuntu-latest
container: golang:1.21.0-bookworm
timeout-minutes: 15
services:
bucket:
image: minio/minio:edge-cicd
env:
MINIO_ROOT_USER: root
MINIO_ROOT_PASSWORD: verysecurepassword
MINIO_ACCESS_KEY: minio_access_key
MINIO_SECRET_KEY: minio_access_key
ports:
- 9000:9000
options: >-
--health-cmd "curl -f http://bucket:9000/minio/health/live"
--health-interval 45s
--health-timeout 30s
--health-retries 10
--health-start-period 120s
volumes:
- minio-data:/data
redis:
image: redis:7.0.12-bookworm
ports:
- 6379:6379
defaults:
run:
working-directory: ./api
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Installling dependencies
run: go mod download
- name: Build
run: go build main.go
- name: Run test & coverage
run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
env:
ENV: development
PORT: 5000
MINIO_HOST: bucket:9000
MINIO_ACCESS_ID: root
MINIO_SECRET_KEY: verysecurepassword
REDIS_URL: redis://@redis:6379
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: go
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
- name: Create Sentry release
uses: getsentry/action-release@v1
continue-on-error: true
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT_API}}
with:
environment: production
set_commits: skip
version: ${{ github.sha }}
- uses: codecov/codecov-action@v4
with:
flags: api
client-build:
name: Client
runs-on: ubuntu-latest
container: node:18.17.1-bookworm
timeout-minutes: 15
defaults:
run:
working-directory: ./client
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Installling dependencies
run: npm install
- name: Lint
run: npm run lint
- name: Build
run: npm run build
env:
VITE_SENTRY_DSN: https://examplePublicKey@o0.ingest.sentry.io/0
VITE_NODE_ENV: development
VITE_API_ENDPOINT: https://jokesbapak2.reinaldyrafli.com/api/v1
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: javascript
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
- name: Create Sentry release
uses: getsentry/action-release@v1
continue-on-error: true
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT_CLIENT }}
with:
environment: production
set_commits: skip
version: ${{ github.sha }}
name: CI
on:
push:
branches: [ "master" ]
jobs:
api-build:
name: API
runs-on: ubuntu-latest
container: golang:1.21.0-bookworm
timeout-minutes: 15
services:
bucket:
image: minio/minio:edge-cicd
env:
MINIO_ROOT_USER: root
MINIO_ROOT_PASSWORD: verysecurepassword
MINIO_ACCESS_KEY: minio_access_key
MINIO_SECRET_KEY: minio_access_key
ports:
- 9000:9000
options: >-
--health-cmd "curl -f http://bucket:9000/minio/health/live"
--health-interval 45s
--health-timeout 30s
--health-retries 10
--health-start-period 120s
volumes:
- minio-data:/data
redis:
image: redis:7.0.12-bookworm
ports:
- 6379:6379
defaults:
run:
working-directory: ./api
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Installling dependencies
run: go mod download
- name: Build
run: go build main.go
- name: Run test & coverage
run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
env:
ENV: development
PORT: 5000
MINIO_HOST: bucket:9000
MINIO_ACCESS_ID: root
MINIO_SECRET_KEY: verysecurepassword
REDIS_URL: redis://@redis:6379
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: go
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
- name: Create Sentry release
uses: getsentry/action-release@v1
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT_API}}
with:
environment: production
set_commits: skip
version: ${{ github.sha }}
- uses: codecov/codecov-action@v2
with:
flags: api
client-build:
name: Client
runs-on: ubuntu-latest
container: node:18.17.1-bookworm
timeout-minutes: 15
defaults:
run:
working-directory: ./client
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Installling dependencies
run: npm install
- name: Lint
run: npm run lint
- name: Build
run: npm run build
env:
VITE_SENTRY_DSN: https://examplePublicKey@o0.ingest.sentry.io/0
VITE_NODE_ENV: development
VITE_API_ENDPOINT: https://jokesbapak2.reinaldyrafli.com/api/v1
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: javascript
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
- name: Create Sentry release
uses: getsentry/action-release@v1
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT_CLIENT }}
with:
environment: production
set_commits: skip
version: ${{ github.sha }}

View File

@ -1,106 +1,106 @@
name: PR
on:
pull_request:
branches: [ "*" ]
jobs:
client-build:
name: Client
runs-on: ubuntu-latest
container: node:18.17.1-bookworm
timeout-minutes: 15
defaults:
run:
working-directory: ./client
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: npm install
- name: Lint
run: npx eslint --ext .svelte,.js,.ts --ignore-path .gitignore .
- name: Formatting
run: npx prettier --check --ignore-path .gitignore --plugin-search-dir=. "./**/*.(ts|json|js|svelte)"
- name: Build
run: npm run build
env:
VITE_SENTRY_DSN: https://examplePublicKey@o0.ingest.sentry.io/0
VITE_NODE_ENV: development
VITE_API_ENDPOINT: https://jokesbapak2.reinaldyrafli.com/api/v1
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: javascript
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
api-build:
name: API
runs-on: ubuntu-latest
container: golang:1.21.0-bookworm
timeout-minutes: 15
services:
bucket:
image: minio/minio:edge-cicd
env:
MINIO_ROOT_USER: root
MINIO_ROOT_PASSWORD: verysecurepassword
MINIO_ACCESS_KEY: minio_access_key
MINIO_SECRET_KEY: minio_access_key
ports:
- 9000:9000
options: >-
--health-cmd "curl -f http://bucket:9000/minio/health/live"
--health-interval 45s
--health-timeout 30s
--health-retries 10
--health-start-period 120s
volumes:
- minio-data:/data
redis:
image: redis:7.0.12-bookworm
ports:
- 6379:6379
defaults:
run:
working-directory: ./api
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Installling dependencies
run: go mod download
- name: Build
run: go build main.go
- name: Run test & coverage
run: go test -v -coverprofile=coverage.out -covermode=atomic ./...
env:
ENV: development
PORT: 5000
MINIO_HOST: bucket:9000
MINIO_ACCESS_ID: root
MINIO_SECRET_KEY: verysecurepassword
REDIS_URL: redis://@redis:6379
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: go
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
- uses: codecov/codecov-action@v4
with:
flags: api
name: PR
on:
pull_request:
branches: [ "*" ]
jobs:
client-build:
name: Client
runs-on: ubuntu-latest
container: node:18.17.1-bookworm
timeout-minutes: 15
defaults:
run:
working-directory: ./client
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install dependencies
run: npm install
- name: Lint
run: npx eslint --ext .svelte,.js,.ts --ignore-path .gitignore .
- name: Formatting
run: npx prettier --check --ignore-path .gitignore --plugin-search-dir=. "./**/*.(ts|json|js|svelte)"
- name: Build
run: npm run build
env:
VITE_SENTRY_DSN: https://examplePublicKey@o0.ingest.sentry.io/0
VITE_NODE_ENV: development
VITE_API_ENDPOINT: https://jokesbapak2.reinaldyrafli.com/api/v1
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: javascript
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
api-build:
name: API
runs-on: ubuntu-latest
container: golang:1.21.0-bookworm
timeout-minutes: 15
services:
bucket:
image: minio/minio:edge-cicd
env:
MINIO_ROOT_USER: root
MINIO_ROOT_PASSWORD: verysecurepassword
MINIO_ACCESS_KEY: minio_access_key
MINIO_SECRET_KEY: minio_access_key
ports:
- 9000:9000
options: >-
--health-cmd "curl -f http://bucket:9000/minio/health/live"
--health-interval 45s
--health-timeout 30s
--health-retries 10
--health-start-period 120s
volumes:
- minio-data:/data
redis:
image: redis:7.0.12-bookworm
ports:
- 6379:6379
defaults:
run:
working-directory: ./api
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Installling dependencies
run: go mod download
- name: Build
run: go build main.go
- name: Run test & coverage
run: go test -v -coverprofile=coverage.out -covermode=atomic ./...
env:
ENV: development
PORT: 5000
MINIO_HOST: bucket:9000
MINIO_ACCESS_ID: root
MINIO_SECRET_KEY: verysecurepassword
REDIS_URL: redis://@redis:6379
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: go
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
- uses: codecov/codecov-action@v2
with:
flags: api

168
README.md
View File

@ -1,85 +1,85 @@
<h1 align="center">
<br>
<img src=".github/images/header_github.png" alt="Jokes Bapak2 Heading" width="1000">
<br>
<br>
Jokes Bapak2 Image API
<br>
</h1>
👋 Hey there! Always a work in progress, if you'd like to contribute this while this repo is still growing, that would be
so great!
ou can access the front facing web on [jokesbapak2.reinaldyrafli.com](http://jokesbapak2.reinaldyrafli.com/).
## Brief explanation of what is this
Jokes Bapak2 is an image API that you can use for free! I've been seeing lots and lots of Indonesian dad jokes on
Twitter, Facebook and Instagram on early 2020. In a month, I made a Discord bot that provides the jokes. But I thought,
why not make it as an API?
This is some kind of [icanhazdadjokes](https://icanhazdadjoke.com/) but it's Indonesian and it's not text, it's images.
Dad jokes in Indonesia is somewhat a bit different than in US/UK because, I guess, here, it's a lot dumber.
## Project Directories
* `api` - REST API service. Created with Go with [Fiber](https://gofiber.io/) framework.
* `client` - Front facing website (front end). Created with [Svelte Kit](https://kit.svelte.dev/).
You can consume this API via a website (linked in the front facing web) with a few endpoints:
* `/` - Random jokes bapak2
* `/id/{number}` - Jokes bapak2 based on ID
* `/today` - Jokes bapak2 of the day
* `/total` - Total available jokes bapak2
Currently I'm (still) searching for an alternative for AWS S3 that I can use for free.
## Tech stacks
* Go (for `api` / back end)
* Node.js (for `client` / front end)
* Postgres
* Redis
That's it.
## Development
Two ways of doing this:
1. Install all the tech stack on your local machine
2. Using docker-compose
See [CONTRIBUTING](./CONTRIBUTING.md) or README files on each project directory for further instruction on how to run
the development environment.
## Thanks to
* [Teknologi Umum](https://t.me/teknologi_umum)
* [Ronny Gunawan](https://github.com/ronnygunawan) for the caching concept & ideas
* [artileda](https://github.com/artileda) for the jokes submission
* [elianiva](https://github.com/elianiva) for solving my SvelteKit problems
* [kokizzu](https://github.com/kokizzu) for the dependency injection concept & ideas
## License
Jokes Bapak2 API is licensed under [GNU GENERAL PUBLIC LICENSE v3 license](./LICENSE)
```
Jokes Bapak2 API is a free-to-use image API of Indonesian dad jokes.
Copyright (C) 2021-present Reinaldy Rafli <aldy505@proton.me>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
<h1 align="center">
<br>
<img src=".github/images/header_github.png" alt="Jokes Bapak2 Heading" width="1000">
<br>
<br>
Jokes Bapak2 Image API
<br>
</h1>
👋 Hey there! Always a work in progress, if you'd like to contribute this while this repo is still growing, that would be
so great!
ou can access the front facing web on [jokesbapak2.reinaldyrafli.com](http://jokesbapak2.reinaldyrafli.com/).
## Brief explanation of what is this
Jokes Bapak2 is an image API that you can use for free! I've been seeing lots and lots of Indonesian dad jokes on
Twitter, Facebook and Instagram on early 2020. In a month, I made a Discord bot that provides the jokes. But I thought,
why not make it as an API?
This is some kind of [icanhazdadjokes](https://icanhazdadjoke.com/) but it's Indonesian and it's not text, it's images.
Dad jokes in Indonesia is somewhat a bit different than in US/UK because, I guess, here, it's a lot dumber.
## Project Directories
* `api` - REST API service. Created with Go with [Fiber](https://gofiber.io/) framework.
* `client` - Front facing website (front end). Created with [Svelte Kit](https://kit.svelte.dev/).
You can consume this API via a website (linked in the front facing web) with a few endpoints:
* `/` - Random jokes bapak2
* `/id/{number}` - Jokes bapak2 based on ID
* `/today` - Jokes bapak2 of the day
* `/total` - Total available jokes bapak2
Currently I'm (still) searching for an alternative for AWS S3 that I can use for free.
## Tech stacks
* Go (for `api` / back end)
* Node.js (for `client` / front end)
* Postgres
* Redis
That's it.
## Development
Two ways of doing this:
1. Install all the tech stack on your local machine
2. Using docker-compose
See [CONTRIBUTING](./CONTRIBUTING.md) or README files on each project directory for further instruction on how to run
the development environment.
## Thanks to
* [Teknologi Umum](https://t.me/teknologi_umum)
* [Ronny Gunawan](https://github.com/ronnygunawan) for the caching concept & ideas
* [artileda](https://github.com/artileda) for the jokes submission
* [elianiva](https://github.com/elianiva) for solving my SvelteKit problems
* [kokizzu](https://github.com/kokizzu) for the dependency injection concept & ideas
## License
Jokes Bapak2 API is licensed under [GNU GENERAL PUBLIC LICENSE v3 license](./LICENSE)
```
Jokes Bapak2 API is a free-to-use image API of Indonesian dad jokes.
Copyright (C) 2021-present Jokes Bapak2 Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
```

View File

@ -1,4 +1,4 @@
NODE_ENV=development
SERVER_API_ENDPOINT=
BROWSER_API_ENDPOINT=
SENTRY_DSN=
VITE_NODE_ENV=development
VITE_SERVER_API_ENDPOINT=
VITE_BROWSER_API_ENDPOINT=
VITE_SENTRY_DSN=

View File

@ -1,24 +1,19 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier', "plugin:astro/recommended",],
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier', 'plugin:svelte/prettier'],
plugins: ['@typescript-eslint'],
ignorePatterns: ['*.cjs'],
settings: {
'svelte3/typescript': () => require('typescript')
},
overrides: [
{
// Define the configuration for `.astro` file.
files: ["*.astro"],
// Allows Astro components to be parsed.
parser: "astro-eslint-parser",
// Parse the script in `.astro` as TypeScript by adding the following configuration.
// It's the setting you need when using TypeScript.
files: ["*.svelte"],
parser: "svelte-eslint-parser",
// Parse the `<script>` in `.svelte` as TypeScript by adding the following configuration.
parserOptions: {
parser: "@typescript-eslint/parser",
extraFileExtensions: [".astro"],
},
rules: {
// override/add rules settings here, such as:
// "astro/no-set-html-directive": "error"
},
},
],

View File

@ -1 +1 @@
v20
v18.17

View File

@ -6,5 +6,8 @@
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 120
"printWidth": 120,
"plugins": [
"prettier-plugin-svelte"
]
}

View File

@ -1,15 +0,0 @@
import { defineConfig } from 'astro/config';
import UnoCSS from '@unocss/astro'
// https://astro.build/config
export default defineConfig({
i18n: {
defaultLocale: "en",
locales: ["en", "id"]
},
integrations: [
UnoCSS({
injectReset: true
}),
],
});

8366
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,38 +5,45 @@
"contributors": [
{
"name": "Reinaldy Rafli",
"email": "aldy505@proton.me",
"email": "aldy505@tutanota.com",
"url": "https://github.com/aldy505"
}
],
"type": "module",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro",
"lint": "eslint --fix --ext .astro,.js,.ts --ignore-path .gitignore .",
"format": "prettier --write --ignore-path .gitignore --plugin-search-dir=. \"./**/*.(ts|json|js|astro)\""
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint --fix --ext .svelte,.js,.ts --ignore-path .gitignore .",
"format": "prettier --write --ignore-path .gitignore --plugin-search-dir=. \"./**/*.(ts|json|js|svelte)\""
},
"devDependencies": {
"@sveltejs/adapter-node": "1.3.1",
"@sveltejs/kit": "^1.22.5",
"@typescript-eslint/eslint-plugin": "6.3.0",
"@typescript-eslint/parser": "6.3.0",
"cssnano": "6.0.1",
"eslint": "8.47.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-astro": "^0.31.4",
"eslint-plugin-svelte": "2.32.4",
"prettier": "3.0.1",
"typescript": "5.3.3"
"prettier-plugin-svelte": "3.0.3",
"svelte": "4.2.0",
"svelte-check": "3.5.0",
"svelte-preprocess": "5.0.4",
"svelte-windicss-preprocess": "4.2.8",
"tslib": "2.6.1",
"typescript": "5.1.6",
"vite": "^4.4.9"
},
"dependencies": {
"@astrojs/check": "^0.5.2",
"@fontsource/fira-mono": "5.0.8",
"@fontsource/rubik": "5.0.8",
"@sentry/astro": "^7.100.1",
"@unocss/astro": "^0.58.5",
"@unocss/reset": "^0.58.5",
"astro": "^4.3.5",
"unocss": "^0.58.5"
"@sentry/browser": "7.63.0",
"dotenv": "16.3.1",
"ohmyfetch": "0.4.21",
"svelte-i18n": "3.7.0"
}
}

View File

@ -1,19 +1,13 @@
---
import { getAbsoluteLocaleUrl } from "astro:i18n";
import * as indonesianTranslation from "../languages/id.json";
import * as englishTranslation from "../languages/en.json";
let open = false;
let duration = 0.4;
let burgerColor = 'rgb(18.4, 18.4, 18.4)';
let menuColor = 'rgb(180, 180, 180)';
<script lang="ts">
import { goto } from '$app/navigation';
import { _ } from 'svelte-i18n';
---
<script>
export function goto(path: string) {
getAbsoluteLocaleUrl()
}
let open = false;
let duration = 0.4;
let burgerColor = 'rgb(18.4, 18.4, 18.4)';
let menuColor = 'rgb(180, 180, 180)';
</script>
<nav class="flex flex-row py-6 font-body items-center">
<div class="hover:cursor-pointer flex-5 font-bold text-2xl" on:click={() => goto('/')}>Jokesbapak2</div>
<div class="navigation-item" on:click={() => goto('/why')}>{$_('navigation.why')}</div>
@ -54,14 +48,14 @@ export function goto(path: string) {
</div>
</nav>
{open &&
{#if open}
<menu
class="top-of-the-world dark:bg-gray-900 dark:text-white bg-lavender-200 bg-gradient-to-br to-lavender-400 dark:to-lavender-900 text-black w-full h-full overscroll-none"
>
<div class="container -pr-10">
<div class="flex flex-col items-center content-center text-center pt-20">
<div
onclick={() => {
on:click={() => {
open = false;
return goto('/');
}}
@ -99,7 +93,7 @@ export function goto(path: string) {
</div>
</div>
</menu>
}
{/if}
<style>
.navigation-item {

View File

@ -1,6 +1,6 @@
---
let emoji = '';
---
<script lang="ts">
export let emoji = '';
</script>
<div
class="bg-chetwode-200 dark:bg-chetwode-800 font-body px-4 md:px-8 py-6 md:my-8 rounded-lg text-black dark:text-white shadow-xl bg-gradient-to-br from-transparent to-chetwode-300 dark:to-chetwode-900"

1
client/src/env.d.ts vendored
View File

@ -1 +0,0 @@
/// <reference types="astro/client" />

1
client/src/global.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="@sveltejs/kit" />

View File

@ -0,0 +1,7 @@
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = ({ event, resolve }) => {
return resolve(event, {
transformPageChunk: ({ html }) => html.replace('%lang%', 'en'),
});
};

View File

@ -1,100 +0,0 @@
---
import '@fontsource/fira-mono';
import '@fontsource/rubik';
import Navbar from '../components/Navbar.astro';
const { lang } = Astro.props;
const currentPath = Astro.url.path.replace("/en/", "/").replace("/id/", "/");
const translations = {
id: {
footer: {
"made": "Diciptakan dengan",
"indonesia": "di Indonesia",
"available": "Proyek ini tersedia di",
"github": "Github",
"language": "Bahasa:",
"english": "Inggris",
"indonesian": "Indonesia"
}
},
en: {
footer: {
"made": "Made with",
"indonesia": "in Indonesia",
"available": "This project is available on",
"github": "Github",
"language": "Languages:",
"english": "English",
"indonesian": "Indonesian"
}
},
};
---
<!DOCTYPE html>
<html lang={lang}>
<head>
<meta charset="utf-8"/>
<link href="/favicon.png" rel="icon"/>
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<meta content="jokesbapak2, jokes, bapak2, indonesian, dad, jokes, api, rest" name="keywords"/>
<meta content="Reinaldy Rafli" name="author"/>
<meta content="#f4a9f5" name="theme-color"/>
<meta content="https://jokesbapak2.reinaldyrafli.com/" name="publisher"/>
<meta content="/social.jpg" property="og:image"/>
<meta content="Jokesbapak2 - Largest collection of Indonesian dad jokes" property="og:image:alt"/>
<meta content="/social.jpg" property="og:image:secure_url"/>
<meta content="1280" property="og:image:width"/>
<meta content="640" property="og:image:height"/>
<meta content="en_US" property="og:locale"/>
<meta content="website" property="og:type"/>
<meta content="summary_large_image" name="twitter:card"/>
<meta content="/social.jpg" name="twitter:image:src"/>
<meta content="/social.jpg" name="twitter:image"/>
<meta content="Reinaldy Rafli" name="twitter:creator"/>
<link href="/favicon.png" rel="icon" type="image/png">
<link href="/favicon.svg" rel="icon" type="image/svg">
%sveltekit.head%
</head>
<body>
<div
class="bg-gradient-to-br from-transparent to-lavender-300 dark:(bg-gray-900 to-lavender-900 text-white) min-h-screen h-full w-full"
>
<div class="container mx-auto xl:px-40 lg:px-28 md:px-20 sm:px-12 px-8 w-full">
<header>
<Navbar />
</header>
<main class="font-body">
<slot />
</main>
<footer class="font-body py-8 w-full md:w-1/2 lg:w-2/5">
<div class="flex flex-col md:flex-row flex-wrap">
<div class="flex-initial pr-3">
<p class="text-sm opacity-50 hover:opacity-90 transition duration-300 ease-in-out inline-block">
{translations[lang].footer.made} <span class="text-red-500">&#10084;</span>
{translations[lang].footer.indonesia}.
</p>
</div>
<div class="flex-initial pr-3">
<p class="text-sm opacity-50 hover:opacity-90 transition duration-300 ease-in-out inline-block">
{translations[lang].footer.available}
<a class="hover:underline" href="https://www.github.com/aldy505/jokes-bapak2">{translations[lang].footer.github}</a>.
</p>
</div>
<div class="flex-initial pr-3">
<p class="text-sm opacity-50 hover:opacity-90 transition duration-300 ease-in-out inline-block">
{translations[lang].footer.language}
<a class="hover:underline" href={`/en/${currentPath}`} target="_top">{translations[lang].footer.english}</a>
|
<a class="hover:underline" href={`/id/${currentPath}`} target="_top">{translations[lang].footer.indonesian}</a>.
</p>
</div>
</div>
</footer>
</div>
</div>
</body>
</html>

6
client/src/lib/env.ts Normal file
View File

@ -0,0 +1,6 @@
export default {
SERVER_API_ENDPOINT: import.meta.env.VITE_SERVER_API_ENDPOINT || 'http://localhost:5000',
BROWSER_API_ENDPOINT: import.meta.env.VITE_BROWSER_API_ENDPOINT || 'https://jokesbapak2.reinaldyrafli.com',
SENTRY_DSN: import.meta.env.VITE_SENTRY_DSN || '',
NODE_ENV: import.meta.env.VITE_NODE_ENV || 'development',
};

15
client/src/lib/locale.ts Normal file
View File

@ -0,0 +1,15 @@
import { addMessages, getLocaleFromNavigator, getLocaleFromQueryString, init } from 'svelte-i18n';
import en from '../languages/en.json';
import id from '../languages/id.json';
addMessages('en', en);
addMessages('en-US', en);
addMessages('en-GB', en);
addMessages('id', id);
addMessages('id-ID', id);
init({
fallbackLocale: 'en',
initialLocale: getLocaleFromQueryString('lang') || getLocaleFromNavigator(),
});

10
client/src/lib/logging.ts Normal file
View File

@ -0,0 +1,10 @@
import * as Sentry from '@sentry/browser';
import env from './env';
Sentry.init({
dsn: String(env.SENTRY_DSN) || '',
enabled: String(env.NODE_ENV) === 'production',
tracesSampleRate: 0.5,
});
export default Sentry;

View File

@ -1,77 +0,0 @@
---
// This page is meant to explain available API endpoints.
import Codeblock from '../../components/Codeblock.astro';
import Notice from '../../components/Notice.astro';
import Layout from '../../layout/Layout.astro';
const totalJokes = async (): Promise<string> => {
const response = await fetch(`${import.meta.env.API_ENDPOINT}/total`);
const responseBody = await response.json();
return responseBody.message;
};
let total = await totalJokes();
---
<!-- <svelte:head>
<title>{$_('navigation.api')} - {$_('meta.title')}</title>
<meta content={$_('navigation.api') + '-' + $_('meta.title')} name="title" />
<meta content={$_('navigation.api') + '-' + $_('meta.title')} name="twitter:title" />
<meta content={$_('navigation.api') + '-' + $_('meta.title')} property="og:title" />
<link href="https://jokesbapak2.reinaldyrafli.com/api" rel="canonical" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="twitter:description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" property="og:description" />
</svelte:head> -->
<Layout>
<section>
<Notice emoji="💡">
We limit the request to be 120 request/minute.
</Notice>
</section>
<section class="api_page">
<h1>Get Jokes</h1>
<h2>Get single random joke</h2>
<p>You'll get different result for every call.</p>
<Codeblock>
GET {import.meta.env.BROWSER_API_ENDPOINT}/
</Codeblock>
<h2>Get today's joke</h2>
<p>A joke a day makes more of a dad out of you.</p>
<Codeblock>
GET {import.meta.env.BROWSER_API_ENDPOINT}/today
</Codeblock>
<h2>Get joke by ID</h2>
<p>You'll get consistent joke for every call with the same ID. Where ID is a number ranging from 1 to {total}.</p>
<Codeblock>
GET {import.meta.env.BROWSER_API_ENDPOINT}/id/&lcub;id&rcub;
</Codeblock>
<h2>Get total number of jokes</h2>
<p>...in a form of JSON response.</p>
<Codeblock>
GET {import.meta.env.BROWSER_API_ENDPOINT}/total
</Codeblock>
</section>
<style>
h1 {
@apply text-4xl;
@apply font-bold;
@apply py-4;
}
h2 {
@apply text-2xl;
@apply font-bold;
@apply pt-6;
@apply pb-1;
}
p {
@apply text-base;
@apply opacity-80;
@apply py-2;
}
</style>
</Layout>

View File

@ -1,80 +0,0 @@
---
import Codeblock from '../../components/Codeblock.astro';
import Layout from '../../layout/Layout.astro';
// This page is meant to guide people on how to use the API.
const browserAPIEndpoint = import.meta.env.BROWSER_API_ENDPOINT;
---
<!-- <svelte:head>
<title>{$_('navigation.guide')} - {$_('meta.title')}</title>
<meta content={$_('navigation.guide') + '-' + $_('meta.title')} name="title" />
<meta content={$_('navigation.guide') + '-' + $_('meta.title')} name="twitter:title" />
<meta content={$_('navigation.guide') + '-' + $_('meta.title')} property="og:title" />
<link href="https://jokesbapak2.reinaldyrafli.com/guide" rel="canonical" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="twitter:description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" property="og:description" />
</svelte:head> -->
<Layout>
<section class="guide_page">
<h1>Guide</h1>
<p>
To access the API, there are a few ways to do it.
It depends on what you are trying to accomplish with it.
</p>
</section>
<section class="guide_page">
<h2>Direct request with <code>&lt;img&gt;</code> block</h2>
<Codeblock>&lt;img src="{browserAPIEndpoint}/" /&gt;</Codeblock>
</section>
<section class="guide_page">
<h2>Using fetch API</h2>
<p>You can use it, but I personally don't recommend it.</p>
<Codeblock>
const response = await fetch(&quot;{browserAPIEndpoint}/&quot;);<br />
<br />
if (!response.ok) &#123;<br />
&nbsp;&nbsp;// Do some error handling if the request fails<br />
&#125;<br />
<br />
const blob = await response.blob();<br />
<br />
const objectURL = URL.createObjectURL(blob);<br />
<br />
&lt;img src=&quot;&#123; objectURL &#125;&quot; /&gt;
</Codeblock>
</section>
<style>
p {
@apply text-base;
@apply py-2;
@apply lg\:w-2\/3;
}
h1 {
@apply text-4xl;
@apply font-bold;
@apply py-2;
}
h2 {
@apply text-2xl;
@apply font-bold;
@apply py-2;
}
a {
@apply hover\:underline;
@apply dark\:text-dodger-200;
@apply text-dodger-700;
}
section {
@apply pt-6;
}
</style>
</Layout>

View File

@ -1,44 +0,0 @@
---
import Codeblock from '../../components/Codeblock.astro';
import Layout from '../../layout/Layout.astro';
async function load() {
const response = await fetch(`${import.meta.env.SERVER_API_ENDPOINT}/total`, {
method: 'GET',
});
const responseBody = await response.json();
return {
total: responseBody.message,
};
}
/** @type {import('./$types').PageData} */
export let data;
let { total } = await load();
---
<Layout lang="en">
<section>
<div class="flex flex-col lg:flex-row items-center py-8">
<div class="flex-1">
<h1 class="text-4xl sm:text-5xl md:text-6xl font-bold py-2">{$_('meta.tagline-total', { values: { total } })}</h1>
<p class="text-base py-4 md:w-2/3">{$_('meta.explanation')}</p>
</div>
<div class="flex-1 md:px-6 w-full">
<div class="max-w-xs mx-auto">
<img alt="Sample joke" class="py-6 shadow-2xl" src={env.BROWSER_API_ENDPOINT + `/today`} />
</div>
<Codeblock>$ curl -XGET 'https://jokesbapak2.reinaldyrafli.com/api/'</Codeblock>
<p class="text-sm text-center py-4 opacity-70 hover:opacity-100 transition duration-300 ease-in-out">
{$_('home.more.1')}
<span class="hover:underline cursor-pointer" on:click={() => goto('/guide')}>{$_('navigation.guide')}</span>
{$_('home.more.2')}
<span class="hover:underline cursor-pointer" on:click={() => goto('/api')}>{$_('navigation.api')}</span>
</p>
</div>
</div>
</section>
</Layout>

View File

@ -1,71 +0,0 @@
---
import Layout from "../../layout/Layout.astro";
---
<!-- <svelte:head>
<title>{$_('navigation.why')} - {$_('meta.title')}</title>
<meta content={$_('navigation.why') + '-' + $_('meta.title')} name="title" />
<meta content={$_('navigation.why') + '-' + $_('meta.title')} name="twitter:title" />
<meta content={$_('navigation.why') + '-' + $_('meta.title')} property="og:title" />
<link href="https://jokesbapak2.reinaldyrafli.com/why" rel="canonical" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="twitter:description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" property="og:description" />
</svelte:head> -->
<Layout>
<section class="why_page">
<h1 id="why-does-this-project-exists">{$_('why.exists.title')}</h1>
<p>{$_('why.exists.body.1')} {$_('why.exists.body.2')} {$_('why.exists.body.3')} {$_('why.exists.body.4')}</p>
<p>{$_('why.exists.body.5')} {$_('why.exists.body.6')}</p>
</section>
<section class="why_page">
<h1 id="can-i-submit-my-dad-joke">{$_('why.submit.title')}</h1>
<p>
{$_('why.submit.body.1')}
{$_('why.submit.body.2')}
<a href="mailto:aldy505@proton.me">{$_('why.submit.body.3')}</a>
{$_('why.submit.body.4')}
{$_('why.submit.body.5')}
{$_('why.submit.body.6')}
</p>
</section>
<section class="why_page">
<h1 id="can-i-contribute">{$_('why.contribute.title')}</h1>
<p>
{$_('why.contribute.body.1')}
{$_('why.contribute.body.2')}
<a href="https://www.github.com/aldy505/jokes-bapak2">{$_('why.contribute.body.3')}</a>
{$_('why.contribute.body.4')}
</p>
</section>
<section class="why_page">
<h1 id="other-inquiries">{$_('why.inquiries.title')}</h1>
<p>
{$_('why.inquiries.body.1')}
<a href="mailto:aldy505@proton.me">{$_('why.inquiries.body.2')}</a>, {$_('why.inquiries.body.3')}
</p>
</section>
<style>
p {
@apply text-base;
@apply py-2;
@apply lg\:w-2\/3;
}
h1 {
@apply text-3xl;
@apply font-bold;
@apply py-2;
}
a {
@apply hover\:underline;
@apply dark\:text-dodger-200;
@apply text-dodger-700;
}
section {
@apply pt-6;
}
</style>
</Layout>

View File

@ -1,77 +0,0 @@
---
// This page is meant to explain available API endpoints.
import Codeblock from '../../components/Codeblock.astro';
import Notice from '../../components/Notice.astro';
import Layout from '../../layout/Layout.astro';
const totalJokes = async (): Promise<string> => {
const response = await fetch(`${import.meta.env.API_ENDPOINT}/total`);
const responseBody = await response.json();
return responseBody.message;
};
let total = await totalJokes();
---
<!-- <svelte:head>
<title>{$_('navigation.api')} - {$_('meta.title')}</title>
<meta content={$_('navigation.api') + '-' + $_('meta.title')} name="title" />
<meta content={$_('navigation.api') + '-' + $_('meta.title')} name="twitter:title" />
<meta content={$_('navigation.api') + '-' + $_('meta.title')} property="og:title" />
<link href="https://jokesbapak2.reinaldyrafli.com/api" rel="canonical" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="twitter:description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" property="og:description" />
</svelte:head> -->
<Layout>
<section>
<Notice emoji="💡">
{$_('api.limit')}
</Notice>
</section>
<section class="api_page">
<h1>{$_('api.get.title')}</h1>
<h2>{$_('api.get.random.title')}</h2>
<p>{$_('api.get.random.body')}</p>
<Codeblock>
GET {import.meta.env.BROWSER_API_ENDPOINT}/
</Codeblock>
<h2>{$_('api.get.today.title')}</h2>
<p>{$_('api.get.today.body')}</p>
<Codeblock>
GET {import.meta.env.BROWSER_API_ENDPOINT}/today
</Codeblock>
<h2>{$_('api.get.id.title')}</h2>
<p>{$_('api.get.id.body', { values: { total } })}</p>
<Codeblock>
GET {import.meta.env.BROWSER_API_ENDPOINT}/id/&lcub;id&rcub;
</Codeblock>
<h2>{$_('api.get.total.title')}</h2>
<p>{$_('api.get.total.body')}</p>
<Codeblock>
GET {import.meta.env.BROWSER_API_ENDPOINT}/total
</Codeblock>
</section>
<style>
h1 {
@apply text-4xl;
@apply font-bold;
@apply py-4;
}
h2 {
@apply text-2xl;
@apply font-bold;
@apply pt-6;
@apply pb-1;
}
p {
@apply text-base;
@apply opacity-80;
@apply py-2;
}
</style>
</Layout>

View File

@ -1,81 +0,0 @@
---
import Codeblock from '../../components/Codeblock.astro';
import { _ } from 'svelte-i18n';
import env from '$lib/env';
import Layout from '../../layout/Layout.astro';
// This page is meant to guide people on how to use the API.
---
<!-- <svelte:head>
<title>{$_('navigation.guide')} - {$_('meta.title')}</title>
<meta content={$_('navigation.guide') + '-' + $_('meta.title')} name="title" />
<meta content={$_('navigation.guide') + '-' + $_('meta.title')} name="twitter:title" />
<meta content={$_('navigation.guide') + '-' + $_('meta.title')} property="og:title" />
<link href="https://jokesbapak2.reinaldyrafli.com/guide" rel="canonical" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="twitter:description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" property="og:description" />
</svelte:head> -->
<Layout>
<section class="guide_page">
<h1>{$_('navigation.guide')}</h1>
<p>
{$_('guide.introduction.1')}
{$_('guide.introduction.2')}
</p>
</section>
<section class="guide_page">
<h2>{$_('guide.direct.1')} <code>&lt;img&gt;</code> {$_('guide.direct.2')}</h2>
<Codeblock>&lt;img src="{env.BROWSER_API_ENDPOINT}/" /&gt;</Codeblock>
</section>
<section class="guide_page">
<h2>{$_('guide.fetch.1')}</h2>
<p>{$_('guide.fetch.2')}</p>
<Codeblock>
const response = await fetch(&quot;{env.BROWSER_API_ENDPOINT}/&quot;);<br />
<br />
if (!response.ok) &#123;<br />
&nbsp;&nbsp;// {$_('guide.fetch.3')}<br />
&#125;<br />
<br />
const blob = await response.blob();<br />
<br />
const objectURL = URL.createObjectURL(blob);<br />
<br />
&lt;img src=&quot;&#123; objectURL &#125;&quot; /&gt;
</Codeblock>
</section>
<style>
p {
@apply text-base;
@apply py-2;
@apply lg\:w-2\/3;
}
h1 {
@apply text-4xl;
@apply font-bold;
@apply py-2;
}
h2 {
@apply text-2xl;
@apply font-bold;
@apply py-2;
}
a {
@apply hover\:underline;
@apply dark\:text-dodger-200;
@apply text-dodger-700;
}
section {
@apply pt-6;
}
</style>
</Layout>

View File

@ -1,44 +0,0 @@
---
import Codeblock from '../../components/Codeblock.astro';
import Layout from '../../layout/Layout.astro';
async function load() {
const response = await fetch(`${import.meta.env.SERVER_API_ENDPOINT}/total`, {
method: 'GET',
});
const responseBody = await response.json();
return {
total: responseBody.message,
};
}
/** @type {import('./$types').PageData} */
export let data;
let { total } = await load();
---
<Layout lang="en">
<section>
<div class="flex flex-col lg:flex-row items-center py-8">
<div class="flex-1">
<h1 class="text-4xl sm:text-5xl md:text-6xl font-bold py-2">{$_('meta.tagline-total', { values: { total } })}</h1>
<p class="text-base py-4 md:w-2/3">{$_('meta.explanation')}</p>
</div>
<div class="flex-1 md:px-6 w-full">
<div class="max-w-xs mx-auto">
<img alt="Sample joke" class="py-6 shadow-2xl" src={env.BROWSER_API_ENDPOINT + `/today`} />
</div>
<Codeblock>$ curl -XGET 'https://jokesbapak2.reinaldyrafli.com/api/'</Codeblock>
<p class="text-sm text-center py-4 opacity-70 hover:opacity-100 transition duration-300 ease-in-out">
{$_('home.more.1')}
<span class="hover:underline cursor-pointer" on:click={() => goto('/guide')}>{$_('navigation.guide')}</span>
{$_('home.more.2')}
<span class="hover:underline cursor-pointer" on:click={() => goto('/api')}>{$_('navigation.api')}</span>
</p>
</div>
</div>
</section>
</Layout>

View File

@ -1,71 +0,0 @@
---
import Layout from "../../layout/Layout.astro";
---
<!-- <svelte:head>
<title>{$_('navigation.why')} - {$_('meta.title')}</title>
<meta content={$_('navigation.why') + '-' + $_('meta.title')} name="title" />
<meta content={$_('navigation.why') + '-' + $_('meta.title')} name="twitter:title" />
<meta content={$_('navigation.why') + '-' + $_('meta.title')} property="og:title" />
<link href="https://jokesbapak2.reinaldyrafli.com/why" rel="canonical" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="twitter:description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" property="og:description" />
</svelte:head> -->
<Layout>
<section class="why_page">
<h1 id="why-does-this-project-exists">{$_('why.exists.title')}</h1>
<p>{$_('why.exists.body.1')} {$_('why.exists.body.2')} {$_('why.exists.body.3')} {$_('why.exists.body.4')}</p>
<p>{$_('why.exists.body.5')} {$_('why.exists.body.6')}</p>
</section>
<section class="why_page">
<h1 id="can-i-submit-my-dad-joke">{$_('why.submit.title')}</h1>
<p>
{$_('why.submit.body.1')}
{$_('why.submit.body.2')}
<a href="mailto:aldy505@proton.me">{$_('why.submit.body.3')}</a>
{$_('why.submit.body.4')}
{$_('why.submit.body.5')}
{$_('why.submit.body.6')}
</p>
</section>
<section class="why_page">
<h1 id="can-i-contribute">{$_('why.contribute.title')}</h1>
<p>
{$_('why.contribute.body.1')}
{$_('why.contribute.body.2')}
<a href="https://www.github.com/aldy505/jokes-bapak2">{$_('why.contribute.body.3')}</a>
{$_('why.contribute.body.4')}
</p>
</section>
<section class="why_page">
<h1 id="other-inquiries">{$_('why.inquiries.title')}</h1>
<p>
{$_('why.inquiries.body.1')}
<a href="mailto:aldy505@proton.me">{$_('why.inquiries.body.2')}</a>, {$_('why.inquiries.body.3')}
</p>
</section>
<style>
p {
@apply text-base;
@apply py-2;
@apply lg\:w-2\/3;
}
h1 {
@apply text-3xl;
@apply font-bold;
@apply py-2;
}
a {
@apply hover\:underline;
@apply dark\:text-dodger-200;
@apply text-dodger-700;
}
section {
@apply pt-6;
}
</style>
</Layout>

View File

@ -1 +0,0 @@
<meta http-equiv="refresh" content="0;url=/en/" />

View File

@ -0,0 +1,19 @@
<script context="module" lang="ts">
import type { ErrorLoad } from '@sveltejs/kit';
import Sentry from '$lib/logging';
export const load: ErrorLoad = ({ error }) => {
Sentry.captureException(error);
return {};
};
</script>
<script lang="ts">
import { goto } from '$app/navigation';
import { _ } from 'svelte-i18n';
</script>
<section>
<h1 class="text-5xl font-bold">{$_('error.heading')}</h1>
<p class="text-base" on:click={() => goto('/')}>{$_('error.homepage')}</p>
</section>

View File

@ -0,0 +1,55 @@
<script context="module" lang="ts">
import '$lib/locale';
</script>
<script lang="ts">
import '@fontsource/fira-mono';
import '@fontsource/rubik';
import { _ } from 'svelte-i18n';
import Navbar from '../components/navbar.svelte';
</script>
<svelte:head>
<meta content={$_('meta.title')} property="og:site_name" />
</svelte:head>
<div
class="bg-gradient-to-br from-transparent to-lavender-300 dark:(bg-gray-900 to-lavender-900 text-white) min-h-screen h-full w-full"
>
<div class="container mx-auto xl:px-40 lg:px-28 md:px-20 sm:px-12 px-8 w-full">
<header>
<Navbar />
</header>
<main class="font-body">
<slot />
</main>
<footer class="font-body py-8 w-full md:w-1/2 lg:w-2/5">
<div class="flex flex-col md:flex-row flex-wrap">
<div class="flex-initial pr-3">
<p class="text-sm opacity-50 hover:opacity-90 transition duration-300 ease-in-out inline-block">
{$_('footer.made')} <span class="text-red-500">&#10084;</span>
{$_('footer.indonesia')}.
</p>
</div>
<div class="flex-initial pr-3">
<p class="text-sm opacity-50 hover:opacity-90 transition duration-300 ease-in-out inline-block">
{$_('footer.available')}
<a class="hover:underline" href="https://www.github.com/aldy505/jokes-bapak2">{$_('footer.github')}</a>.
</p>
</div>
<div class="flex-initial pr-3">
<p class="text-sm opacity-50 hover:opacity-90 transition duration-300 ease-in-out inline-block">
{$_('footer.language')}
<a class="hover:underline" href="?lang=en" target="_top">{$_('footer.english')}</a>
|
<a class="hover:underline" href="?lang=id" target="_top">{$_('footer.indonesian')}</a>.
</p>
</div>
</div>
</footer>
</div>
</div>
<style windi:global windi:preflights:global windi:safelist:global>
</style>

View File

@ -0,0 +1,19 @@
import { $fetch } from 'ohmyfetch';
import env from '../lib/env';
interface TotalResponse {
message: number;
}
/** @type {import('./$types').PageServerLoad} */
export async function load() {
const response = await $fetch<TotalResponse>('total', {
method: 'GET',
baseURL: env.SERVER_API_ENDPOINT,
parseResponse: JSON.parse,
});
return {
total: response.message,
};
}

View File

@ -0,0 +1,43 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { goto } from '$app/navigation';
import env from '$lib/env';
import Codeblock from '../components/codeblock.svelte';
/** @type {import('./$types').PageData} */
export let data;
let total = data.total;
</script>
<svelte:head>
<title>{$_('meta.title')} - {$_('meta.tagline')}</title>
<meta content={$_('meta.title') + '-' + $_('meta.tagline')} name="title" />
<meta content={$_('meta.title') + '-' + $_('meta.tagline')} name="twitter:title" />
<meta content={$_('meta.title') + '-' + $_('meta.tagline')} property="og:title" />
<link href="https://jokesbapak2.reinaldyrafli.com/" rel="canonical" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="twitter:description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" property="og:description" />
</svelte:head>
<section>
<div class="flex flex-col lg:flex-row items-center py-8">
<div class="flex-1">
<h1 class="text-4xl sm:text-5xl md:text-6xl font-bold py-2">{$_('meta.tagline-total', { values: { total } })}</h1>
<p class="text-base py-4 md:w-2/3">{$_('meta.explanation')}</p>
</div>
<div class="flex-1 md:px-6 w-full">
<div class="max-w-xs mx-auto">
<img alt="Sample joke" class="py-6 shadow-2xl" src={env.BROWSER_API_ENDPOINT + `/today`} />
</div>
<Codeblock>$ curl -XGET 'https://jokesbapak2.reinaldyrafli.com/api/'</Codeblock>
<p class="text-sm text-center py-4 opacity-70 hover:opacity-100 transition duration-300 ease-in-out">
{$_('home.more.1')}
<span class="hover:underline cursor-pointer" on:click={() => goto('/guide')}>{$_('navigation.guide')}</span>
{$_('home.more.2')}
<span class="hover:underline cursor-pointer" on:click={() => goto('/api')}>{$_('navigation.api')}</span>
</p>
</div>
</div>
</section>

View File

@ -0,0 +1 @@
export const ssr = true;

View File

@ -0,0 +1,86 @@
<script lang="ts">
// This page is meant to explain available API endpoints.
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import env from '$lib/env';
import { $fetch as omf } from 'ohmyfetch';
import Codeblock from '../../components/codeblock.svelte';
import Notice from '../../components/notice.svelte';
interface TotalResponse {
message: string;
}
let total;
onMount(async () => {
const totalJokes = async (): Promise<string> => {
const response = await omf<TotalResponse>(`${env.API_ENDPOINT}/total`);
return response.message;
};
total = await totalJokes();
});
</script>
<svelte:head>
<title>{$_('navigation.api')} - {$_('meta.title')}</title>
<meta content={$_('navigation.api') + '-' + $_('meta.title')} name="title" />
<meta content={$_('navigation.api') + '-' + $_('meta.title')} name="twitter:title" />
<meta content={$_('navigation.api') + '-' + $_('meta.title')} property="og:title" />
<link href="https://jokesbapak2.reinaldyrafli.com/api" rel="canonical" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="twitter:description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" property="og:description" />
</svelte:head>
<section>
<Notice emoji="💡">
{$_('api.limit')}
</Notice>
</section>
<section class="api_page">
<h1>{$_('api.get.title')}</h1>
<h2>{$_('api.get.random.title')}</h2>
<p>{$_('api.get.random.body')}</p>
<Codeblock>
GET {env.BROWSER_API_ENDPOINT}/
</Codeblock>
<h2>{$_('api.get.today.title')}</h2>
<p>{$_('api.get.today.body')}</p>
<Codeblock>
GET {env.BROWSER_API_ENDPOINT}/today
</Codeblock>
<h2>{$_('api.get.id.title')}</h2>
<p>{$_('api.get.id.body', { values: { total } })}</p>
<Codeblock>
GET {env.BROWSER_API_ENDPOINT}/id/&lcub;id&rcub;
</Codeblock>
<h2>{$_('api.get.total.title')}</h2>
<p>{$_('api.get.total.body')}</p>
<Codeblock>
GET {env.BROWSER_API_ENDPOINT}/total
</Codeblock>
</section>
<style>
h1 {
@apply text-4xl;
@apply font-bold;
@apply py-4;
}
h2 {
@apply text-2xl;
@apply font-bold;
@apply pt-6;
@apply pb-1;
}
p {
@apply text-base;
@apply opacity-80;
@apply py-2;
}
</style>

View File

@ -0,0 +1,78 @@
<script lang="ts">
import Codeblock from '../../components/codeblock.svelte';
import { _ } from 'svelte-i18n';
import env from '$lib/env';
// This page is meant to guide people on how to use the API.
</script>
<svelte:head>
<title>{$_('navigation.guide')} - {$_('meta.title')}</title>
<meta content={$_('navigation.guide') + '-' + $_('meta.title')} name="title" />
<meta content={$_('navigation.guide') + '-' + $_('meta.title')} name="twitter:title" />
<meta content={$_('navigation.guide') + '-' + $_('meta.title')} property="og:title" />
<link href="https://jokesbapak2.reinaldyrafli.com/guide" rel="canonical" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="twitter:description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" property="og:description" />
</svelte:head>
<section class="guide_page">
<h1>{$_('navigation.guide')}</h1>
<p>
{$_('guide.introduction.1')}
{$_('guide.introduction.2')}
</p>
</section>
<section class="guide_page">
<h2>{$_('guide.direct.1')} <code>&lt;img&gt;</code> {$_('guide.direct.2')}</h2>
<Codeblock>&lt;img src="{env.BROWSER_API_ENDPOINT}/" /&gt;</Codeblock>
</section>
<section class="guide_page">
<h2>{$_('guide.fetch.1')}</h2>
<p>{$_('guide.fetch.2')}</p>
<Codeblock>
const response = await fetch(&quot;{env.BROWSER_API_ENDPOINT}/&quot;);<br />
<br />
if (!response.ok) &#123;<br />
&nbsp;&nbsp;// {$_('guide.fetch.3')}<br />
&#125;<br />
<br />
const blob = await response.blob();<br />
<br />
const objectURL = URL.createObjectURL(blob);<br />
<br />
&lt;img src=&quot;&#123; objectURL &#125;&quot; /&gt;
</Codeblock>
</section>
<style>
p {
@apply text-base;
@apply py-2;
@apply lg\:w-2\/3;
}
h1 {
@apply text-4xl;
@apply font-bold;
@apply py-2;
}
h2 {
@apply text-2xl;
@apply font-bold;
@apply py-2;
}
a {
@apply hover\:underline;
@apply dark\:text-dodger-200;
@apply text-dodger-700;
}
section {
@apply pt-6;
}
</style>

View File

@ -0,0 +1,71 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
</script>
<svelte:head>
<title>{$_('navigation.why')} - {$_('meta.title')}</title>
<meta content={$_('navigation.why') + '-' + $_('meta.title')} name="title" />
<meta content={$_('navigation.why') + '-' + $_('meta.title')} name="twitter:title" />
<meta content={$_('navigation.why') + '-' + $_('meta.title')} property="og:title" />
<link href="https://jokesbapak2.reinaldyrafli.com/why" rel="canonical" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" name="twitter:description" />
<meta content="Largest collection of Indonesian dad jokes as a consumable API" property="og:description" />
</svelte:head>
<section class="why_page">
<h1 id="why-does-this-project-exists">{$_('why.exists.title')}</h1>
<p>{$_('why.exists.body.1')} {$_('why.exists.body.2')} {$_('why.exists.body.3')} {$_('why.exists.body.4')}</p>
<p>{$_('why.exists.body.5')} {$_('why.exists.body.6')}</p>
</section>
<section class="why_page">
<h1 id="can-i-submit-my-dad-joke">{$_('why.submit.title')}</h1>
<p>
{$_('why.submit.body.1')}
{$_('why.submit.body.2')}
<a href="mailto:aldy505@tutanota.com">{$_('why.submit.body.3')}</a>
{$_('why.submit.body.4')}
{$_('why.submit.body.5')}
{$_('why.submit.body.6')}
</p>
</section>
<section class="why_page">
<h1 id="can-i-contribute">{$_('why.contribute.title')}</h1>
<p>
{$_('why.contribute.body.1')}
{$_('why.contribute.body.2')}
<a href="https://www.github.com/aldy505/jokes-bapak2">{$_('why.contribute.body.3')}</a>
{$_('why.contribute.body.4')}
</p>
</section>
<section class="why_page">
<h1 id="other-inquiries">{$_('why.inquiries.title')}</h1>
<p>
{$_('why.inquiries.body.1')}
<a href="mailto:aldy505@tutanota.com">{$_('why.inquiries.body.2')}</a>, {$_('why.inquiries.body.3')}
</p>
</section>
<style>
p {
@apply text-base;
@apply py-2;
@apply lg\:w-2\/3;
}
h1 {
@apply text-3xl;
@apply font-bold;
@apply py-2;
}
a {
@apply hover\:underline;
@apply dark\:text-dodger-200;
@apply text-dodger-700;
}
section {
@apply pt-6;
}
</style>

View File

@ -1,3 +0,0 @@
{
"extends": "astro/tsconfigs/strict"
}

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 306 KiB

View File

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

35
client/svelte.config.js Normal file
View File

@ -0,0 +1,35 @@
import { vitePreprocess } from '@sveltejs/kit/vite';
import adapter from '@sveltejs/adapter-node';
import { windi } from 'svelte-windicss-preprocess';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: [
windi({
configPath: './windi.config.ts',
preflights: false,
}),
vitePreprocess({ postcss: false }),
],
kit: {
// hydrate the <div id="svelte"> element in src/app.html
trailingSlash: 'never',
files: {
routes: './src/routes',
assets: './static',
hooks: {
server: './src',
client: './src',
},
lib: './src/lib',
},
adapter: adapter({
out: 'dist',
}),
},
};
export default config;

32
client/tsconfig.json Normal file
View File

@ -0,0 +1,32 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"moduleResolution": "node",
"module": "es2020",
"lib": ["es2020"],
"target": "es2019",
/**
svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
to enforce using \`import type\` instead of \`import\` for Types.
*/
"importsNotUsedAsValues": "error",
"isolatedModules": true,
"resolveJsonModule": true,
/**
To have warnings/errors of the Svelte compiler at the correct position,
enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"allowJs": true,
"checkJs": true,
"paths": {
"$lib": ["src/lib"],
"$lib/*": ["src/lib/*"]
}
},
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
}

6
client/vite.config.ts Normal file
View File

@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
});

View File

@ -1,4 +1,4 @@
import { defineConfig } from 'unocss';
import { defineConfig } from 'windicss/helpers';
export default defineConfig({
darkMode: 'media',