Compare commits
63 Commits
i18n/engli
...
master
Author | SHA1 | Date | |
---|---|---|---|
6188e0012a | |||
cea91dbf97 | |||
aaaa2cf597 | |||
f65596963f | |||
62f60c7491 | |||
cbe4ebc8d1 | |||
aeb1b32441 | |||
1431aab04a | |||
f1e16e8c74 | |||
0330439362 | |||
8f61c0f99f | |||
37946d65b2 | |||
9c6a574708 | |||
1f282bf07a | |||
08a82bfcf3 | |||
165d969c84 | |||
4250cc42db | |||
2456dcaccc | |||
9674be37cf | |||
d9cc6c2d30 | |||
6ea4390223 | |||
21df3f0826 | |||
1e19829f19 | |||
616b64a742 | |||
64c4203b21 | |||
360c51cbea | |||
892b719222 | |||
466c4eb51d | |||
0101387b46 | |||
dfa914af5a | |||
318d1caa9e | |||
7cb50bd982 | |||
9c74cc7263 | |||
86cc813977 | |||
820cdc903b | |||
193b8604b6 | |||
9e798f1b15 | |||
5097f70d90 | |||
05a5b436d1 | |||
e6b140068b | |||
4f5a437c7c | |||
d5bc7b98dd | |||
09e69f61b9 | |||
622c436a9f | |||
100d329d47 | |||
ecad060770 | |||
fb31a822a0 | |||
d0df5fbbc6 | |||
430c50b3ae | |||
7e77b4326f | |||
c6f0d851c2 | |||
e7e449a079 | |||
0539bedc84 | |||
e406ed40a6 | |||
9bc380c81d | |||
5ea7471678 | |||
873e127251 | |||
6aae6b37bd | |||
ad19793879 | |||
59090b979c | |||
5821818618 | |||
68750ad964 | |||
296083577a |
79
.gitea/workflows/static-deploy.yml
Normal file
79
.gitea/workflows/static-deploy.yml
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
run-name: ${{ gitea.actor }}, deploy with ssh
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "release"
|
||||||
|
|
||||||
|
env:
|
||||||
|
BUILD_PATH: "."
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Detect package manager
|
||||||
|
id: detect-package-manager
|
||||||
|
run: |
|
||||||
|
if [ -f "${{ github.workspace }}/yarn.lock" ]; then
|
||||||
|
echo "manager=yarn" >> $GITHUB_OUTPUT
|
||||||
|
echo "command=install" >> $GITHUB_OUTPUT
|
||||||
|
echo "runner=yarn" >> $GITHUB_OUTPUT
|
||||||
|
echo "lockfile=yarn.lock" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
elif [ -f "${{ github.workspace }}/package.json" ]; then
|
||||||
|
echo "manager=npm" >> $GITHUB_OUTPUT
|
||||||
|
echo "command=ci" >> $GITHUB_OUTPUT
|
||||||
|
echo "runner=npx --no-install" >> $GITHUB_OUTPUT
|
||||||
|
echo "lockfile=package-lock.json" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Unable to determine package manager"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
- name: Install package manager (from package.json)
|
||||||
|
run: |
|
||||||
|
corepack enable
|
||||||
|
corepack install
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
cache: ${{ steps.detect-package-manager.outputs.manager }}
|
||||||
|
cache-dependency-path:
|
||||||
|
${{ env.BUILD_PATH }}/${{
|
||||||
|
steps.detect-package-manager.outputs.lockfile }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run:
|
||||||
|
${{ steps.detect-package-manager.outputs.manager }} ${{
|
||||||
|
steps.detect-package-manager.outputs.command }}
|
||||||
|
working-directory: ${{ env.BUILD_PATH }}
|
||||||
|
- name: Make envfile
|
||||||
|
uses: SpicyPizza/create-envfile@v2.0
|
||||||
|
with:
|
||||||
|
envkey_PUBLIC_BACKEND_URL: ${{ vars.PUBLIC_BACKEND_URL }}
|
||||||
|
- name: Build with Astro
|
||||||
|
run: |
|
||||||
|
${{ steps.detect-package-manager.outputs.runner }} astro build
|
||||||
|
working-directory: ${{ env.BUILD_PATH }}
|
||||||
|
- name: Deploy to my server
|
||||||
|
id: deploy
|
||||||
|
uses: up9cloud/action-rsync@master
|
||||||
|
env:
|
||||||
|
USER: ${{ secrets.USERNAME }}
|
||||||
|
HOST: ${{ secrets.HOST }}
|
||||||
|
KEY: ${{ secrets.KEY }}
|
||||||
|
SOURCE: ./dist/
|
||||||
|
TARGET: ${{ secrets.DESTINATION_FOLDER }}
|
||||||
|
|
||||||
|
ARGS: -avz --exclude=/.git/
|
||||||
|
SSH_ARGS:
|
||||||
|
"-p 22 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
||||||
|
VERBOSE: true
|
||||||
|
|
||||||
|
PRE_SCRIPT: |
|
||||||
|
echo start at:
|
||||||
|
date -u
|
||||||
|
POST_SCRIPT: "echo done at: && date -u"
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -19,3 +19,4 @@ pnpm-debug.log*
|
||||||
|
|
||||||
# macOS-specific files
|
# macOS-specific files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
TODO
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
{
|
{
|
||||||
"endOfLine": "lf",
|
"endOfLine": "lf",
|
||||||
"printWidth": 120,
|
"printWidth": 80,
|
||||||
"arrowParens": "avoid",
|
"arrowParens": "always",
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"htmlWhitespaceSensitivity": "css",
|
"htmlWhitespaceSensitivity": "css",
|
||||||
"insertPragma": false,
|
"insertPragma": false,
|
||||||
"bracketSameLine": true,
|
"bracketSameLine": true,
|
||||||
"jsxSingleQuote": true,
|
"jsxSingleQuote": true,
|
||||||
"proseWrap": "preserve",
|
"proseWrap": "always",
|
||||||
"quoteProps": "as-needed",
|
"quoteProps": "as-needed",
|
||||||
"requirePragma": false,
|
"requirePragma": false,
|
||||||
"semi": false,
|
"semi": false,
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"trailingComma": "none",
|
"trailingComma": "es5",
|
||||||
"useTabs": false
|
"useTabs": false
|
||||||
}
|
}
|
55
README.md
55
README.md
|
@ -1,54 +1,3 @@
|
||||||
# Astro Starter Kit: Basics
|
# KONULU KONUM
|
||||||
|
|
||||||
```sh
|
https://konulukonum.com
|
||||||
npm create astro@latest -- --template basics
|
|
||||||
```
|
|
||||||
|
|
||||||
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
|
|
||||||
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
|
|
||||||
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
|
|
||||||
|
|
||||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
|
||||||
|
|
||||||
![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)
|
|
||||||
|
|
||||||
## 🚀 Project Structure
|
|
||||||
|
|
||||||
Inside of your Astro project, you'll see the following folders and files:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/
|
|
||||||
├── public/
|
|
||||||
│ └── favicon.svg
|
|
||||||
├── src/
|
|
||||||
│ ├── components/
|
|
||||||
│ │ └── Card.astro
|
|
||||||
│ ├── layouts/
|
|
||||||
│ │ └── Layout.astro
|
|
||||||
│ └── pages/
|
|
||||||
│ └── index.astro
|
|
||||||
└── package.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
|
||||||
|
|
||||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
|
||||||
|
|
||||||
Any static assets, like images, can be placed in the `public/` directory.
|
|
||||||
|
|
||||||
## 🧞 Commands
|
|
||||||
|
|
||||||
All commands are run from the root of the project, from a terminal:
|
|
||||||
|
|
||||||
| Command | Action |
|
|
||||||
| :------------------------ | :----------------------------------------------- |
|
|
||||||
| `npm install` | Installs dependencies |
|
|
||||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
|
||||||
| `npm run build` | Build your production site to `./dist/` |
|
|
||||||
| `npm run preview` | Preview your build locally, before deploying |
|
|
||||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
|
||||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
|
||||||
|
|
||||||
## 👀 Want to learn more?
|
|
||||||
|
|
||||||
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
|
||||||
|
|
|
@ -2,13 +2,13 @@ import { defineConfig } from 'astro/config';
|
||||||
import react from "@astrojs/react";
|
import react from "@astrojs/react";
|
||||||
import tailwind from "@astrojs/tailwind";
|
import tailwind from "@astrojs/tailwind";
|
||||||
|
|
||||||
import vercel from "@astrojs/vercel/serverless";
|
const devMode = import.meta.env.DEV
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
integrations: [react(), tailwind({
|
integrations: [react(), tailwind({
|
||||||
applyBaseStyles: false
|
applyBaseStyles: false
|
||||||
})],
|
})],
|
||||||
output: "server",
|
output: "static",
|
||||||
adapter: vercel()
|
site: devMode ? "http://localhost:4321" : "https://konulukonum.com"
|
||||||
});
|
});
|
||||||
|
|
47
package.json
47
package.json
|
@ -11,36 +11,33 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.4.1",
|
"@astrojs/check": "^0.4.1",
|
||||||
"@astrojs/react": "^3.0.9",
|
"@astrojs/react": "^3.6.0",
|
||||||
"@astrojs/tailwind": "^5.1.0",
|
"@astrojs/tailwind": "^5.1.0",
|
||||||
"@astrojs/vercel": "^7.3.1",
|
"@radix-ui/react-checkbox": "^1.1.1",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@supabase/supabase-js": "^2.39.7",
|
"astro": "^4.11.6",
|
||||||
"@types/react": "^18.2.47",
|
|
||||||
"@types/react-dom": "^18.2.18",
|
|
||||||
"@vercel/blob": "^0.19.0",
|
|
||||||
"@vercel/postgres-kysely": "^0.7.1",
|
|
||||||
"@vercel/speed-insights": "^1.0.9",
|
|
||||||
"astro": "^4.1.2",
|
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.11",
|
||||||
"kysely": "^0.26.0",
|
"leaflet": "^1.9.4",
|
||||||
"lucide-react": "^0.309.0",
|
"lucide-react": "^0.309.0",
|
||||||
"nanoid": "^5.0.4",
|
"react": "^18.3.1",
|
||||||
"react": "^18.2.0",
|
"react-dom": "^18.3.1",
|
||||||
"react-dom": "^18.2.0",
|
"tailwind-merge": "^2.4.0",
|
||||||
"tailwind-merge": "^2.2.0",
|
"tailwindcss": "^3.4.6",
|
||||||
"tailwindcss": "^3.4.1",
|
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5.3.3"
|
"toastify-js": "^1.12.0",
|
||||||
|
"typescript": "^5.5.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.0.5",
|
"@types/leaflet": "^1.9.12",
|
||||||
"@types/google.maps": "^3.54.10"
|
"@types/react": "^18.3.3",
|
||||||
}
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@types/google.maps": "^3.55.11",
|
||||||
|
"@types/toastify-js": "^1.12.3"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 320 KiB |
|
@ -1,61 +0,0 @@
|
||||||
---
|
|
||||||
interface Props {
|
|
||||||
title: string;
|
|
||||||
body: string;
|
|
||||||
href: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { href, title, body } = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<li class="link-card">
|
|
||||||
<a href={href}>
|
|
||||||
<h2>
|
|
||||||
{title}
|
|
||||||
<span>→</span>
|
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
{body}
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<style>
|
|
||||||
.link-card {
|
|
||||||
list-style: none;
|
|
||||||
display: flex;
|
|
||||||
padding: 1px;
|
|
||||||
background-color: #23262d;
|
|
||||||
background-image: none;
|
|
||||||
background-size: 400%;
|
|
||||||
border-radius: 7px;
|
|
||||||
background-position: 100%;
|
|
||||||
transition: background-position 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
|
||||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
.link-card > a {
|
|
||||||
width: 100%;
|
|
||||||
text-decoration: none;
|
|
||||||
line-height: 1.4;
|
|
||||||
padding: calc(1.5rem - 1px);
|
|
||||||
border-radius: 8px;
|
|
||||||
color: white;
|
|
||||||
background-color: #23262d;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
transition: color 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.link-card:is(:hover, :focus-within) {
|
|
||||||
background-position: 0;
|
|
||||||
background-image: var(--accent-gradient);
|
|
||||||
}
|
|
||||||
.link-card:is(:hover, :focus-within) h2 {
|
|
||||||
color: rgb(var(--accent-light));
|
|
||||||
}
|
|
||||||
</style>
|
|
10
src/components/Leaflet/constants.ts
Normal file
10
src/components/Leaflet/constants.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import L from "leaflet"
|
||||||
|
|
||||||
|
export const openstreetmapTiles = L.tileLayer(
|
||||||
|
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||||
|
{
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution:
|
||||||
|
'© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||||
|
}
|
||||||
|
)
|
18
src/components/Leaflet/geolocation.ts
Normal file
18
src/components/Leaflet/geolocation.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import L from "leaflet"
|
||||||
|
|
||||||
|
function updateMarkerLocation(
|
||||||
|
marker: L.Marker,
|
||||||
|
icon: L.Icon,
|
||||||
|
position: L.LatLng,
|
||||||
|
map: L.Map
|
||||||
|
) {
|
||||||
|
if (marker) {
|
||||||
|
marker.setLatLng(position)
|
||||||
|
} else {
|
||||||
|
marker = L.marker(position, { icon })
|
||||||
|
marker.addTo(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
return marker
|
||||||
|
}
|
||||||
|
export { updateMarkerLocation }
|
13
src/components/Leaflet/icons.ts
Normal file
13
src/components/Leaflet/icons.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import L from "leaflet"
|
||||||
|
|
||||||
|
var targetLocationIcon = L.icon({
|
||||||
|
iconUrl: "/goal.svg",
|
||||||
|
iconSize: [32, 32],
|
||||||
|
})
|
||||||
|
|
||||||
|
var currentLocationIcon = L.icon({
|
||||||
|
iconUrl: "/blue-dot.png",
|
||||||
|
iconSize: [32, 32],
|
||||||
|
})
|
||||||
|
|
||||||
|
export { targetLocationIcon, currentLocationIcon }
|
|
@ -1,147 +0,0 @@
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
|
||||||
|
|
||||||
import { LockClosedIcon, LockOpen1Icon } from "@radix-ui/react-icons"
|
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
|
|
||||||
import "../styles/locked-content.css"
|
|
||||||
import type { Generated } from "kysely"
|
|
||||||
import { onLocationError } from "@/lib/error"
|
|
||||||
|
|
||||||
const incrementCounter = async (id: string | Generated<string>) =>
|
|
||||||
await fetch(`${import.meta.env.PUBLIC_HOME_URL}/api/content/increment?id=${id}`)
|
|
||||||
|
|
||||||
const LocationButton = ({
|
|
||||||
contentId = "",
|
|
||||||
imageUrl = "#",
|
|
||||||
location = ""
|
|
||||||
}: {
|
|
||||||
contentId?: string | Generated<string>
|
|
||||||
imageUrl?: string
|
|
||||||
location?: string
|
|
||||||
}) => {
|
|
||||||
const [atTarget, setAtTarget] = useState(false)
|
|
||||||
const [contentVisible, setContentVisible] = useState(false)
|
|
||||||
const [hasPermission, setHasPermission] = useState(false)
|
|
||||||
const [watchId, setWatchId] = useState<number>()
|
|
||||||
const [distanceRemain, setDistanceRemain] = useState<string>("")
|
|
||||||
|
|
||||||
const targetCoordinates = JSON.parse(location).coordinates
|
|
||||||
|
|
||||||
const targetPos = {
|
|
||||||
lat: targetCoordinates[0],
|
|
||||||
lng: targetCoordinates[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
const startWatchingLocation = () => {
|
|
||||||
setHasPermission(true)
|
|
||||||
if (!watchId) {
|
|
||||||
const id = navigator.geolocation.watchPosition(
|
|
||||||
(position: GeolocationPosition) => {
|
|
||||||
const pos = {
|
|
||||||
lat: position.coords.latitude,
|
|
||||||
lng: position.coords.longitude
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error 3rd party script
|
|
||||||
const targetLatLng = L.latLng(targetPos)
|
|
||||||
|
|
||||||
// @ts-expect-error 3rd party script
|
|
||||||
const currentLatLng = L.latLng(pos)
|
|
||||||
|
|
||||||
const betweenMeters = currentLatLng.distanceTo(targetLatLng)
|
|
||||||
|
|
||||||
if (betweenMeters > 1000) {
|
|
||||||
setDistanceRemain(`${(betweenMeters / 1000).toFixed()} KM`)
|
|
||||||
} else if (betweenMeters > 50) {
|
|
||||||
setDistanceRemain(`${betweenMeters.toFixed(0)} M`)
|
|
||||||
} else {
|
|
||||||
setAtTarget(true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
err => onLocationError(err),
|
|
||||||
{ enableHighAccuracy: true, timeout: 27000, maximumAge: 10000 }
|
|
||||||
)
|
|
||||||
|
|
||||||
setWatchId(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUnlock = async () => {
|
|
||||||
setContentVisible(true)
|
|
||||||
await incrementCounter(contentId)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
navigator.permissions.query({ name: "geolocation" }).then(permissionStatus => {
|
|
||||||
if (permissionStatus.state === "granted") {
|
|
||||||
setHasPermission(true)
|
|
||||||
startWatchingLocation()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
if (contentVisible) {
|
|
||||||
return (
|
|
||||||
<div className='w-full h-[475px] p-4 flex justify-center'>
|
|
||||||
<img src={imageUrl} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div className='w-full h-[475px] overflow-hidden border border-zinc-200 shadow-sm p-4 rounded'>
|
|
||||||
{atTarget ? (
|
|
||||||
<div className='flex flex-col justify-center items-center image-wrapper'>
|
|
||||||
<img src={imageUrl} className='blur-2xl h-[450px]' />
|
|
||||||
|
|
||||||
<div className='flex flex-col justify-center gap-4 overlay'>
|
|
||||||
<Button
|
|
||||||
size='lg'
|
|
||||||
className='text-lg p-6 animate-pulse bg-indigo-600 hover:bg-indigo-700 hover:animate-none border-2 border-indigo-800'
|
|
||||||
onClick={handleUnlock}>
|
|
||||||
<LockOpen1Icon className='mr-2 h-4 w-4' />
|
|
||||||
İçeriğin Kilidi Açıldı
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Card className='p-2'>
|
|
||||||
<CardContent className='pb-0 text-center'>Click to see the photo!</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='flex flex-col justify-center items-center image-wrapper'>
|
|
||||||
<img src={imageUrl} className='blur-2xl h-[450px]' />
|
|
||||||
<div className='flex flex-col justify-center gap-4 overlay'>
|
|
||||||
<Button size='lg' className='text-md'>
|
|
||||||
<LockClosedIcon className='mr-2 h-4 w-4' /> Photo is locked
|
|
||||||
</Button>
|
|
||||||
<Card className='p-2'>
|
|
||||||
{hasPermission ? (
|
|
||||||
<CardContent className='pb-0 text-center'>
|
|
||||||
<p>You'll have to go the location to see it</p>
|
|
||||||
<p>{distanceRemain && `Kalan mesafe: ${distanceRemain}`}</p>
|
|
||||||
</CardContent>
|
|
||||||
) : (
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<CardContent className='pb-0 text-center'>
|
|
||||||
Click below to see how close you are
|
|
||||||
</CardContent>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size='sm'
|
|
||||||
className='bg-green-700 hover:bg-green-600 text-md'
|
|
||||||
onClick={() => startWatchingLocation()}>
|
|
||||||
Give geolocation permission
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LocationButton
|
|
133
src/components/LockedContent/geolocation.ts
Normal file
133
src/components/LockedContent/geolocation.ts
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
import L, { type LatLngTuple } from "leaflet"
|
||||||
|
import {
|
||||||
|
addAttribute,
|
||||||
|
addClasses,
|
||||||
|
removeClasses,
|
||||||
|
removeElement,
|
||||||
|
revealContent,
|
||||||
|
toggleClass,
|
||||||
|
updateText,
|
||||||
|
} from "../../lib/domUtils"
|
||||||
|
import { mapLocationSuccessCallback } from "@/scripts/initMap"
|
||||||
|
import { toast } from "@/lib/utils"
|
||||||
|
|
||||||
|
// Update the elements according to distance remaining
|
||||||
|
function locationSuccessCallback(
|
||||||
|
position: GeolocationPosition,
|
||||||
|
targetPosition: LatLngTuple,
|
||||||
|
radius: number
|
||||||
|
) {
|
||||||
|
// Enable current location control
|
||||||
|
removeClasses("current-location-control", "disabled-button")
|
||||||
|
const newPosition = position.coords
|
||||||
|
|
||||||
|
// Calculate the distance remaining
|
||||||
|
const distance = calculateDistance(
|
||||||
|
[newPosition.latitude, newPosition.longitude],
|
||||||
|
targetPosition
|
||||||
|
)
|
||||||
|
|
||||||
|
// If user has arrived to destination
|
||||||
|
if (distance < radius) {
|
||||||
|
// Change the description texts
|
||||||
|
updateText("button-text", "İçeriği Göster")
|
||||||
|
updateText("locked-content-description", "İçeriği görmek için butona bas!")
|
||||||
|
|
||||||
|
// Swap the icon
|
||||||
|
toggleClass("lock-icon", "hidden")
|
||||||
|
toggleClass("unlock-icon", "hidden")
|
||||||
|
|
||||||
|
// Tansform the unlock button
|
||||||
|
removeClasses("unlock-content-button", "bg-primary", "hover:bg-primary/90")
|
||||||
|
addClasses(
|
||||||
|
"unlock-content-button",
|
||||||
|
"bg-indigo-600",
|
||||||
|
"hover:bg-indigo-700",
|
||||||
|
"hover:animate-none",
|
||||||
|
"border-2",
|
||||||
|
"border-indigo-800"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wait for transition to finish then add animation
|
||||||
|
setTimeout(() => {
|
||||||
|
removeClasses("unlock-content-button", "duration-1000")
|
||||||
|
addClasses("unlock-content-button", "animate-pulse")
|
||||||
|
}, 800)
|
||||||
|
|
||||||
|
// Reveal image when the unlock button is clicked
|
||||||
|
const unlockButton = document.getElementById("unlock-content-button")
|
||||||
|
unlockButton?.addEventListener("click", revealContent)
|
||||||
|
} else {
|
||||||
|
const distanceText = generateDistanceText(distance)
|
||||||
|
updateText("locked-content-description", `Kalan mesafe: ${distanceText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeElement("location-permission-button", true)
|
||||||
|
|
||||||
|
// Update leaflet controls
|
||||||
|
mapLocationSuccessCallback(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This callback will be fired on geolocation error
|
||||||
|
function errorCallback(err: GeolocationPositionError) {
|
||||||
|
let errorMessage
|
||||||
|
// Show toast accoring to the error state
|
||||||
|
switch (err.code) {
|
||||||
|
case GeolocationPositionError.PERMISSION_DENIED:
|
||||||
|
errorMessage =
|
||||||
|
"Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin."
|
||||||
|
updateText(
|
||||||
|
"locked-content-description",
|
||||||
|
"Konum izleme izni alınamadı. \nİçeriği görüntüleyebilmek için konum bilginiz gerekiyor."
|
||||||
|
)
|
||||||
|
addAttribute("current-location-control", "disabled", "true")
|
||||||
|
addClasses("current-location-control", "disabled-button")
|
||||||
|
removeElement("location-permission-button")
|
||||||
|
break
|
||||||
|
case GeolocationPositionError.POSITION_UNAVAILABLE:
|
||||||
|
errorMessage =
|
||||||
|
"Konumunuz tespit edilemedi, lütfen biraz sonra tekrar deneyiniz."
|
||||||
|
break
|
||||||
|
case GeolocationPositionError.TIMEOUT:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
errorMessage =
|
||||||
|
"Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin."
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
toast(errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateDistance(
|
||||||
|
currentPosition: L.LatLngTuple,
|
||||||
|
targetPosition: L.LatLngTuple
|
||||||
|
) {
|
||||||
|
// Get target position in latitudes and longitudes
|
||||||
|
const targetLatLng = L.latLng(targetPosition)
|
||||||
|
|
||||||
|
// Get current position in latitudes and longitudes
|
||||||
|
const currentLatLng = L.latLng(currentPosition)
|
||||||
|
|
||||||
|
// Calculate the distance between target and current position in meters
|
||||||
|
const betweenMeters = currentLatLng.distanceTo(targetLatLng)
|
||||||
|
|
||||||
|
return betweenMeters
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a human readable destination text according to
|
||||||
|
// distance remaining
|
||||||
|
function generateDistanceText(distance: number) {
|
||||||
|
if (distance > 1000) {
|
||||||
|
return `${(distance / 1000).toFixed()} KM`
|
||||||
|
} else if (distance > 100) {
|
||||||
|
return `${distance.toFixed(0)} M`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
errorCallback,
|
||||||
|
locationSuccessCallback,
|
||||||
|
calculateDistance,
|
||||||
|
generateDistanceText,
|
||||||
|
}
|
14
src/components/LockedContent/serverUtils.ts
Normal file
14
src/components/LockedContent/serverUtils.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// This counter is shown at the bottom of the page and incremented
|
||||||
|
// each time "show content" button is clicked
|
||||||
|
function incrementUnlockCounter(id: string | undefined) {
|
||||||
|
if (id) {
|
||||||
|
fetch(
|
||||||
|
`${import.meta.env.PUBLIC_BACKEND_URL}/api/location/increment/${id}`,
|
||||||
|
{
|
||||||
|
method: "PATCH",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { incrementUnlockCounter }
|
|
@ -1,11 +1,12 @@
|
||||||
|
import { toast } from "@/lib/utils"
|
||||||
import { Button } from "./ui/button"
|
import { Button } from "./ui/button"
|
||||||
|
|
||||||
const ShareButton = () => {
|
const ShareButton = () => {
|
||||||
const shareLink = async () => {
|
const shareLink = async () => {
|
||||||
const shareData = {
|
const shareData = {
|
||||||
title: "Konulu Konum",
|
title: "Konulu Konum",
|
||||||
text: "Surprise you're loved ones!",
|
text: "Sevdiklerinizi şaşırtın!",
|
||||||
url: document.URL
|
url: document.URL,
|
||||||
}
|
}
|
||||||
|
|
||||||
await navigator.share(shareData)
|
await navigator.share(shareData)
|
||||||
|
@ -14,20 +15,7 @@ const ShareButton = () => {
|
||||||
const copyLink = async () => {
|
const copyLink = async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(document.URL)
|
await navigator.clipboard.writeText(document.URL)
|
||||||
// @ts-ignore
|
toast("Konulu konum kopyalandı.")
|
||||||
Toastify({
|
|
||||||
text: "Konulu konum copied.",
|
|
||||||
duration: 3000,
|
|
||||||
gravity: "top", // `top` or `bottom`
|
|
||||||
position: "center", // `left`, `center` or `right`
|
|
||||||
stopOnFocus: true, // Prevents dismissing of toast on hover
|
|
||||||
style: {
|
|
||||||
background: "black",
|
|
||||||
borderRadius: "6px",
|
|
||||||
margin: "16px"
|
|
||||||
},
|
|
||||||
onClick: function () {} // Callback after click
|
|
||||||
}).showToast()
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err.message)
|
console.error(err.message)
|
||||||
}
|
}
|
||||||
|
@ -38,12 +26,12 @@ const ShareButton = () => {
|
||||||
{
|
{
|
||||||
//@ts-expect-error navigator is not always defined
|
//@ts-expect-error navigator is not always defined
|
||||||
window.navigator.share ? (
|
window.navigator.share ? (
|
||||||
<Button className='w-full text-lg' size='lg' onClick={shareLink}>
|
<Button className='w-full text-xl' size='lg' onClick={shareLink}>
|
||||||
Share
|
Paylaş
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button className='w-full text-lg' size='lg' onClick={copyLink}>
|
<Button className='w-full text-xl' size='lg' onClick={copyLink}>
|
||||||
Share
|
Paylaş
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
9
src/env.d.ts
vendored
9
src/env.d.ts
vendored
|
@ -1,3 +1,12 @@
|
||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
/// <reference types="astro/client" />
|
/// <reference types="astro/client" />
|
||||||
/// <reference types="@types/google.maps" />
|
/// <reference types="@types/google.maps" />
|
||||||
/// <reference types="@types/leaflet" />
|
/// <reference types="@types/leaflet" />
|
||||||
|
|
||||||
|
export declare global {
|
||||||
|
namespace astroHTML.JSX {
|
||||||
|
interface HTMLAttributes {
|
||||||
|
_?: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,47 +1,28 @@
|
||||||
---
|
---
|
||||||
import SpeedInsights from '@vercel/speed-insights/astro';
|
import "leaflet/dist/leaflet.css"
|
||||||
|
import "toastify-js/src/toastify.css"
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang='en'>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset='UTF-8' />
|
||||||
<meta name="description" content="Astro description" />
|
<meta name='description' content='Astro description' />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name='viewport' content='width=device-width' />
|
||||||
<link rel="icon" type="image/jpg" href="/konulu-konum-logo.jpg" />
|
<link rel='icon' type='image/jpg' href='/konulu-konum-logo.jpg' />
|
||||||
<link
|
<meta property='og:title' content='Konulu Konum' />
|
||||||
rel="stylesheet"
|
<meta property='og:description' content='Sevdiklerinizi şaşırtın!' />
|
||||||
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
<meta property='og:url' content='https://konulukonum.com' />
|
||||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
|
||||||
crossorigin=""
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
type="text/css"
|
|
||||||
href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css"
|
|
||||||
/>
|
|
||||||
<script
|
|
||||||
is:inline
|
|
||||||
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
||||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
|
||||||
crossorigin=""></script>
|
|
||||||
<script
|
|
||||||
type="text/javascript"
|
|
||||||
src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
|
|
||||||
<meta property="og:title" content="Konulu Konum" />
|
|
||||||
<meta property="og:description" content="Sevdiklerinizi şaşırtın!" />
|
|
||||||
<meta property="og:url" content="https://konulu-konum.vercel.app" />
|
|
||||||
<meta
|
<meta
|
||||||
property="og:image"
|
property='og:image'
|
||||||
content="https://konulu-konum.vercel.app/konulu-konum-logo.jpg"
|
content='https://konulukonum.com/konulu-konum-logo.jpg'
|
||||||
/>
|
/>
|
||||||
<meta name="twitter:card" content="summary" />
|
<meta name='twitter:card' content='summary' />
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name='generator' content={Astro.generator} />
|
||||||
<title>Konulu Konum</title>
|
<title>Konulu Konum</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="container my-8">
|
<body class='container my-8'>
|
||||||
<slot />
|
<slot />
|
||||||
<SpeedInsights />
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
<style is:global></style>
|
<style is:global></style>
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import type { Generated } from "kysely"
|
|
||||||
|
|
||||||
export interface Database {
|
export interface Database {
|
||||||
contents: ContentTable
|
contents: ContentTable
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContentTable {
|
export interface ContentTable {
|
||||||
id: Generated<string>
|
id: string
|
||||||
url: string
|
url: string
|
||||||
blob_url: string
|
blob_url: string
|
||||||
loc: string
|
loc: string
|
||||||
author: string
|
author: string
|
||||||
description: string
|
description: string
|
||||||
created_at: Generated<string>
|
created_at: string
|
||||||
unlocked_counter: Generated<number>
|
radius: number
|
||||||
|
unlocked_counter: number
|
||||||
}
|
}
|
||||||
|
|
98
src/lib/domUtils.ts
Normal file
98
src/lib/domUtils.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import { incrementUnlockCounter } from "../components/LockedContent/serverUtils"
|
||||||
|
|
||||||
|
function updateText(elemId: string, text: string) {
|
||||||
|
const elem = document.getElementById(elemId)
|
||||||
|
if (elem) elem.innerText = text
|
||||||
|
else console.error("Element could not be found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateInputValue(elemId: string, value: string) {
|
||||||
|
const elem = document.getElementById(elemId) as HTMLInputElement
|
||||||
|
if (elem) elem.value = value
|
||||||
|
else console.error("Element could not be found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleClass(elemId: string, className: string) {
|
||||||
|
const elem = document.getElementById(elemId)
|
||||||
|
if (elem) elem.classList.toggle(className)
|
||||||
|
else console.error("Element could not be found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeClasses(elemId: string, ...inputs: string[]) {
|
||||||
|
const elem = document.getElementById(elemId)
|
||||||
|
if (elem) elem.classList.remove(...inputs)
|
||||||
|
else console.error("Element could not be found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
function addClasses(elemId: string, ...inputs: string[]) {
|
||||||
|
const elem = document.getElementById(elemId)
|
||||||
|
if (elem) elem.classList.add(...inputs)
|
||||||
|
else console.error("Element could not be found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeElement(elemId: string, silent = false) {
|
||||||
|
const elem = document.getElementById(elemId)
|
||||||
|
if (elem) elem.remove()
|
||||||
|
else if (silent) return
|
||||||
|
else console.error("Element could not be found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAttribute(elemId: string, attribute: string, value: string) {
|
||||||
|
const elem = document.getElementById(elemId)
|
||||||
|
if (elem) elem.setAttribute(attribute, value)
|
||||||
|
else console.error("Element could not be found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
function revealContent() {
|
||||||
|
incrementUnlockCounter(document.URL.slice(-12))
|
||||||
|
removeClasses("content", "blur-2xl")
|
||||||
|
removeElement("unlock-button-container")
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateFileInput(el: HTMLInputElement) {
|
||||||
|
const files = el.files
|
||||||
|
|
||||||
|
if (!files) return
|
||||||
|
|
||||||
|
if (files.length > 0) {
|
||||||
|
if (files[0].size > 4 * 1024 * 1024) {
|
||||||
|
el.setCustomValidity("Dosya boyutu 4 MB'den küçük olmalıdır.")
|
||||||
|
el.reportValidity()
|
||||||
|
} else {
|
||||||
|
el.setCustomValidity("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleButton(elemId: string, spinnerId: string) {
|
||||||
|
const elem = document.getElementById(elemId) as HTMLButtonElement | null
|
||||||
|
const spinner = document.getElementById(spinnerId)
|
||||||
|
|
||||||
|
if (!elem) {
|
||||||
|
throw new Error("Element could not be found!")
|
||||||
|
} else if (elem.disabled) {
|
||||||
|
spinner?.classList.add("hidden")
|
||||||
|
removeClasses(elemId, "bg-slate-500")
|
||||||
|
addClasses(elemId, "bg-slate-900")
|
||||||
|
elem.disabled = false
|
||||||
|
} else {
|
||||||
|
spinner?.classList.remove("hidden")
|
||||||
|
removeClasses(elemId, "bg-slate-900")
|
||||||
|
addClasses(elemId, "bg-slate-500")
|
||||||
|
elem.disabled = true
|
||||||
|
setTimeout(() => toggleButton(elemId, spinnerId), 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
addClasses,
|
||||||
|
removeClasses,
|
||||||
|
removeElement,
|
||||||
|
toggleClass,
|
||||||
|
updateText,
|
||||||
|
updateInputValue,
|
||||||
|
revealContent,
|
||||||
|
addAttribute,
|
||||||
|
validateFileInput,
|
||||||
|
toggleButton,
|
||||||
|
}
|
|
@ -1,31 +0,0 @@
|
||||||
export function onLocationError(err: GeolocationPositionError) {
|
|
||||||
let errorMessage
|
|
||||||
switch (err.code) {
|
|
||||||
case 1:
|
|
||||||
errorMessage = "Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin."
|
|
||||||
break
|
|
||||||
case 2:
|
|
||||||
errorMessage = "Konumunuz tespit edilemedi, lütfen biraz sonra tekrar deneyiniz."
|
|
||||||
break
|
|
||||||
case 3:
|
|
||||||
errorMessage = "Konum isteği zaman aşımına uğradı, lütfen sayfayı yenileyip tekrar deneyiniz."
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
errorMessage = "Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin."
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// @ts-ignore
|
|
||||||
Toastify({
|
|
||||||
text: errorMessage,
|
|
||||||
duration: 3000,
|
|
||||||
gravity: "top", // `top` or `bottom`
|
|
||||||
position: "center", // `left`, `center` or `right`
|
|
||||||
stopOnFocus: true, // Prevents dismissing of toast on hover
|
|
||||||
style: {
|
|
||||||
background: "black",
|
|
||||||
borderRadius: "6px",
|
|
||||||
margin: "16px"
|
|
||||||
},
|
|
||||||
onClick: function () {} // Callback after click
|
|
||||||
}).showToast()
|
|
||||||
}
|
|
|
@ -1,10 +1,23 @@
|
||||||
import { type ClassValue, clsx } from "clsx"
|
import { type ClassValue, clsx } from "clsx"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
|
import Toastify from "toastify-js"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function remoteLog(data: any) {
|
export function toast(errorMessage: string) {
|
||||||
fetch("/api/debug", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) })
|
Toastify({
|
||||||
|
text: errorMessage,
|
||||||
|
duration: 3000,
|
||||||
|
gravity: "top",
|
||||||
|
position: "center",
|
||||||
|
stopOnFocus: true,
|
||||||
|
style: {
|
||||||
|
background: "black",
|
||||||
|
borderRadius: "6px",
|
||||||
|
margin: "16px",
|
||||||
|
},
|
||||||
|
onClick: function () {},
|
||||||
|
}).showToast()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
---
|
|
||||||
import '@/styles/globals.css';
|
|
||||||
import '../styles/locked-page.css';
|
|
||||||
|
|
||||||
import Layout from '../layouts/Layout.astro';
|
|
||||||
import ShareButton from '../components/ShareButton';
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card';
|
|
||||||
import { CalendarIcon } from '@radix-ui/react-icons';
|
|
||||||
import LockedContent from '@/components/LockedContent';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import type { ContentTable } from '@/lib/db';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
||||||
import utc from 'dayjs/plugin/utc';
|
|
||||||
|
|
||||||
type Content = Omit<ContentTable, 'url'>;
|
|
||||||
|
|
||||||
const { id } = Astro.params;
|
|
||||||
|
|
||||||
const res = await fetch(
|
|
||||||
`${import.meta.env.PUBLIC_HOME_URL}/api/content?id=${id}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const data: Content | null = res.status === 200 ? await res.json() : null;
|
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
|
|
||||||
dayjs.extend(utc);
|
|
||||||
|
|
||||||
// @ts-expect-error Generated<string> is string
|
|
||||||
const dateFromNow = dayjs.utc(data?.created_at).from(dayjs.utc());
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout>
|
|
||||||
<main class="flex flex-col gap-4">
|
|
||||||
<div>
|
|
||||||
<h1
|
|
||||||
class="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl hover:underline"
|
|
||||||
>
|
|
||||||
<a href=`${import.meta.env.PUBLIC_HOME_URL}`> Konulu Konum</a>
|
|
||||||
</h1>
|
|
||||||
<p class="leading-7 [&:not(:first-child)]:mt-6 text-xl">
|
|
||||||
You have a message and photo from you friend. To see it, you'll have to
|
|
||||||
go to location below. Once you're close, you'll be able to see it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<Card className="w-full">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{data?.author}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<p>{data?.description}</p>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="gap-2">
|
|
||||||
<CalendarIcon />
|
|
||||||
<p>{dateFromNow}</p>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<LockedContent
|
|
||||||
contentId={data?.id}
|
|
||||||
imageUrl={data?.blob_url}
|
|
||||||
location={data?.loc}
|
|
||||||
client:load
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="map"
|
|
||||||
class="w-full h-[450px] rounded"
|
|
||||||
data-target-location={data?.loc}
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ShareButton client:only />
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<p class="text-muted-foreground">
|
|
||||||
Photo is unlocked <b>{data?.unlocked_counter}</b> times
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<script src="../scripts/initMap.js"></script>
|
|
||||||
</Layout>
|
|
|
@ -1,39 +0,0 @@
|
||||||
import type { Database } from "@/lib/db"
|
|
||||||
import { createKysely } from "@vercel/postgres-kysely"
|
|
||||||
import type { APIRoute } from "astro"
|
|
||||||
|
|
||||||
export const GET: APIRoute = async ({ request }) => {
|
|
||||||
const contentId = new URL(request.url).searchParams.get("id")
|
|
||||||
|
|
||||||
if (!contentId) {
|
|
||||||
return new Response(null, {
|
|
||||||
status: 400,
|
|
||||||
statusText: "Content id is required"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = createKysely<Database>({ connectionString: import.meta.env.POSTGRES_URL })
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await db
|
|
||||||
.updateTable("contents")
|
|
||||||
.set(eb => ({ unlocked_counter: eb("unlocked_counter", "+", 1) }))
|
|
||||||
.where("id", "=", contentId)
|
|
||||||
.executeTakeFirst()
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
return new Response(JSON.stringify({ counter: Number(result) }))
|
|
||||||
} else {
|
|
||||||
return new Response(null, {
|
|
||||||
status: 404,
|
|
||||||
statusText: "Could not increment the counter"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching content:", error)
|
|
||||||
return new Response(null, {
|
|
||||||
status: 500,
|
|
||||||
statusText: "Error while fetching content"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
import { createClient } from "@supabase/supabase-js"
|
|
||||||
import type { APIRoute } from "astro"
|
|
||||||
import { createKysely } from "@vercel/postgres-kysely"
|
|
||||||
import { customAlphabet } from "nanoid"
|
|
||||||
import sharpService from "astro/assets/services/sharp"
|
|
||||||
|
|
||||||
import type { Database } from "@/lib/db"
|
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
|
||||||
const formData = await request.formData()
|
|
||||||
|
|
||||||
const image = formData.get("selected-photo") as File
|
|
||||||
const author = formData.get("author")
|
|
||||||
const description = formData.get("description")
|
|
||||||
const geolocation = formData.get("geolocation")
|
|
||||||
|
|
||||||
if (!image || !geolocation) {
|
|
||||||
return new Response(null, {
|
|
||||||
status: 400,
|
|
||||||
statusText: "Image and geolocation are required fields"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const pos = geolocation.toString().split(",")
|
|
||||||
|
|
||||||
if (pos.length !== 2) {
|
|
||||||
return new Response(null, {
|
|
||||||
status: 400,
|
|
||||||
statusText: "Geolocation not correctly formatted"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const supabaseUrl = import.meta.env.SUPABASE_URL
|
|
||||||
const supabaseKey = import.meta.env.SUPABASE_KEY
|
|
||||||
const supabase = createClient(supabaseUrl, supabaseKey)
|
|
||||||
|
|
||||||
const nanoid = customAlphabet("abcdefghijklmnoprstuvyz", 10)
|
|
||||||
|
|
||||||
const randomImageId = nanoid()
|
|
||||||
|
|
||||||
const imageName = `${image.name.replace(/\.[^/.]+$/, "")}${randomImageId}.webp`
|
|
||||||
|
|
||||||
const imageBuf = await image.arrayBuffer()
|
|
||||||
|
|
||||||
const { data, format } = await sharpService.transform(
|
|
||||||
new Uint8Array(imageBuf),
|
|
||||||
{ src: imageName, format: "webp" },
|
|
||||||
{ domains: [], remotePatterns: [], service: { entrypoint: "", config: { limitInputPixels: false } } }
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log(format)
|
|
||||||
|
|
||||||
const { error } = await supabase.storage.from("images").upload(`public/${imageName}`, data, {
|
|
||||||
cacheControl: "3600",
|
|
||||||
upsert: false,
|
|
||||||
contentType: "image/webp"
|
|
||||||
})
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error(error.message, imageName, error.cause)
|
|
||||||
return new Response(null, {
|
|
||||||
status: 400,
|
|
||||||
statusText: error.message
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const imagePublicUrl = `https://sozfqjbdyppxfwhqktja.supabase.co/storage/v1/object/public/images/public/${imageName}`
|
|
||||||
|
|
||||||
const db = createKysely<Database>({ connectionString: import.meta.env.POSTGRES_URL })
|
|
||||||
|
|
||||||
const newUrl = nanoid()
|
|
||||||
|
|
||||||
const res = await db
|
|
||||||
.insertInto("contents")
|
|
||||||
.values({
|
|
||||||
url: `${newUrl.slice(0, 3)}-${newUrl.slice(3, 7)}-${newUrl.slice(7)}`,
|
|
||||||
blob_url: imagePublicUrl,
|
|
||||||
author: author?.toString() ?? "",
|
|
||||||
description: description?.toString() ?? "",
|
|
||||||
loc: `SRID=4326;POINT(${pos[0]} ${pos[1]})`
|
|
||||||
})
|
|
||||||
.returning("url")
|
|
||||||
.executeTakeFirst()
|
|
||||||
|
|
||||||
if (res?.url) {
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
url: res.url
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return new Response(null, {
|
|
||||||
status: 500,
|
|
||||||
statusText: "Error while saving data"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GET: APIRoute = async ({ request }) => {
|
|
||||||
const contentId = new URL(request.url).searchParams.get("id")
|
|
||||||
|
|
||||||
if (!contentId) {
|
|
||||||
return new Response(null, {
|
|
||||||
status: 400,
|
|
||||||
statusText: "Content id is required"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = createKysely<Database>({ connectionString: import.meta.env.POSTGRES_URL })
|
|
||||||
|
|
||||||
try {
|
|
||||||
const content = await db
|
|
||||||
.selectFrom("contents")
|
|
||||||
.select(({ fn }) => [
|
|
||||||
"id",
|
|
||||||
"blob_url",
|
|
||||||
fn<string>("ST_AsGeoJSON", ["loc"]).as("loc"),
|
|
||||||
"description",
|
|
||||||
"author",
|
|
||||||
"created_at",
|
|
||||||
"unlocked_counter"
|
|
||||||
])
|
|
||||||
.where("url", "=", contentId)
|
|
||||||
.executeTakeFirst()
|
|
||||||
|
|
||||||
if (content) {
|
|
||||||
return new Response(JSON.stringify(content))
|
|
||||||
} else {
|
|
||||||
return new Response(null, {
|
|
||||||
status: 404,
|
|
||||||
statusText: "Content not found"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching content:", error)
|
|
||||||
return new Response(null, {
|
|
||||||
status: 500,
|
|
||||||
statusText: "Error while fetching content"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import type { APIRoute } from "astro"
|
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
|
||||||
const data = await request.json()
|
|
||||||
|
|
||||||
console.log(data)
|
|
||||||
|
|
||||||
return new Response(null, {
|
|
||||||
status: 200
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,224 +1,145 @@
|
||||||
---
|
---
|
||||||
export const prerender = true;
|
import "@/styles/globals.css"
|
||||||
|
import "../styles/locked-page.css"
|
||||||
|
|
||||||
import '@/styles/globals.css';
|
import Layout from "../layouts/Layout.astro"
|
||||||
import Layout from '../layouts/Layout.astro';
|
import { Loader2 } from "lucide-react"
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
|
|
||||||
import '../styles/locked-page.css';
|
const backendUrl = import.meta.env.PUBLIC_BACKEND_URL
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
|
||||||
import { ReloadIcon } from '@radix-ui/react-icons';
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<main>
|
<main>
|
||||||
<form
|
<form
|
||||||
class="flex flex-col gap-8"
|
class='flex flex-col gap-12 md:gap-10'
|
||||||
id="sample-form"
|
id='sample-form'
|
||||||
enctype="multipart/form-data"
|
action={`${backendUrl}/api/location`}
|
||||||
>
|
method='post'
|
||||||
|
enctype='multipart/form-data'>
|
||||||
<div>
|
<div>
|
||||||
<h1
|
<h1
|
||||||
class="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl hover:underline"
|
class='scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl hover:underline'>
|
||||||
>
|
<a href=`${import.meta.env.SITE}`> Konulu Konum</a>
|
||||||
<a href=`${import.meta.env.PUBLIC_HOME_URL}`> Konulu Konum</a>
|
|
||||||
</h1>
|
</h1>
|
||||||
<p class="leading-7 [&:not(:first-child)]:mt-6 text-xl">
|
<p class='leading-7 [&:not(:first-child)]:mt-6 text-2xl'>
|
||||||
Lock your files <b>based on a location</b> in three easy steps.
|
3 basit adımda fotoğraflarınızı ve videolarınızı <b
|
||||||
|
>yalnızca belirli bir konumda</b
|
||||||
|
> açılacak şekilde kilitleyin.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2
|
<h2
|
||||||
class="mt-10 scroll-m-20 border-b pb-2 text-2xl font-semibold tracking-tight transition-colors first:mt-0"
|
class='mt-10 scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0'>
|
||||||
>
|
1. Fotoğraf Seçimi
|
||||||
1. Choose a photo
|
|
||||||
</h2>
|
</h2>
|
||||||
<p class="leading-7 [&:not(:first-child)]:mt-6">
|
<p class='leading-7 [&:not(:first-child)]:mt-6 text-2xl'>
|
||||||
Select a photo, it'll be locked based on a location you'll choose.
|
Bir fotoğraf seçin.
|
||||||
</p>
|
</p>
|
||||||
<div class="grid w-full max-w-sm items-center gap-1.5 mt-4">
|
<div class='grid items-center gap-1.5 mt-4'>
|
||||||
<Input
|
<input
|
||||||
type="file"
|
type='file'
|
||||||
name="selected-photo"
|
name='selected-photo'
|
||||||
id="photo-selector"
|
id='photo-selector'
|
||||||
|
class='flex w-full p-2 border border-gray rounded-lg file:bg-inherit file:border-0'
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<p class="text-sm text-muted-foreground">Select a photo.</p>
|
<p class='px-2 text-lg text-muted-foreground'>
|
||||||
|
Galerinizden bir fotoğraf seçin.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2
|
<h2
|
||||||
class="mt-10 scroll-m-20 border-b pb-2 text-2xl font-semibold tracking-tight transition-colors first:mt-0"
|
class='mt-10 scroll-m-20 border-b pb-2 font-semibold tracking-tight transition-colors first:mt-0 text-3xl'>
|
||||||
>
|
2. Fotoğrafın Açılacağı Konum
|
||||||
2. Choose a location
|
|
||||||
</h2>
|
</h2>
|
||||||
<p class="leading-7 [&:not(:first-child)]:mt-6">
|
<p class='leading-7 [&:not(:first-child)]:mt-6 text-2xl'>
|
||||||
Choose a location on the map. Once the <i>konulu konum</i> link is generated,
|
Haritadan bir nokta seçin.
|
||||||
users will only be able to see this photo on this location.
|
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
id="map"
|
id='map'
|
||||||
class="w-full h-[450px] rounded mt-4 transition-colors ease-in-out border-2 duration-300"
|
class='w-full h-[450px] rounded mt-6 transition-colors ease-in-out border-2 duration-300'>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xl mt-2">
|
<p class='text-lg text-muted-foreground mt-2'>
|
||||||
<span class="font-semibold">Selected Location:</span>
|
Bağlantıyı gönderdiğiniz kişi bu konuma gittiği zaman seçtiğiniz
|
||||||
|
fotoğrafı görebilecek.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class='text-2xl mt-4 mb-2'>
|
||||||
|
<span class='font-semibold'>Seçilen Konum:</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
class="text-lg transition ease-in-out duration-700"
|
class='text-xl transition ease-in-out duration-700'
|
||||||
id="coordinates"
|
id='coordinates'>
|
||||||
>
|
Henüz konum seçmediniz, konum seçmek için haritadaki bir noktaya
|
||||||
You haven't chosen a location yet, please click on a point on the
|
basınız.
|
||||||
map to choose a location.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
class="text-lg text-muted-foreground mt-2"
|
class='text-lg text-muted-foreground mt-2'
|
||||||
id="location-selected-confirmation"
|
id='location-selected-confirmation'>
|
||||||
>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<input name="geolocation" id="geolocation-input" hidden />
|
<input name='geolocation' id='geolocation-input' hidden />
|
||||||
|
<input name='geolocation-radius' id='geolocation-radius-input' hidden />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2
|
<h2
|
||||||
class="mt-10 scroll-m-20 border-b pb-2 text-2xl font-semibold tracking-tight transition-colors first:mt-0"
|
class='mt-10 scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0'>
|
||||||
>
|
3. Fotoğraf Açıklaması
|
||||||
3. Photo description
|
|
||||||
</h2>
|
</h2>
|
||||||
<div class="grid w-full max-w-md items-center gap-2 mt-4">
|
<div class='grid w-full max-w-md items-center gap-4 mt-4'>
|
||||||
<div class="grid w-full items-center gap-1.5">
|
<div class='grid w-full items-center gap-2'>
|
||||||
<Label htmlFor="location-author" className="lg:text-lg text-md">
|
<label for='location-author' class='text-xl'> Gönderici: </label>
|
||||||
Sender
|
<input
|
||||||
</Label>
|
id='location-author'
|
||||||
<Input
|
name='author'
|
||||||
id="location-author"
|
placeholder='İsminizi buraya yazınız.'
|
||||||
name="author"
|
class='text-lg py-2 px-3 border border-gray rounded-lg placeholder:text-slate-400'
|
||||||
placeholder="Enter your name"
|
|
||||||
className="lg:text-lg text-md"
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid w-full items-center gap-1.5">
|
<div class='grid w-full items-center gap-2'>
|
||||||
<Label
|
<label for='location-description' class='text-xl'>
|
||||||
htmlFor="location-description"
|
Açıklama:
|
||||||
className="lg:text-lg text-md"
|
</label>
|
||||||
>
|
<textarea
|
||||||
Description
|
placeholder='Açıklamanızı buraya yazınız.'
|
||||||
</Label>
|
name='description'
|
||||||
<Textarea
|
id='location-description'
|
||||||
placeholder="Enter your description"
|
class='text-lg py-2 px-3 border border-gray rounded-lg placeholder:text-slate-400 resize-none'
|
||||||
name="description"
|
required></textarea>
|
||||||
id="location-description"
|
<p class='text-lg text-muted-foreground'>
|
||||||
className="lg:text-lg text-md"
|
Bir açıklama girin, yüklediğiniz içeriğin üzerine bu metin
|
||||||
required
|
görünecek.
|
||||||
/>
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
Enter a description, it'll be shown on top of the photo you
|
|
||||||
selected.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 my-6 items-center">
|
<hr class='my-4' />
|
||||||
<input type="checkbox" id="kvkk" class="w-6 h-6" required />
|
<div class='flex gap-2 my-6 items-center mt-4'>
|
||||||
<Label
|
<input type='checkbox' id='kvkk' class='w-6 h-6' required />
|
||||||
htmlFor="kvkk"
|
<label
|
||||||
className="text-lg font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
for='kvkk'
|
||||||
>
|
class='text-xl font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'>
|
||||||
I'm not uploading a personal data.
|
Konulu Konum'u KVKK kapsamında dava etmeyeceğimi onaylıyorum.
|
||||||
</Label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<button
|
||||||
className="w-full text-lg"
|
class='w-full text-lg bg-slate-900 text-white p-2 rounded-lg inline-flex justify-center items-center gap-2'
|
||||||
type="submit"
|
type='submit'
|
||||||
id="submit-button"
|
id='submit-button'>
|
||||||
size="lg"
|
<Loader2 id='submit-button-spinner' className='animate-spin hidden' />
|
||||||
>
|
Bağlantıyı Oluştur
|
||||||
<ReloadIcon
|
</button>
|
||||||
className="mr-2 h-4 w-4 animate-spin hidden"
|
|
||||||
id="submit-button-reload"
|
|
||||||
/>
|
|
||||||
Generate
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<script src="../scripts/initSelectionMap.js"></script>
|
<script src='../scripts/initSelectionMap.ts'></script>
|
||||||
|
<script src='../scripts/index.ts'></script>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
|
||||||
const handleSubmit = async (e: SubmitEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const locationSelected = document.getElementById(
|
|
||||||
'location-selected-confirmation'
|
|
||||||
)?.innerText;
|
|
||||||
|
|
||||||
if (!locationSelected) {
|
|
||||||
const coordinatesText = document.getElementById('coordinates');
|
|
||||||
|
|
||||||
const mapDiv = document.getElementById('map');
|
|
||||||
|
|
||||||
mapDiv?.classList.add('border-slate-700');
|
|
||||||
coordinatesText?.classList.add('drop-shadow-xl');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
mapDiv?.classList.remove('border-slate-700');
|
|
||||||
coordinatesText?.classList.remove('drop-shadow-xl');
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitButton = document.getElementById(
|
|
||||||
'submit-button'
|
|
||||||
) as HTMLButtonElement;
|
|
||||||
const reloadIcon = document.getElementById(
|
|
||||||
'submit-button-reload'
|
|
||||||
) as HTMLElement;
|
|
||||||
|
|
||||||
submitButton.disabled = true;
|
|
||||||
reloadIcon.classList.toggle('hidden');
|
|
||||||
|
|
||||||
const formData = new FormData(e.target as HTMLFormElement);
|
|
||||||
|
|
||||||
const res = await fetch(`/api/content`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
reloadIcon.classList.toggle('hidden');
|
|
||||||
submitButton.disabled = false;
|
|
||||||
|
|
||||||
if (res.status === 200) {
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
if (data.url) location.assign('/' + data.url);
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
Toastify({
|
|
||||||
text: 'Konulu konum oluşturulamadı, lütfen tekrar deneyin.',
|
|
||||||
duration: 3000,
|
|
||||||
gravity: 'top', // `top` or `bottom`
|
|
||||||
position: 'center', // `left`, `center` or `right`
|
|
||||||
stopOnFocus: true, // Prevents dismissing of toast on hover
|
|
||||||
style: {
|
|
||||||
background: 'black',
|
|
||||||
borderRadius: '6px',
|
|
||||||
margin: '16px',
|
|
||||||
},
|
|
||||||
onClick: function () {}, // Callback after click
|
|
||||||
}).showToast();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById('sample-form')!.onsubmit = handleSubmit;
|
|
||||||
</script>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
170
src/pages/x/index.astro
Normal file
170
src/pages/x/index.astro
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
---
|
||||||
|
// Styles
|
||||||
|
import "@/styles/globals.css"
|
||||||
|
import "@/styles/locked-page.css"
|
||||||
|
import "@/styles/locked-content.css"
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import Layout from "@/layouts/Layout.astro"
|
||||||
|
import ShareButton from "@/components/ShareButton"
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card"
|
||||||
|
import { CalendarIcon } from "@radix-ui/react-icons"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout>
|
||||||
|
<main class='flex flex-col gap-4'>
|
||||||
|
<div class='flex flex-col'>
|
||||||
|
<h1
|
||||||
|
class='scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl hover:underline'>
|
||||||
|
<a href=`${import.meta.env.SITE}`> Konulu Konum</a>
|
||||||
|
</h1>
|
||||||
|
<p class='[&:not(:first-child)]:mt-6 text-2xl'>
|
||||||
|
Size bir konulu konum bırakıldı, görüntüleyebilmek için haritadaki
|
||||||
|
konuma gitmeniz gerekiyor.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className='w-full'>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className='text-2xl' id='card-title' />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='text-xl'>
|
||||||
|
<p id='card-description'></p>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className='gap-2 text-lg'>
|
||||||
|
<CalendarIcon />
|
||||||
|
<p id='card-footer'></p>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id='locked-content-container'
|
||||||
|
class='w-full h-[475px] overflow-hidden border border-zinc-200 shadow-sm p-4 rounded'>
|
||||||
|
<div class='flex flex-col justify-center items-center image-wrapper'>
|
||||||
|
<img
|
||||||
|
id='content'
|
||||||
|
src='#'
|
||||||
|
class='h-[450px] blur-2xl transition-all duration-1000'
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
id='unlock-button-container'
|
||||||
|
class='flex flex-col justify-center gap-4 overlay'>
|
||||||
|
<button
|
||||||
|
id='unlock-content-button'
|
||||||
|
class='transition-[background-color] duration-1000 inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-11 rounded-md text-lg p-6 text-md'>
|
||||||
|
<svg
|
||||||
|
id='lock-icon'
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
width='28'
|
||||||
|
height='28'
|
||||||
|
fill='#ffffff'
|
||||||
|
viewBox='0 0 256 256'>
|
||||||
|
<path
|
||||||
|
d='M208,80H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80ZM96,56a32,32,0,0,1,64,0V80H96ZM208,208H48V96H208V208Zm-68-56a12,12,0,1,1-12-12A12,12,0,0,1,140,152Z'
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
id='unlock-icon'
|
||||||
|
class='hidden'
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
width='24'
|
||||||
|
height='24'
|
||||||
|
fill='#ffffff'
|
||||||
|
viewBox='0 0 256 256'>
|
||||||
|
<path
|
||||||
|
d='M208,80H96V56a32,32,0,0,1,32-32c15.37,0,29.2,11,32.16,25.59a8,8,0,0,0,15.68-3.18C171.32,24.15,151.2,8,128,8A48.05,48.05,0,0,0,80,56V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80Zm0,128H48V96H208V208Zm-68-56a12,12,0,1,1-12-12A12,12,0,0,1,140,152Z'
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<p id='button-text' class='text-xl'>İçerik Kilitli</p>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
class='rounded-lg border bg-card text-card-foreground shadow-sm p-2'>
|
||||||
|
<div class='pb-0 text-center flex flex-col gap-4'>
|
||||||
|
<p id='locked-content-description' class='text-xl'>
|
||||||
|
Ne kadar yaklaştığını görmek için aşağıdaki butona bas.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
id='location-permission-button'
|
||||||
|
class='text-xl inline-flex items-center justify-center whitespace-nowrap font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 text-primary-foreground h-10 rounded-md px-3 bg-green-700 hover:bg-green-600 text-md'>
|
||||||
|
Konum İzni Ver
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id='map' class='w-full h-[450px] rounded'></div>
|
||||||
|
<ShareButton client:only />
|
||||||
|
<div class='flex justify-center'>
|
||||||
|
<p class='text-muted-foreground'>
|
||||||
|
Fotoğrafın kilidi şu ana dek <b id='counter'></b> kez açıldı
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
import type { ContentTable } from "@/lib/db"
|
||||||
|
import { updateText } from "@/lib/domUtils"
|
||||||
|
|
||||||
|
// Dayjs
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import relativeTime from "dayjs/plugin/relativeTime"
|
||||||
|
import utc from "dayjs/plugin/utc"
|
||||||
|
|
||||||
|
// Map
|
||||||
|
import { initMap } from "@/scripts/initMap"
|
||||||
|
|
||||||
|
const url = new URL(document.URL).searchParams
|
||||||
|
const id = url.get("id")
|
||||||
|
|
||||||
|
type Content = ContentTable
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`${import.meta.env.PUBLIC_BACKEND_URL}/api/location/${id}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const data: Content | null = res.status === 200 ? await res.json() : null
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
|
dayjs.extend(utc)
|
||||||
|
|
||||||
|
const dateFromNow = dayjs.utc(data?.created_at).from(dayjs.utc())
|
||||||
|
|
||||||
|
updateText("card-title", data?.author ?? "")
|
||||||
|
updateText("card-description", data?.description ?? "")
|
||||||
|
updateText("card-footer", dateFromNow)
|
||||||
|
|
||||||
|
const lockedContentContainer = document.getElementById(
|
||||||
|
"locked-content-container"
|
||||||
|
)
|
||||||
|
if (lockedContentContainer) {
|
||||||
|
lockedContentContainer.dataset.targetPosition = data?.loc
|
||||||
|
}
|
||||||
|
|
||||||
|
const leafletMap = document.getElementById("map")
|
||||||
|
if (leafletMap) {
|
||||||
|
leafletMap.dataset.targetLocation = data?.loc
|
||||||
|
leafletMap.dataset.targetRadius = data?.radius.toString() ?? "50"
|
||||||
|
}
|
||||||
|
|
||||||
|
initMap()
|
||||||
|
|
||||||
|
const content = document.getElementById(
|
||||||
|
"content"
|
||||||
|
) as HTMLImageElement | null
|
||||||
|
if (content)
|
||||||
|
content.src = `${import.meta.env.PUBLIC_BACKEND_URL}/images/${data?.blob_url}`
|
||||||
|
|
||||||
|
const counter = document.getElementById("counter")
|
||||||
|
if (counter) counter.innerText = data?.unlocked_counter.toString() ?? ""
|
||||||
|
</script>
|
||||||
|
<script src='@/scripts/initMap.ts'></script>
|
||||||
|
<script src='@/scripts/lockedContent.ts'></script>
|
||||||
|
</Layout>
|
53
src/scripts/index.ts
Normal file
53
src/scripts/index.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { toggleButton, validateFileInput } from "@/lib/domUtils"
|
||||||
|
import { toast } from "@/lib/utils"
|
||||||
|
|
||||||
|
export function toggleMap() {
|
||||||
|
const coordinatesText = document.getElementById("coordinates")
|
||||||
|
|
||||||
|
const mapDiv = document.getElementById("map")
|
||||||
|
|
||||||
|
mapDiv?.classList.add("border-slate-900")
|
||||||
|
coordinatesText?.classList.add("drop-shadow-2xl")
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
mapDiv?.classList.remove("border-slate-900")
|
||||||
|
coordinatesText?.classList.remove("drop-shadow-2xl")
|
||||||
|
}, 800)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: SubmitEvent) => {
|
||||||
|
toggleButton("submit-button", "submit-button-spinner")
|
||||||
|
const locationSelected = document.getElementById(
|
||||||
|
"geolocation-input"
|
||||||
|
) as HTMLInputElement | null
|
||||||
|
|
||||||
|
if (!locationSelected) {
|
||||||
|
throw new Error("Element could not be found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!locationSelected.value) {
|
||||||
|
const map = document.getElementById("map")
|
||||||
|
map?.scrollIntoView({ behavior: "smooth", block: "center" })
|
||||||
|
return toggleMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputEl = document.getElementById(
|
||||||
|
"photo-selector"
|
||||||
|
) as HTMLInputElement | null
|
||||||
|
|
||||||
|
if (!inputEl) {
|
||||||
|
throw new Error("Element could not be found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
validateFileInput(inputEl)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("sample-form")!.onsubmit = handleSubmit
|
||||||
|
|
||||||
|
document.getElementById("photo-selector")!.oninput = (ev) =>
|
||||||
|
validateFileInput(ev.target as HTMLInputElement)
|
||||||
|
|
||||||
|
const url = new URL(document.URL)
|
||||||
|
|
||||||
|
if (url.searchParams.get("error"))
|
||||||
|
toast("Konulu konum oluşturulamadı, lütfen tekrar deneyin.")
|
|
@ -1,161 +0,0 @@
|
||||||
const data = JSON.parse(document.getElementById('map').dataset.targetLocation)
|
|
||||||
|
|
||||||
const TARGET_LOCATION = data.coordinates
|
|
||||||
|
|
||||||
function startWatchingLocation() {
|
|
||||||
map.locate({ watch: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
var map = L.map('map').setView(TARGET_LOCATION, 13);
|
|
||||||
|
|
||||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
||||||
maxZoom: 19,
|
|
||||||
attribution:
|
|
||||||
'© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
var targetLocationIcon = L.icon({
|
|
||||||
iconUrl: 'goal.svg',
|
|
||||||
iconSize: [32, 32],
|
|
||||||
});
|
|
||||||
|
|
||||||
L.marker(TARGET_LOCATION, { icon: targetLocationIcon }).addTo(map);
|
|
||||||
|
|
||||||
var circle = L.circle(TARGET_LOCATION, {
|
|
||||||
color: 'blue',
|
|
||||||
fillColor: '#30f',
|
|
||||||
fillOpacity: 0.2,
|
|
||||||
radius: 50
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
var currentLocationIcon = L.icon({
|
|
||||||
iconUrl: 'blue-dot.png',
|
|
||||||
iconSize: [32, 32],
|
|
||||||
});
|
|
||||||
|
|
||||||
let currentLocationMarker;
|
|
||||||
|
|
||||||
function onLocationError(err) {
|
|
||||||
let errorMessage;
|
|
||||||
switch (err.code) {
|
|
||||||
case 1:
|
|
||||||
errorMessage = 'Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin.'
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
errorMessage = 'Konumunuz tespit edilemedi, lütfen biraz sonra tekrar deneyiniz.'
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
errorMessage = 'Konum isteği zaman aşımına uğradı, lütfen sayfayı yenileyip tekrar deneyiniz.'
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
errorMessage = 'Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin.'
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// @ts-ignore
|
|
||||||
Toastify({
|
|
||||||
text: errorMessage,
|
|
||||||
duration: 3000,
|
|
||||||
gravity: 'top', // `top` or `bottom`
|
|
||||||
position: 'center', // `left`, `center` or `right`
|
|
||||||
stopOnFocus: true, // Prevents dismissing of toast on hover
|
|
||||||
style: {
|
|
||||||
background: 'black',
|
|
||||||
borderRadius: '6px',
|
|
||||||
margin: '16px',
|
|
||||||
},
|
|
||||||
onClick: function () { }, // Callback after click
|
|
||||||
}).showToast();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function onLocationSuccess(locationEvent) {
|
|
||||||
const position = locationEvent.latlng
|
|
||||||
|
|
||||||
const currentPos = {
|
|
||||||
lat: position.lat,
|
|
||||||
lng: position.lng
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentLocationMarker) {
|
|
||||||
currentLocationMarker.setLatLng(currentPos);
|
|
||||||
} else {
|
|
||||||
currentLocationMarker = L.marker(currentPos, { icon: currentLocationIcon });
|
|
||||||
currentLocationMarker.addTo(map);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
map.on('locationerror', onLocationError);
|
|
||||||
|
|
||||||
map.on('locationfound', onLocationSuccess)
|
|
||||||
|
|
||||||
|
|
||||||
L.Control.GoToCurrentLocation = L.Control.extend({
|
|
||||||
onAdd: function (map) {
|
|
||||||
const locationButton = document.createElement('button');
|
|
||||||
|
|
||||||
locationButton.textContent = 'Konum İzni Ver';
|
|
||||||
|
|
||||||
locationButton.classList.add('custom-map-control-button');
|
|
||||||
|
|
||||||
locationButton.type = 'button'
|
|
||||||
|
|
||||||
locationButton.addEventListener('click', (ev) => {
|
|
||||||
if (locationButton.textContent != 'Konumuma Git') {
|
|
||||||
startWatchingLocation()
|
|
||||||
locationButton.textContent = 'Konumuma Git';
|
|
||||||
} else {
|
|
||||||
if (currentLocationMarker) {
|
|
||||||
map.setView(currentLocationMarker.getLatLng(), 12);
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
Toastify({
|
|
||||||
text: 'Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin.',
|
|
||||||
duration: 3000,
|
|
||||||
gravity: 'top', // `top` or `bottom`
|
|
||||||
position: 'center', // `left`, `center` or `right`
|
|
||||||
stopOnFocus: true, // Prevents dismissing of toast on hover
|
|
||||||
style: {
|
|
||||||
background: 'black',
|
|
||||||
borderRadius: '6px',
|
|
||||||
margin: '16px',
|
|
||||||
},
|
|
||||||
onClick: function () { }, // Callback after click
|
|
||||||
}).showToast();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
L.DomEvent.stopPropagation(ev)
|
|
||||||
})
|
|
||||||
|
|
||||||
return locationButton;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
L.Control.GoToTargetLocation = L.Control.extend({
|
|
||||||
onAdd: function (map) {
|
|
||||||
const locationButton = document.createElement('button');
|
|
||||||
|
|
||||||
locationButton.textContent = 'Hedefe Git';
|
|
||||||
|
|
||||||
locationButton.classList.add('custom-map-control-button');
|
|
||||||
|
|
||||||
locationButton.addEventListener('click', () => {
|
|
||||||
map.setView(TARGET_LOCATION, 18);
|
|
||||||
});
|
|
||||||
|
|
||||||
return locationButton;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
L.control.currentLocation = function (opts) {
|
|
||||||
return new L.Control.GoToCurrentLocation(opts);
|
|
||||||
};
|
|
||||||
|
|
||||||
L.control.targetLocation = function (opts) {
|
|
||||||
return new L.Control.GoToTargetLocation(opts);
|
|
||||||
};
|
|
||||||
|
|
||||||
L.control.currentLocation({ position: 'bottomleft' }).addTo(map);
|
|
||||||
|
|
||||||
L.control.targetLocation({ position: 'bottomleft' }).addTo(map);
|
|
||||||
|
|
120
src/scripts/initMap.ts
Normal file
120
src/scripts/initMap.ts
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
import {
|
||||||
|
currentLocationIcon,
|
||||||
|
targetLocationIcon,
|
||||||
|
} from "@/components/Leaflet/icons"
|
||||||
|
import { toast } from "@/lib/utils"
|
||||||
|
import L from "leaflet"
|
||||||
|
|
||||||
|
var map: L.Map
|
||||||
|
const mapEl = document.getElementById("map")
|
||||||
|
|
||||||
|
type TargetLocation = [lat: number, lng: number] | null
|
||||||
|
|
||||||
|
let currentLocationMarker: L.Marker
|
||||||
|
|
||||||
|
export function mapLocationSuccessCallback(position: GeolocationPosition) {
|
||||||
|
const currentPos = {
|
||||||
|
lat: position.coords.latitude,
|
||||||
|
lng: position.coords.longitude,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLocationMarker) {
|
||||||
|
currentLocationMarker.setLatLng(currentPos)
|
||||||
|
} else {
|
||||||
|
currentLocationMarker = L.marker(currentPos, { icon: currentLocationIcon })
|
||||||
|
currentLocationMarker.addTo(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CurrentLocation = L.Control.extend({
|
||||||
|
onAdd: function (map: L.Map) {
|
||||||
|
const locationButton = document.createElement("button")
|
||||||
|
|
||||||
|
locationButton.textContent = "Konumuma Git"
|
||||||
|
|
||||||
|
locationButton.classList.add("custom-map-control-button", "disabled-button")
|
||||||
|
|
||||||
|
locationButton.type = "button"
|
||||||
|
|
||||||
|
locationButton.id = "current-location-control"
|
||||||
|
|
||||||
|
locationButton.addEventListener("click", () => {
|
||||||
|
if (currentLocationMarker) {
|
||||||
|
map.setView(currentLocationMarker.getLatLng(), 12)
|
||||||
|
} else {
|
||||||
|
toast("Konumunuza erişilemiyor, lütfen biraz bekleyip tekrar deneyin.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return locationButton
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const GoToTargetLocation = L.Control.extend({
|
||||||
|
onAdd: function (map: L.Map) {
|
||||||
|
const locationButton = document.createElement("button")
|
||||||
|
|
||||||
|
locationButton.textContent = "Hedefe Git"
|
||||||
|
|
||||||
|
locationButton.classList.add("custom-map-control-button")
|
||||||
|
|
||||||
|
locationButton.addEventListener("click", () => {
|
||||||
|
const targetLocation = getTargetLocation()
|
||||||
|
if (targetLocation) map.setView(targetLocation, 12)
|
||||||
|
})
|
||||||
|
|
||||||
|
return locationButton
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const goToCurrentLocationControl = new CurrentLocation({
|
||||||
|
position: "bottomleft",
|
||||||
|
})
|
||||||
|
|
||||||
|
const targetLocationControl = new GoToTargetLocation({
|
||||||
|
position: "bottomleft",
|
||||||
|
})
|
||||||
|
|
||||||
|
function addTargetLocationMarker(target: TargetLocation, radius = 50) {
|
||||||
|
if (target) {
|
||||||
|
L.marker(target, { icon: targetLocationIcon }).addTo(map)
|
||||||
|
|
||||||
|
L.circle(target, {
|
||||||
|
color: "blue",
|
||||||
|
fillColor: "#30f",
|
||||||
|
fillOpacity: 0.2,
|
||||||
|
radius,
|
||||||
|
}).addTo(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTargetLocation(): TargetLocation | null {
|
||||||
|
const targetLocation = mapEl?.dataset.targetLocation
|
||||||
|
|
||||||
|
const data = targetLocation ? JSON.parse(targetLocation) : null
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initMap() {
|
||||||
|
map = L.map("map")
|
||||||
|
|
||||||
|
const targetLocation = mapEl?.dataset.targetLocation
|
||||||
|
const targetRadius = mapEl?.dataset.targetRadius
|
||||||
|
|
||||||
|
const data = targetLocation ? JSON.parse(targetLocation) : null
|
||||||
|
|
||||||
|
const TARGET_LOCATION = data as TargetLocation
|
||||||
|
|
||||||
|
if (TARGET_LOCATION) map.setView(TARGET_LOCATION, 13)
|
||||||
|
|
||||||
|
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution:
|
||||||
|
'© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||||
|
}).addTo(map)
|
||||||
|
|
||||||
|
addTargetLocationMarker(TARGET_LOCATION, Number(targetRadius))
|
||||||
|
targetLocationControl.addTo(map)
|
||||||
|
goToCurrentLocationControl.addTo(map)
|
||||||
|
}
|
|
@ -1,119 +0,0 @@
|
||||||
import { onLocationError } from "@/lib/error";
|
|
||||||
|
|
||||||
var map = L.map('map').setView([41.024857599001905, 28.940787550837882], 10);
|
|
||||||
|
|
||||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
||||||
maxZoom: 19,
|
|
||||||
attribution:
|
|
||||||
'© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
let targetLocationMarker;
|
|
||||||
|
|
||||||
let targetLocationCircle;
|
|
||||||
|
|
||||||
var targetLocationIcon = L.icon({
|
|
||||||
iconUrl: 'goal.svg',
|
|
||||||
iconSize: [32, 32],
|
|
||||||
});
|
|
||||||
|
|
||||||
var currentLocationIcon = L.icon({
|
|
||||||
iconUrl: 'blue-dot.png',
|
|
||||||
iconSize: [32, 32],
|
|
||||||
});
|
|
||||||
|
|
||||||
let currentLocationMarker;
|
|
||||||
|
|
||||||
function startWatchingLocation() {
|
|
||||||
map.locate({ watch: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLocationSuccess(locationEvent) {
|
|
||||||
const position = locationEvent.latlng
|
|
||||||
|
|
||||||
const currentPos = {
|
|
||||||
lat: position.lat,
|
|
||||||
lng: position.lng
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentLocationMarker) {
|
|
||||||
currentLocationMarker.setLatLng(currentPos);
|
|
||||||
} else {
|
|
||||||
currentLocationMarker = L.marker(currentPos, { icon: currentLocationIcon });
|
|
||||||
currentLocationMarker.addTo(map);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
map.on('locationerror', onLocationError);
|
|
||||||
|
|
||||||
map.on('locationfound', onLocationSuccess)
|
|
||||||
|
|
||||||
L.Control.GoToCurrentLocation = L.Control.extend({
|
|
||||||
onAdd: function (map) {
|
|
||||||
const locationButton = document.createElement('button');
|
|
||||||
|
|
||||||
locationButton.textContent = 'Konum İzni Ver';
|
|
||||||
|
|
||||||
locationButton.classList.add('custom-map-control-button');
|
|
||||||
|
|
||||||
locationButton.type = 'button'
|
|
||||||
|
|
||||||
locationButton.addEventListener('click', (ev) => {
|
|
||||||
if (locationButton.textContent != 'Konumuma Git') {
|
|
||||||
startWatchingLocation()
|
|
||||||
locationButton.textContent = 'Konumuma Git';
|
|
||||||
} else {
|
|
||||||
if (currentLocationMarker) {
|
|
||||||
map.setView(currentLocationMarker.getLatLng(), 12);
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
Toastify({
|
|
||||||
text: 'Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin.',
|
|
||||||
duration: 3000,
|
|
||||||
gravity: 'top', // `top` or `bottom`
|
|
||||||
position: 'center', // `left`, `center` or `right`
|
|
||||||
stopOnFocus: true, // Prevents dismissing of toast on hover
|
|
||||||
style: {
|
|
||||||
background: 'black',
|
|
||||||
borderRadius: '6px',
|
|
||||||
margin: '16px',
|
|
||||||
},
|
|
||||||
onClick: function () { }, // Callback after click
|
|
||||||
}).showToast();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
L.DomEvent.stopPropagation(ev)
|
|
||||||
})
|
|
||||||
|
|
||||||
return locationButton;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
L.control.currentLocation = function (opts) {
|
|
||||||
return new L.Control.GoToCurrentLocation(opts);
|
|
||||||
};
|
|
||||||
|
|
||||||
L.control.currentLocation({ position: 'bottomleft' }).addTo(map);
|
|
||||||
|
|
||||||
|
|
||||||
map.on('click', (e) => {
|
|
||||||
if (targetLocationMarker) {
|
|
||||||
targetLocationMarker.setLatLng(e.latlng)
|
|
||||||
targetLocationCircle.setLatLng(e.latlng)
|
|
||||||
} else {
|
|
||||||
targetLocationMarker = L.marker(e.latlng, { icon: targetLocationIcon }).addTo(map);
|
|
||||||
|
|
||||||
targetLocationCircle = L.circle(e.latlng, {
|
|
||||||
color: 'blue',
|
|
||||||
fillColor: '#30f',
|
|
||||||
fillOpacity: 0.2,
|
|
||||||
radius: 50
|
|
||||||
}).addTo(map);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pos = targetLocationMarker.getLatLng()
|
|
||||||
document.getElementById('coordinates').innerText = `${pos.lat}. Enlem, ${pos.lng}. Boylam`
|
|
||||||
document.getElementById('geolocation-input').value = `${pos.lat},${pos.lng}`
|
|
||||||
document.getElementById('location-selected-confirmation').innerText = "Konum seçildi, bir sonraki adıma geçebilirsiniz."
|
|
||||||
})
|
|
240
src/scripts/initSelectionMap.ts
Normal file
240
src/scripts/initSelectionMap.ts
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
import L from "leaflet"
|
||||||
|
import { openstreetmapTiles } from "@/components/Leaflet/constants"
|
||||||
|
import { toast } from "@/lib/utils"
|
||||||
|
import {
|
||||||
|
currentLocationIcon,
|
||||||
|
targetLocationIcon,
|
||||||
|
} from "@/components/Leaflet/icons"
|
||||||
|
import {
|
||||||
|
addClasses,
|
||||||
|
removeClasses,
|
||||||
|
updateInputValue,
|
||||||
|
updateText,
|
||||||
|
} from "@/lib/domUtils"
|
||||||
|
import { updateMarkerLocation } from "@/components/Leaflet/geolocation"
|
||||||
|
|
||||||
|
var map = L.map("map").setView([41.024857599001905, 28.940787550837882], 10)
|
||||||
|
|
||||||
|
openstreetmapTiles.addTo(map)
|
||||||
|
|
||||||
|
let targetLocationMarker: L.Marker
|
||||||
|
|
||||||
|
let targetLocationCircle: L.Circle
|
||||||
|
|
||||||
|
let targetLocationCircleRadius = 50
|
||||||
|
|
||||||
|
let currentLocationMarker: L.Marker
|
||||||
|
|
||||||
|
map.on("locationerror", () =>
|
||||||
|
toast(
|
||||||
|
"Konum izni alınamadı, lütfen tarayıcınızın ve cihazınızın gizlilik ayarlarını kontrol edin."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
map.on("locationfound", (ev) => {
|
||||||
|
removeClasses("current-location-control", "disabled-button")
|
||||||
|
addClasses("ask-permission-control", "disabled-button")
|
||||||
|
|
||||||
|
currentLocationMarker = updateMarkerLocation(
|
||||||
|
currentLocationMarker,
|
||||||
|
currentLocationIcon,
|
||||||
|
ev.latlng,
|
||||||
|
map
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const CurrentLocationControl = L.Control.extend({
|
||||||
|
onAdd: function (map: L.Map) {
|
||||||
|
const locationButton = document.createElement("button")
|
||||||
|
|
||||||
|
locationButton.textContent = "Konumuma Git"
|
||||||
|
|
||||||
|
locationButton.classList.add("custom-map-control-button", "disabled-button")
|
||||||
|
|
||||||
|
locationButton.type = "button"
|
||||||
|
|
||||||
|
locationButton.id = "current-location-control"
|
||||||
|
|
||||||
|
locationButton.addEventListener("click", (ev) => {
|
||||||
|
if (currentLocationMarker) {
|
||||||
|
map.setView(currentLocationMarker.getLatLng(), 12)
|
||||||
|
} else {
|
||||||
|
toast("Konumunuza erişilemiyor, lütfen biraz bekleyip tekrar deneyin.")
|
||||||
|
}
|
||||||
|
L.DomEvent.stopPropagation(ev)
|
||||||
|
})
|
||||||
|
|
||||||
|
return locationButton
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const AskPermissonControl = L.Control.extend({
|
||||||
|
onAdd: function (map: L.Map) {
|
||||||
|
const locationButton = document.createElement("button")
|
||||||
|
|
||||||
|
locationButton.textContent = "Konum İzni Ver"
|
||||||
|
|
||||||
|
locationButton.classList.add("custom-map-control-button")
|
||||||
|
|
||||||
|
locationButton.type = "button"
|
||||||
|
|
||||||
|
locationButton.id = "ask-permission-control"
|
||||||
|
|
||||||
|
locationButton.addEventListener("click", (ev) => {
|
||||||
|
map.locate({ watch: true })
|
||||||
|
L.DomEvent.stopPropagation(ev)
|
||||||
|
})
|
||||||
|
|
||||||
|
return locationButton
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const RadiusControl = L.Control.extend({
|
||||||
|
onAdd: function (map: L.Map) {
|
||||||
|
const radiusContainer = document.createElement("div")
|
||||||
|
|
||||||
|
radiusContainer.classList.add(
|
||||||
|
"bg-white",
|
||||||
|
"h-[40px]",
|
||||||
|
"p-2",
|
||||||
|
"flex",
|
||||||
|
"items-center",
|
||||||
|
"gap-2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const radiusButtonClasses = [
|
||||||
|
"text-xl",
|
||||||
|
"border-2",
|
||||||
|
"border-slate-700",
|
||||||
|
"max-h-[30px]",
|
||||||
|
"flex",
|
||||||
|
"items-center",
|
||||||
|
"p-2",
|
||||||
|
"rounded-lg",
|
||||||
|
"bg-gray-100",
|
||||||
|
"hover:bg-gray-300",
|
||||||
|
]
|
||||||
|
|
||||||
|
const radiusIncreaseButton = document.createElement("button")
|
||||||
|
|
||||||
|
radiusIncreaseButton.classList.add(...radiusButtonClasses)
|
||||||
|
|
||||||
|
const radiusDecreaseButton = document.createElement("button")
|
||||||
|
|
||||||
|
radiusIncreaseButton.type = "button"
|
||||||
|
|
||||||
|
radiusDecreaseButton.type = "button"
|
||||||
|
|
||||||
|
radiusDecreaseButton.classList.add(...radiusButtonClasses)
|
||||||
|
|
||||||
|
const radiusContainerText = document.createElement("p")
|
||||||
|
|
||||||
|
const radiusText = document.createElement("p")
|
||||||
|
|
||||||
|
radiusContainerText.classList.add("text-xl")
|
||||||
|
|
||||||
|
radiusText.classList.add("text-xl")
|
||||||
|
|
||||||
|
radiusIncreaseButton.textContent = "+"
|
||||||
|
|
||||||
|
radiusDecreaseButton.textContent = "-"
|
||||||
|
|
||||||
|
radiusContainerText.textContent = "Yarıçap: "
|
||||||
|
|
||||||
|
radiusText.textContent = `${targetLocationCircleRadius.toString()}m`
|
||||||
|
|
||||||
|
radiusContainer.insertAdjacentElement("afterbegin", radiusIncreaseButton)
|
||||||
|
|
||||||
|
radiusContainer.insertAdjacentElement("afterbegin", radiusText)
|
||||||
|
|
||||||
|
radiusContainer.insertAdjacentElement("afterbegin", radiusDecreaseButton)
|
||||||
|
|
||||||
|
radiusContainer.insertAdjacentElement("afterbegin", radiusContainerText)
|
||||||
|
|
||||||
|
radiusContainer.id = "radius-control"
|
||||||
|
|
||||||
|
L.DomEvent.on(radiusIncreaseButton, "click", (ev) => {
|
||||||
|
targetLocationCircleRadius = Math.min(
|
||||||
|
targetLocationCircleRadius + 100,
|
||||||
|
2000
|
||||||
|
)
|
||||||
|
targetLocationCircle.setRadius(targetLocationCircleRadius)
|
||||||
|
|
||||||
|
radiusText.innerText = `${targetLocationCircleRadius.toString()}m`
|
||||||
|
updateInputValue(
|
||||||
|
"geolocation-radius-input",
|
||||||
|
targetLocationCircleRadius.toString()
|
||||||
|
)
|
||||||
|
L.DomEvent.stop(ev)
|
||||||
|
})
|
||||||
|
|
||||||
|
L.DomEvent.on(radiusIncreaseButton, "dblclick", (ev) => L.DomEvent.stop(ev))
|
||||||
|
|
||||||
|
L.DomEvent.on(radiusDecreaseButton, "click", (ev) => {
|
||||||
|
targetLocationCircleRadius = Math.max(
|
||||||
|
targetLocationCircleRadius - 100,
|
||||||
|
50
|
||||||
|
)
|
||||||
|
targetLocationCircle.setRadius(targetLocationCircleRadius)
|
||||||
|
radiusText.innerText = `${targetLocationCircleRadius.toString()}m`
|
||||||
|
updateInputValue(
|
||||||
|
"geolocation-radius-input",
|
||||||
|
targetLocationCircleRadius.toString()
|
||||||
|
)
|
||||||
|
|
||||||
|
L.DomEvent.stop(ev)
|
||||||
|
})
|
||||||
|
|
||||||
|
L.DomEvent.on(radiusDecreaseButton, "dblclick", (ev) => L.DomEvent.stop(ev))
|
||||||
|
|
||||||
|
return radiusContainer
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const askPermissionControl = new AskPermissonControl({ position: "bottomleft" })
|
||||||
|
|
||||||
|
const currentLocationControl = new CurrentLocationControl({
|
||||||
|
position: "bottomleft",
|
||||||
|
})
|
||||||
|
|
||||||
|
const radiusControl = new RadiusControl({ position: "topright" })
|
||||||
|
|
||||||
|
askPermissionControl.addTo(map)
|
||||||
|
|
||||||
|
currentLocationControl.addTo(map)
|
||||||
|
|
||||||
|
map.on("click", (e) => {
|
||||||
|
if (targetLocationMarker) {
|
||||||
|
targetLocationMarker.setLatLng(e.latlng)
|
||||||
|
targetLocationCircle.setLatLng(e.latlng)
|
||||||
|
} else {
|
||||||
|
targetLocationMarker = L.marker(e.latlng, {
|
||||||
|
icon: targetLocationIcon,
|
||||||
|
}).addTo(map)
|
||||||
|
|
||||||
|
targetLocationCircle = L.circle(e.latlng, {
|
||||||
|
color: "blue",
|
||||||
|
fillColor: "#30f",
|
||||||
|
fillOpacity: 0.2,
|
||||||
|
radius: targetLocationCircleRadius,
|
||||||
|
}).addTo(map)
|
||||||
|
|
||||||
|
radiusControl.addTo(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = targetLocationMarker.getLatLng()
|
||||||
|
updateInputValue("geolocation-input", `${pos.lat},${pos.lng}`)
|
||||||
|
updateText("coordinates", `${pos.lat}. Enlem, ${pos.lng}. Boylam`)
|
||||||
|
updateText(
|
||||||
|
"location-selected-confirmation",
|
||||||
|
"Konum seçildi, bir sonraki adıma geçebilirsiniz."
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const geolocationInputEl = document.getElementById(
|
||||||
|
"geolocation-input"
|
||||||
|
) as HTMLInputElement | null
|
||||||
|
|
||||||
|
if (geolocationInputEl?.value) {
|
||||||
|
geolocationInputEl.value = ""
|
||||||
|
}
|
94
src/scripts/lockedContent.ts
Normal file
94
src/scripts/lockedContent.ts
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import {
|
||||||
|
errorCallback,
|
||||||
|
locationSuccessCallback,
|
||||||
|
} from "@/components/LockedContent/geolocation"
|
||||||
|
import type { LatLngTuple } from "leaflet"
|
||||||
|
|
||||||
|
// Geolocation watch id to avoid duplicate watchPosition calls
|
||||||
|
let watchId: number
|
||||||
|
|
||||||
|
// DOM ELEMENTS
|
||||||
|
const locationPermissionButton = document.getElementById(
|
||||||
|
"location-permission-button"
|
||||||
|
)
|
||||||
|
|
||||||
|
const unlockButton = document.getElementById("unlock-content-button")
|
||||||
|
|
||||||
|
const lockedContentContainer = document.getElementById(
|
||||||
|
"locked-content-container"
|
||||||
|
)
|
||||||
|
|
||||||
|
// These elements MUST be defined
|
||||||
|
// Throw error if they are not found
|
||||||
|
if (!locationPermissionButton || !lockedContentContainer || !unlockButton) {
|
||||||
|
throw new Error("Element not found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// EVENT LISTENERS
|
||||||
|
locationPermissionButton.addEventListener("click", startWatchingLocation)
|
||||||
|
|
||||||
|
lockedContentContainer.addEventListener("askpermission", startWatchingLocation)
|
||||||
|
|
||||||
|
// Get target position from container's dataset
|
||||||
|
function getTargetPosition() {
|
||||||
|
const lockedContentContainer = document.getElementById(
|
||||||
|
"locked-content-container"
|
||||||
|
)
|
||||||
|
|
||||||
|
const targetPositionString = lockedContentContainer?.dataset.targetPosition
|
||||||
|
|
||||||
|
// TARGET_POSITION is required to calculate distance
|
||||||
|
if (!targetPositionString) throw new Error("Target position is not provided!")
|
||||||
|
|
||||||
|
const data = JSON.parse(targetPositionString) as LatLngTuple
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRadius() {
|
||||||
|
const leafletMap = document.getElementById("map")
|
||||||
|
|
||||||
|
let targetRadiusString = leafletMap?.dataset.targetRadius
|
||||||
|
|
||||||
|
// TARGET_POSITION is required to calculate distance
|
||||||
|
if (!targetRadiusString) targetRadiusString = "50"
|
||||||
|
|
||||||
|
const data = Number(targetRadiusString)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Geolocation API to start watching user location
|
||||||
|
function startWatchingLocation() {
|
||||||
|
const TARGET_POSITION = getTargetPosition()
|
||||||
|
const radius = getRadius()
|
||||||
|
|
||||||
|
if (!watchId) {
|
||||||
|
watchId = window.navigator.geolocation.watchPosition(
|
||||||
|
(position) => locationSuccessCallback(position, TARGET_POSITION, radius),
|
||||||
|
errorCallback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the web page is loaded, check if user has given geolocation
|
||||||
|
// permission before
|
||||||
|
navigator.permissions
|
||||||
|
.query({ name: "geolocation" })
|
||||||
|
.then((permissionStatus) => {
|
||||||
|
switch (permissionStatus.state) {
|
||||||
|
case "granted":
|
||||||
|
const TARGET_POSITION = getTargetPosition()
|
||||||
|
const radius = getRadius()
|
||||||
|
watchId = window.navigator.geolocation.watchPosition(
|
||||||
|
(position) =>
|
||||||
|
locationSuccessCallback(position, TARGET_POSITION, radius),
|
||||||
|
errorCallback
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case "denied":
|
||||||
|
case "prompt":
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
|
@ -1,11 +1,24 @@
|
||||||
.custom-map-control-button {
|
.custom-map-control-button {
|
||||||
|
@apply text-xl;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 1px 4px -1px rgba(0, 0, 0, 0.3);
|
||||||
|
margin: 10px;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-map-control-div {
|
||||||
|
@apply text-xl;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
box-shadow: 0 1px 4px -1px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 1px 4px -1px rgba(0, 0, 0, 0.3);
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
padding: 0 0.5em;
|
padding: 0 0.5em;
|
||||||
font: 400 18px Roboto, Arial, sans-serif;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -14,3 +27,12 @@
|
||||||
.custom-map-control-button:hover {
|
.custom-map-control-button:hover {
|
||||||
background: rgb(235, 235, 235);
|
background: rgb(235, 235, 235);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled-button {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled-button:hover {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
|
@ -1,21 +1,26 @@
|
||||||
export default function distance(lat1, lon1, lat2, lon2, unit = 'K') {
|
export default function distance(lat1, lon1, lat2, lon2, unit = "K") {
|
||||||
if ((lat1 == lat2) && (lon1 == lon2)) {
|
if (lat1 == lat2 && lon1 == lon2) {
|
||||||
return 0;
|
return 0
|
||||||
}
|
} else {
|
||||||
else {
|
var radlat1 = (Math.PI * lat1) / 180
|
||||||
var radlat1 = Math.PI * lat1 / 180;
|
var radlat2 = (Math.PI * lat2) / 180
|
||||||
var radlat2 = Math.PI * lat2 / 180;
|
var theta = lon1 - lon2
|
||||||
var theta = lon1 - lon2;
|
var radtheta = (Math.PI * theta) / 180
|
||||||
var radtheta = Math.PI * theta / 180;
|
var dist =
|
||||||
var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
|
Math.sin(radlat1) * Math.sin(radlat2) +
|
||||||
|
Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta)
|
||||||
if (dist > 1) {
|
if (dist > 1) {
|
||||||
dist = 1;
|
dist = 1
|
||||||
}
|
}
|
||||||
dist = Math.acos(dist);
|
dist = Math.acos(dist)
|
||||||
dist = dist * 180 / Math.PI;
|
dist = (dist * 180) / Math.PI
|
||||||
dist = dist * 60 * 1.1515;
|
dist = dist * 60 * 1.1515
|
||||||
if (unit == "K") { dist = dist * 1.609344 }
|
if (unit == "K") {
|
||||||
if (unit == "N") { dist = dist * 0.8684 }
|
dist = dist * 1.609344
|
||||||
return dist;
|
}
|
||||||
|
if (unit == "N") {
|
||||||
|
dist = dist * 0.8684
|
||||||
|
}
|
||||||
|
return dist
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,9 @@
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
},
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user