Vipps Donations #33
|
|
@ -2,8 +2,13 @@ import config from "../deno.json" with { type: "json" };
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 border-t-2 pt-4">
|
<div className={"p-4 mx-auto max-w-screen-md"}>
|
||||||
<a class="text-blue-600 hover:underline" href={config.source}>Kildekode</a>
|
<div className="mt-4 border-t-2 pt-4">
|
||||||
|
<a class="text-blue-600 hover:underline" href={config.source}>Kildekode</a>
|
||||||
|
<p>
|
||||||
|
Krets AS - 922 739 625
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,3 @@ egen app fremfor å bygge oppunder åpne standarder som RSS.
|
||||||
Søk på NRK-podcasten du vil høre på. Kopier deretter URL-en under bildet. Lim denne inn i akkurat den
|
Søk på NRK-podcasten du vil høre på. Kopier deretter URL-en under bildet. Lim denne inn i akkurat den
|
||||||
podcastspilleren du måtte foretrekke! Se [her](https://help.omnystudio.com/en/articles/5222518-podcast-apps-that-support-add-rss-feed)
|
podcastspilleren du måtte foretrekke! Se [her](https://help.omnystudio.com/en/articles/5222518-podcast-apps-that-support-add-rss-feed)
|
||||||
for en oversikt over hvordan det gjøres i populære podcastspillere.
|
for en oversikt over hvordan det gjøres i populære podcastspillere.
|
||||||
|
|
||||||
## Penger og sånt
|
|
||||||
|
|
||||||
Denne siden har mye trafikk! Det er jeg veldig takknemlig for. Imidlertid koster det derfor en del penger å ha det kjørende. _Dersom du har anledning til det_ (og bare da!), oppfordrer jeg til å [slenge over en kaffeslant eller to](/donations) slik at NRSS fortsatt kan være gratis tilgjengelig for de som ikke har det.
|
|
||||||
|
|
|
||||||
12
fresh.gen.ts
12
fresh.gen.ts
|
|
@ -5,9 +5,13 @@
|
||||||
import * as $_app from "./routes/_app.tsx";
|
import * as $_app from "./routes/_app.tsx";
|
||||||
import * as $api_feeds_seriesId_ from "./routes/api/feeds/[seriesId].ts";
|
import * as $api_feeds_seriesId_ from "./routes/api/feeds/[seriesId].ts";
|
||||||
import * as $api_feeds_seriesId_episodeId_chapters from "./routes/api/feeds/[seriesId]/[episodeId]/chapters.ts";
|
import * as $api_feeds_seriesId_episodeId_chapters from "./routes/api/feeds/[seriesId]/[episodeId]/chapters.ts";
|
||||||
import * as $donations from "./routes/donations.tsx";
|
import * as $api_trigger_donation_vipps from "./routes/api/trigger-donation/vipps.ts";
|
||||||
|
import * as $donations_cancel from "./routes/donations-cancel.tsx";
|
||||||
|
import * as $donations_error from "./routes/donations-error.tsx";
|
||||||
|
import * as $donations_success from "./routes/donations-success.tsx";
|
||||||
import * as $index from "./routes/index.tsx";
|
import * as $index from "./routes/index.tsx";
|
||||||
import * as $CopyButton from "./islands/CopyButton.tsx";
|
import * as $CopyButton from "./islands/CopyButton.tsx";
|
||||||
|
import * as $DonationSection from "./islands/DonationSection.tsx";
|
||||||
import { type Manifest } from "$fresh/server.ts";
|
import { type Manifest } from "$fresh/server.ts";
|
||||||
|
|
||||||
const manifest = {
|
const manifest = {
|
||||||
|
|
@ -15,11 +19,15 @@ const manifest = {
|
||||||
"./routes/_app.tsx": $_app,
|
"./routes/_app.tsx": $_app,
|
||||||
"./routes/api/feeds/[seriesId].ts": $api_feeds_seriesId_,
|
"./routes/api/feeds/[seriesId].ts": $api_feeds_seriesId_,
|
||||||
"./routes/api/feeds/[seriesId]/[episodeId]/chapters.ts": $api_feeds_seriesId_episodeId_chapters,
|
"./routes/api/feeds/[seriesId]/[episodeId]/chapters.ts": $api_feeds_seriesId_episodeId_chapters,
|
||||||
"./routes/donations.tsx": $donations,
|
"./routes/api/trigger-donation/vipps.ts": $api_trigger_donation_vipps,
|
||||||
|
"./routes/donations-cancel.tsx": $donations_cancel,
|
||||||
|
"./routes/donations-error.tsx": $donations_error,
|
||||||
|
"./routes/donations-success.tsx": $donations_success,
|
||||||
"./routes/index.tsx": $index,
|
"./routes/index.tsx": $index,
|
||||||
},
|
},
|
||||||
islands: {
|
islands: {
|
||||||
"./islands/CopyButton.tsx": $CopyButton,
|
"./islands/CopyButton.tsx": $CopyButton,
|
||||||
|
"./islands/DonationSection.tsx": $DonationSection,
|
||||||
},
|
},
|
||||||
baseUrl: import.meta.url,
|
baseUrl: import.meta.url,
|
||||||
} satisfies Manifest;
|
} satisfies Manifest;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import { Input } from "../components/Input.tsx";
|
||||||
|
|
||||||
|
export const DonationSection = function () {
|
||||||
|
const [emailInput, setEmailInput] = useState<string>("");
|
||||||
|
const [emailValid, setEmailValid] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const valid = emailInput.match(
|
||||||
|
// https://emailregex.com/
|
||||||
|
// deno-lint-ignore no-control-regex
|
||||||
|
/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/,
|
||||||
|
);
|
||||||
|
setEmailValid(!!valid);
|
||||||
|
}, [emailInput]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<script
|
||||||
|
async
|
||||||
|
type="text/javascript"
|
||||||
|
src="https://checkout.vipps.no/checkout-button/v1/vipps-checkout-button.js"
|
||||||
|
>
|
||||||
|
</script>
|
||||||
|
<div className="max-w mx-auto py-4">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">
|
||||||
|
Økonomisk Støtte
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
Jeg ønsker at NRSS skal være gratis tilgjengelig for alle. Imidlertid koster det penger og tid å drifte og
|
||||||
|
vedlikeholde en nettside. Dersom du har råd til det (og bare da!) setter jeg stor pris på om du vil støtte
|
||||||
|
prosjektet med et valgfritt, månedlig beløp via Vipps.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="justify-center my-8">
|
||||||
|
<form className={"flex"}>
|
||||||
|
<Input
|
||||||
|
type={"email"}
|
||||||
|
placeholder={"din@epost.no"}
|
||||||
|
required
|
||||||
|
onInput={(e) => {
|
||||||
|
setEmailInput(e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
</Input>
|
||||||
|
<a
|
||||||
|
// don't navigate if email is not valid
|
||||||
|
href={emailInput ? "/api/trigger-donation/vipps" : undefined}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (!emailValid) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<vipps-mobilepay-button
|
||||||
|
type="submit"
|
||||||
|
brand="vipps"
|
||||||
|
language="no"
|
||||||
|
variant="primary"
|
||||||
|
rounded="false"
|
||||||
|
verb="pay"
|
||||||
|
stretched="true"
|
||||||
|
branded="true"
|
||||||
|
loading="false"
|
||||||
|
>
|
||||||
|
</vipps-mobilepay-button>
|
||||||
|
</a>
|
||||||
|
valid: {emailValid.toString()}
|
||||||
|
</form>
|
||||||
|
<p
|
||||||
|
className={"text-sm text-gray-500"}
|
||||||
|
>
|
||||||
|
Eposten brukes utelukkende som referanse dersom du ønsker å avslutte støtten. Du kommer ikke til å motta
|
||||||
|
noen eposter.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<b>
|
||||||
|
Du kan når som helst avslutte støtten fra{" "}
|
||||||
|
<a className="text-blue-600 underline" href="/donations-cancel">
|
||||||
|
denne siden
|
||||||
|
</a>.
|
||||||
|
</b>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-4">
|
||||||
|
Opplever du problemer med betalingen, eller har du andre spørsmål? Ta{" "}
|
||||||
|
<a className="text-blue-600 underline" href="mailto:olav@sundfoer.com">
|
||||||
|
kontakt på mail
|
||||||
|
</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -24,20 +24,20 @@ function seriesKey(series: { id: string }) {
|
||||||
return ["series", series.id];
|
return ["series", series.id];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function read(options: { id: string }): Promise<Series | null> {
|
async function readSeries(options: { id: string }): Promise<Series | null> {
|
||||||
const { id } = options;
|
const { id } = options;
|
||||||
const key = seriesKey({ id });
|
const key = seriesKey({ id });
|
||||||
const read = await kv.get<Series>(key);
|
const read = await kv.get<Series>(key);
|
||||||
return read.value;
|
return read.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function write(series: Series): Promise<boolean> {
|
async function writeSeries(series: Series): Promise<boolean> {
|
||||||
const key = seriesKey(series);
|
const key = seriesKey(series);
|
||||||
const stored = await kv.set(key, series);
|
const stored = await kv.set(key, series);
|
||||||
return stored.ok;
|
return stored.ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const storage = {
|
export const storage = {
|
||||||
read,
|
readSeries,
|
||||||
write,
|
writeSeries,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
10
lib/utils.ts
10
lib/utils.ts
|
|
@ -5,8 +5,14 @@ export function getHostName() {
|
||||||
if (deploymentId) {
|
if (deploymentId) {
|
||||||
return `https://nrss-${deploymentId}.deno.dev`;
|
return `https://nrss-${deploymentId}.deno.dev`;
|
||||||
} else {
|
} else {
|
||||||
// assume env
|
const proxyUrl = Deno.env.get("PROXY_URL");
|
||||||
return "http://localhost:8000";
|
if (proxyUrl) {
|
||||||
|
console.debug(`Using proxy URL ${proxyUrl}`);
|
||||||
|
return proxyUrl;
|
||||||
|
} else {
|
||||||
|
console.debug(`Assuming localhost`);
|
||||||
|
return "http://localhost:8000";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
import "jsr:@std/dotenv/load";
|
||||||
|
import { getHostName } from "../utils.ts";
|
||||||
|
import { STATUS_CODE } from "$fresh/server.ts";
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
clientId: Deno.env.get("VIPPS_CLIENT_ID"),
|
||||||
|
clientSecret: Deno.env.get("VIPPS_CLIENT_SECRET"),
|
||||||
|
ocpApimSubscriptionKeyPrimary: Deno.env.get("VIPPS_OCP_APIM_SUBSCRIPTION_KEY_PRIMARY"),
|
||||||
|
ocpApimSubscriptionKeySecondary: Deno.env.get("VIPPS_OCM_APIM_SUBSCRIPTION_KEY_SECONDARY"),
|
||||||
|
testMobileNumber: Deno.env.get("VIPPS_TEST_MOBILE_NUMBER"),
|
||||||
|
msn: Deno.env.get("VIPPS_MSN"),
|
||||||
|
baseUrl: Deno.env.get("VIPPS_API_BASE_URL"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ensure all environment variables are set
|
||||||
|
for (const [key, value] of Object.entries(config)) {
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`Missing environment variable: ${key}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const standardVippsHeaders = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Ocp-Apim-Subscription-Key": config.ocpApimSubscriptionKeyPrimary,
|
||||||
|
"Merchant-Serial-Number": config.msn,
|
||||||
|
"Vipps-System-Name": "Krets AS",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const getAccessToken = async function () {
|
||||||
|
console.log(`Fetching ${`${config.baseUrl}/accesstoken/get`}`);
|
||||||
|
const response = await fetch(`${config.baseUrl}/accesstoken/get`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"client_id": config.clientId,
|
||||||
|
"client_secret": config.clientSecret,
|
||||||
|
...standardVippsHeaders,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.access_token as string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
curl -X POST https://apitest.vipps.no/recurring/v3/agreements/ \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer YOUR-ACCESS-TOKEN" \
|
||||||
|
-H "Ocp-Apim-Subscription-Key: YOUR-SUBSCRIPTION-KEY" \
|
||||||
|
-H "Merchant-Serial-Number: YOUR-MSN" \
|
||||||
|
-H 'Idempotency-Key: YOUR-IDEMPOTENCY-KEY' \
|
||||||
|
-H "Vipps-System-Name: acme" \
|
||||||
|
-H "Vipps-System-Version: 3.1.2" \
|
||||||
|
-H "Vipps-System-Plugin-Name: acme-webshop" \
|
||||||
|
-H "Vipps-System-Plugin-Version: 4.5.6" \
|
||||||
|
-d '{
|
||||||
|
"interval": {
|
||||||
|
"unit" : "WEEK",
|
||||||
|
"count": 2
|
||||||
|
},
|
||||||
|
"pricing": {
|
||||||
|
"amount": 1000,
|
||||||
|
"currency": "NOK"
|
||||||
|
},
|
||||||
|
"merchantRedirectUrl": "https://example.com/redirect-url",
|
||||||
|
"merchantAgreementUrl": "https://example.com/agreement-url",
|
||||||
|
"phoneNumber": "12345678",
|
||||||
|
"productName": "Test product"
|
||||||
|
}'
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const createAgreement = async function () {
|
||||||
|
const token = await getAccessToken();
|
||||||
|
const idempotencyKey = crypto.randomUUID();
|
||||||
|
const response = await fetch(`${config.baseUrl}/recurring/v3/agreements/`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${token}`,
|
||||||
|
"Idempotency-Key": idempotencyKey,
|
||||||
|
...standardVippsHeaders,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"interval": {
|
||||||
|
"unit": "MONTH",
|
||||||
|
"count": 1,
|
||||||
|
},
|
||||||
|
"pricing": {
|
||||||
|
"suggestedMaxAmount": 5000, // 50,- NOK
|
||||||
|
"currency": "NOK",
|
||||||
|
"type": "VARIABLE",
|
||||||
|
},
|
||||||
|
"merchantRedirectUrl": `${getHostName()}/donations-success`,
|
||||||
|
"merchantAgreementUrl": `${getHostName()}/donations`,
|
||||||
|
"productName": "Månedlig støtte til NRSS",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === STATUS_CODE.Created) {
|
||||||
|
const body = response.json();
|
||||||
|
return body as unknown as {
|
||||||
|
vippsConfirmationUrl: string;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const errorMessage = `Failed to create Vipps agreement ${response.status} ${await response.text()}`;
|
||||||
|
console.error(errorMessage);
|
||||||
|
return new Error(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { PageProps } from "$fresh/server.ts";
|
import { PageProps } from "$fresh/server.ts";
|
||||||
|
import Footer from "../components/Footer.tsx";
|
||||||
|
import Header from "../components/Header.tsx";
|
||||||
|
|
||||||
export default function App({ Component }: PageProps) {
|
export default function App({ Component }: PageProps) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -14,7 +16,9 @@ export default function App({ Component }: PageProps) {
|
||||||
<title>NRSS</title>
|
<title>NRSS</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-slate-50">
|
<body class="bg-slate-50">
|
||||||
|
<Header></Header>
|
||||||
<Component />
|
<Component />
|
||||||
|
<Footer></Footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { getHostName } from "../../../lib/utils.ts";
|
||||||
|
import { createAgreement } from "../../../lib/vipps/vipps.ts";
|
||||||
|
|
||||||
|
export const handler = async function (_req: Request): Promise<Response> {
|
||||||
|
const agreement = await createAgreement();
|
||||||
|
if (agreement instanceof Error) {
|
||||||
|
return Response.redirect(`${getHostName()}/donations-error`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.redirect(agreement.vippsConfirmationUrl);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Input } from "../components/Input.tsx";
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={"p-4 mx-auto max-w-screen-md text-center"}
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
className={"text-4xl"}
|
||||||
|
>
|
||||||
|
Avslutt støtte til NRSS
|
||||||
|
</h1>
|
||||||
|
<Input
|
||||||
|
placeholder={"ditt mobillnummer"}
|
||||||
|
>
|
||||||
|
</Input>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
export default function () {
|
||||||
|
return (
|
||||||
|
<div className="my-16 text-center flex flex-col items-center min-h-screen ">
|
||||||
|
<h1 className="text-6xl mb-8">
|
||||||
|
💥🔧🚨😱
|
||||||
|
</h1>
|
||||||
|
<p className="text-4xl mb-4">
|
||||||
|
Auda. Noe gikk galt. Det er flaut.
|
||||||
|
</p>
|
||||||
|
<p className="text-4xl">
|
||||||
|
Forteller du meg om feilen{" "}
|
||||||
|
<a className="text-blue-600 underline" href="mailto:olav@sundfoer.com">
|
||||||
|
på mail
|
||||||
|
</a>{" "}
|
||||||
|
blir jeg veldig glad.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
export default function () {
|
||||||
|
return (
|
||||||
|
<div className="my-16 text-center flex flex-col items-center min-h-screen ">
|
||||||
|
<h1 className="text-6xl mb-8">
|
||||||
|
🙏🌟😊💖
|
||||||
|
</h1>
|
||||||
|
<p className="text-4xl mb-4">
|
||||||
|
Tusen takk! Jeg er veldig takknemlig for all støtte.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a className="text-blue-600 underline" href="/">
|
||||||
|
Gå tilbake til forsiden
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
import { ButtonLink } from "../components/Button.tsx";
|
|
||||||
import { Button } from "../components/Button.tsx";
|
|
||||||
import Header from "../components/Header.tsx";
|
|
||||||
import { Input } from "../components/Input.tsx";
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
return (
|
|
||||||
<div className="p-4 mx-auto max-w-screen-md">
|
|
||||||
<h1
|
|
||||||
className={"bg-orange-200 text-white text-2xl font-semibold p-4 text-center"}
|
|
||||||
>
|
|
||||||
DEMOSIDE - FUNGERER IKKE
|
|
||||||
</h1>
|
|
||||||
<Header></Header>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={"max-w-2xl mx-auto px-4 py-8"}
|
|
||||||
>
|
|
||||||
<h2
|
|
||||||
className={"text-2xl font-semibold mb-4"}
|
|
||||||
>
|
|
||||||
Økonomisk støtte til NRSS
|
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
Jeg ønsker at NRSS skal være gratis tilgjengelig for alle. Imidlertid koster det penger og tid å drifte og
|
|
||||||
vedlikeholde en nettside. Dersom du har råd til det (og bare da!) setter jeg stor pris på om du vil støtte
|
|
||||||
prosjektet med et valgfritt, månedlig beløp via Vipps.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form
|
|
||||||
className={"mt-6"}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
type={"tel"}
|
|
||||||
placeholder={"Ditt telefonnummer."}
|
|
||||||
>
|
|
||||||
</Input>
|
|
||||||
<div className={"flex"}>
|
|
||||||
<Button
|
|
||||||
type={"submit"}
|
|
||||||
>
|
|
||||||
Støtt NRSS
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type={"button"}
|
|
||||||
>
|
|
||||||
Avslutt eksisterende abonnement
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
className={"text-sm"}
|
|
||||||
>
|
|
||||||
Epost blir utelukkende brukt til å knytte deg til en donasjon, slik at du kan kansellere den senere.
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p
|
|
||||||
className={"mt-4 font-semibold"}
|
|
||||||
>
|
|
||||||
Du kan når som helst avslutte abonnementet fra denne siden.
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className={"mt-4"}
|
|
||||||
>
|
|
||||||
Opplever du problemer med betalingen, eller har du andre spørsmål? Ta{" "}
|
|
||||||
<a
|
|
||||||
className={"text-blue-600 underline"}
|
|
||||||
href={"mailto:olav@sundfoer.com"}
|
|
||||||
>
|
|
||||||
kontakt på mail
|
|
||||||
</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<footer
|
|
||||||
className={"mt-8"}
|
|
||||||
>
|
|
||||||
Krets AS - 922 739 625
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { Head } from "$fresh/runtime.ts";
|
import { Head } from "$fresh/runtime.ts";
|
||||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
import Footer from "../components/Footer.tsx";
|
|
||||||
import Header from "../components/Header.tsx";
|
|
||||||
import Search from "../components/Search.tsx";
|
import Search from "../components/Search.tsx";
|
||||||
import SeriesCard from "../components/SeriesCard.tsx";
|
import SeriesCard from "../components/SeriesCard.tsx";
|
||||||
import { CSS, render } from "$gfm";
|
import { CSS, render } from "$gfm";
|
||||||
import { nrkRadio, NrkSearchResultList } from "../lib/nrk/nrk.ts";
|
import { nrkRadio, NrkSearchResultList } from "../lib/nrk/nrk.ts";
|
||||||
|
import { DonationSection } from "../islands/DonationSection.tsx";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
query: string | null;
|
query: string | null;
|
||||||
|
|
@ -43,14 +42,13 @@ export default function Home({ data, url }: PageProps<Props>) {
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="p-4 mx-auto max-w-screen-md">
|
<div className="p-4 mx-auto max-w-screen-md">
|
||||||
<Header />
|
|
||||||
<Search defaultValue={data.query} />
|
<Search defaultValue={data.query} />
|
||||||
<SearchResult result={data.result} origin={url.origin} />
|
<SearchResult result={data.result} origin={url.origin} />
|
||||||
<div
|
<div
|
||||||
class="markdown-body"
|
class="markdown-body"
|
||||||
dangerouslySetInnerHTML={{ __html: render(data?.rawMarkdown) }}
|
dangerouslySetInnerHTML={{ __html: render(data?.rawMarkdown) }}
|
||||||
/>
|
/>
|
||||||
<Footer />
|
<DonationSection />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue