Merge pull request 'new navbar' (#6) from new-landing into main
All checks were successful
Vercel Production Deployment / Deploy-Production (push) Successful in 1m51s
All checks were successful
Vercel Production Deployment / Deploy-Production (push) Successful in 1m51s
Reviewed-on: #6
This commit is contained in:
commit
4d7de6a959
@ -2,9 +2,9 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
body {
|
/* body {
|
||||||
transition:
|
transition:
|
||||||
background-color 0.2s ease-in-out,
|
background-color 0.2s ease-in-out,
|
||||||
color 0.2s ease-in-out;
|
color 0.2s ease-in-out;
|
||||||
@apply bg-stone-200 dark:bg-gray-900 text-gray-900 dark:text-stone-200 text-center;
|
/* @apply bg-stone-200 dark:bg-gray-900 text-gray-900 dark:text-stone-200 text-center; */
|
||||||
}
|
/* } */
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover" class="font-jose font-light">
|
<body data-sveltekit-preload-data="hover" class="bg-stone-200 dark:bg-primary-900 text-primary-900 dark:text-stone-200 font-jose font-light">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
5
src/lib/PageHeader.svelte
Normal file
5
src/lib/PageHeader.svelte
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
export let title;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 class="text-center text-4xl">{title}</h1>
|
@ -1,7 +0,0 @@
|
|||||||
<script>
|
|
||||||
export let data;
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<h2>{data.post.title}</h2>
|
|
||||||
<p>data.</p>
|
|
@ -7,7 +7,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex justify-center gap-12 my-12">
|
<div class="flex justify-center gap-12 my-12">
|
||||||
<div class="hover:scale-125">
|
<div class="hover:scale-110">
|
||||||
<a href={url}>
|
<a href={url}>
|
||||||
<GlobeOutline class="mx-auto" />
|
<GlobeOutline class="mx-auto" />
|
||||||
</a>
|
</a>
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a href={url}>
|
<a href={url}>
|
||||||
<div class="flex flex-col justify-center hover:scale-110">
|
<div class="flex flex-col justify-center h-16 hover:scale-110">
|
||||||
<img src={img} alt={name} class="max-h-12 max-w-12 self-center" />
|
<img src={img} alt={name} class="min-h-12 max-w-12 self-center" />
|
||||||
<p>{name}</p>
|
<p>{name}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
49
src/lib/images/vendorIcons/flowbiteSvelteIcon.svg
Normal file
49
src/lib/images/vendorIcons/flowbiteSvelteIcon.svg
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<svg width="40" height="40" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M73.5582 38.8281C73.3358 40.3595 72.3261 42.4399 71.1677 44.2486C69.7061 46.5309 67.3867 48.1294 64.7322 48.6845L53.0391 51.1296C51.1598 51.5226 49.4556 52.5062 48.1761 53.9365L40.5091 62.5065C39.0682 64.1171 38.0611 63.733 38.0611 61.5728C38.0436 61.6529 34.2559 71.421 44.2938 77.2108C48.1507 79.4355 53.7025 78.6362 57.5594 76.4115L77.995 64.6242C85.6375 60.216 91.0335 52.7586 92.8271 44.1261C92.898 43.7846 92.9528 43.4419 93.012 43.0995L73.5582 38.8281Z" fill="url(#paint0_linear_2768_3907)"/>
|
||||||
|
<path d="M68.2584 27.0537C72.1153 29.2783 73.6911 32.5903 73.6911 37.0396C73.6911 37.6436 73.6436 38.2404 73.5583 38.828L81.7924 42.365L93.0121 43.0993C94.4487 34.7848 91.6333 26.2265 86.586 19.4238C82.7877 14.3045 77.9001 9.86189 72.0242 6.47262C67.2511 3.71946 62.2909 1.9055 57.3074 0.910156L51.7082 8.16118L49.9395 16.4873L68.2584 27.0537Z" fill="url(#paint1_linear_2768_3907)"/>
|
||||||
|
<path d="M2.35055 33.1554C2.34845 33.162 2.35425 33.1639 2.35643 33.1573C2.78851 31.8614 3.33029 30.4156 4.00667 28.8711C7.53979 20.8029 14.3483 15.4219 22.7245 12.6782C31.1006 9.93467 40.2463 10.8964 47.8802 15.2996L49.9393 16.4873L57.3073 0.910235C33.8726 -3.77029 9.92159 9.9976 2.38034 33.0651C2.37674 33.0762 2.36363 33.1147 2.35055 33.1554Z" fill="url(#paint2_linear_2768_3907)"/>
|
||||||
|
<path d="M56.7594 76.4112C52.9026 78.6359 48.1507 78.6359 44.2939 76.4112C43.7704 76.1092 43.2768 75.7697 42.81 75.4021L36.027 80.0655L30.1807 89.5128C36.6698 94.913 45.0962 96.6491 53.5167 95.6836C59.8535 94.9569 66.1484 92.9495 72.0242 89.5603C76.7974 86.8071 80.8499 83.4225 84.2045 79.6084L80.7186 71.1384L75.0784 65.8447L56.7594 76.4112Z" fill="url(#paint3_linear_2768_3907)"/>
|
||||||
|
<path d="M42.8101 75.4019C39.8385 73.0612 38.0612 69.4711 38.0612 65.6258V65.2423V33.2168C38.0612 31.4056 38.5948 31.0978 40.1648 32.0034C37.7467 30.6086 32.1606 25.8205 25.2633 29.7989C21.4065 32.0236 18.2305 36.9342 18.2305 41.3834V64.9581C18.2305 73.7746 22.7969 82.9713 29.3832 88.8393C29.6437 89.0714 29.9134 89.2902 30.1807 89.5126L42.8101 75.4019Z" fill="url(#paint4_linear_2768_3907)"/>
|
||||||
|
<path d="M83.7314 15.9369C83.7268 15.9318 83.7223 15.9359 83.7268 15.941C84.6342 16.9629 85.6166 18.1545 86.6173 19.512C91.8446 26.6029 93.7977 35.5839 91.988 44.2028C90.1781 52.8217 84.7717 60.2537 77.1377 64.657L75.0786 65.8447L84.2047 79.6084C99.9793 61.6728 100.02 34.0663 83.7948 16.0078C83.7871 15.9992 83.7602 15.9686 83.7314 15.9369Z" fill="url(#paint5_linear_2768_3907)"/>
|
||||||
|
<path d="M19.0306 41.384C19.0306 36.9348 21.4065 32.8234 25.2634 30.5988C25.7869 30.2968 26.328 30.0396 26.8801 29.8195L26.2291 21.6191L21.6558 12.2368C13.7301 15.1511 7.31911 21.1739 3.94584 28.9422C1.40729 34.7882 2.9376e-05 41.2383 0 48.0167C0 53.5231 0.907647 58.7216 2.5366 63.5311L11.6217 64.75L19.0306 62.517V41.384V41.384Z" fill="url(#paint6_linear_2768_3907)"/>
|
||||||
|
<path d="M26.88 29.8195C30.3947 28.4189 34.3955 28.6762 37.7287 30.5988L38.0612 30.7906L64.7473 46.1832C66.6194 47.263 66.4492 48.3255 64.3334 48.7679L65.8728 48.4461C67.8986 48.0225 69.7492 46.9873 71.1645 45.4787C73.5979 42.8849 74.4912 39.7555 74.4912 37.0398C74.4912 32.5905 72.1153 28.4792 68.2585 26.2546L47.8229 14.4672C40.1804 10.059 31.0179 9.11901 22.6382 11.8835C22.3067 11.9928 21.9822 12.1168 21.6558 12.2368L26.88 29.8195Z" fill="url(#paint7_linear_2768_3907)"/>
|
||||||
|
<path d="M57.9666 94.9574C57.9733 94.956 57.9721 94.95 57.9653 94.9514C56.6259 95.2255 55.1017 95.4797 53.4247 95.6667C44.6642 96.6439 35.9025 93.8433 29.3362 87.968C22.7698 82.0927 19.0307 73.699 19.0307 64.8925L19.0306 62.5171L2.53662 63.5311C10.1967 86.1472 34.1068 99.9859 57.8734 94.9768C57.8848 94.9744 57.9247 94.9665 57.9666 94.9574Z" fill="url(#paint8_linear_2768_3907)"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_2768_3907" x1="60.1798" y1="71.3098" x2="69.6224" y2="38.4205" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#A72F09"/>
|
||||||
|
<stop offset="1" stop-color="#F83C00"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint1_linear_2768_3907" x1="81.9279" y1="31.2009" x2="57.089" y2="6.44934" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#F83C00"/>
|
||||||
|
<stop offset="1" stop-color="#F83C00"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint2_linear_2768_3907" x1="48.4931" y1="15.0634" x2="11.0227" y2="17.4312" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#F83C00"/>
|
||||||
|
<stop offset="1" stop-color="#F83C00"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint3_linear_2768_3907" x1="45.9592" y1="85.9878" x2="79.6097" y2="76.9076" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#F83C00"/>
|
||||||
|
<stop offset="1" stop-color="#F83C00"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint4_linear_2768_3907" x1="21.8062" y1="47.0477" x2="45.6971" y2="71.8037" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#A72F09"/>
|
||||||
|
<stop offset="1" stop-color="#F83C00"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint5_linear_2768_3907" x1="76.3514" y1="64.9067" x2="93.0208" y2="31.3033" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#F83C00"/>
|
||||||
|
<stop offset="1" stop-color="#F83C00"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint6_linear_2768_3907" x1="16.0916" y1="27.393" x2="7.17161" y2="60.9197" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#D03504"/>
|
||||||
|
<stop offset="1" stop-color="#F83C00"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint7_linear_2768_3907" x1="61.6291" y1="25.7983" x2="29.0329" y2="33.9141" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#A72E08"/>
|
||||||
|
<stop offset="1" stop-color="#CF3403"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint8_linear_2768_3907" x1="19.2203" y1="64.0702" x2="39.9976" y2="95.3241" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#F83C00"/>
|
||||||
|
<stop offset="1" stop-color="#F83C00" stop-opacity="0.52"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.6 KiB |
BIN
src/lib/logo.png
Normal file
BIN
src/lib/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
3900
src/lib/logo.svg
Normal file
3900
src/lib/logo.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 186 KiB |
@ -1,7 +1,7 @@
|
|||||||
import svelteIcon from '$lib/images/vendorIcons/svelteIcon.svg';
|
import svelteIcon from '$lib/images/vendorIcons/svelteIcon.svg';
|
||||||
import tailwindIcon from '$lib/images/vendorIcons/tailwindIcon.svg';
|
import tailwindIcon from '$lib/images/vendorIcons/tailwindIcon.svg';
|
||||||
import vercelIcon from '$lib/images/vendorIcons/vercelIcon.svg';
|
import vercelIcon from '$lib/images/vendorIcons/vercelIcon.svg';
|
||||||
import flowbiteLight from '$lib/images/vendorIcons/flowbiteLight.svg';
|
import flowbiteSvelteIcon from '$lib/images/vendorIcons/flowbiteSvelteIcon.svg';
|
||||||
import reactIcon from '$lib/images/vendorIcons/reactIcon.svg';
|
import reactIcon from '$lib/images/vendorIcons/reactIcon.svg';
|
||||||
import nextjsIcon from '$lib/images/vendorIcons/nextjsIcon.svg';
|
import nextjsIcon from '$lib/images/vendorIcons/nextjsIcon.svg';
|
||||||
import nextuiIcon from '$lib/images/vendorIcons/nextuiIcon.png';
|
import nextuiIcon from '$lib/images/vendorIcons/nextuiIcon.png';
|
||||||
@ -31,7 +31,7 @@ export const projects = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Flowbite',
|
name: 'Flowbite',
|
||||||
img: flowbiteLight,
|
img: flowbiteSvelteIcon,
|
||||||
url: 'https://flowbite-svelte.com//'
|
url: 'https://flowbite-svelte.com//'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,84 +1,55 @@
|
|||||||
<script>
|
<script>
|
||||||
import { blur } from 'svelte/transition';
|
import {
|
||||||
|
Navbar,
|
||||||
|
NavBrand,
|
||||||
|
NavLi,
|
||||||
|
NavUl,
|
||||||
|
NavHamburger,
|
||||||
|
Dropdown,
|
||||||
|
DropdownItem,
|
||||||
|
DarkMode
|
||||||
|
} from 'flowbite-svelte';
|
||||||
|
import { ChevronDownOutline, HomeOutline } from 'flowbite-svelte-icons';
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
import CloudflareAnalytics from '../lib/CloudflareAnalytics.svelte';
|
import CloudflareAnalytics from '$lib/CloudflareAnalytics.svelte';
|
||||||
import { DarkMode } from 'flowbite-svelte';
|
import logo from '$lib/logo.png';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
// Navigation links are generated based on this object
|
$: activeUrl = $page.url.pathname;
|
||||||
const nav = [
|
|
||||||
{
|
|
||||||
name: 'index',
|
|
||||||
subPages: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'whoami',
|
|
||||||
subPages: ['about', 'resume']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'portfolio',
|
|
||||||
subPages: ['svelte', 'next.js', 'homelab']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'contact',
|
|
||||||
subPages: ['get_in_touch', 'socials']
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
let selected = nav[0]; // keep track of the selected 'page' object.
|
|
||||||
let intSelected = 0; // selected page index
|
|
||||||
|
|
||||||
// change the selected component (the event.originalTarget.id is not accessible in Chrome so switched to event.srcElement.id)
|
|
||||||
function changeComponent(event) {
|
|
||||||
selected = nav[event.srcElement.id];
|
|
||||||
intSelected = event.srcElement.id;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CloudflareAnalytics />
|
<CloudflareAnalytics />
|
||||||
<nav class="nav-container h-32 w-full mt-12">
|
<Navbar class="bg-transparent">
|
||||||
<ul class="text-center">
|
<NavBrand href="/">
|
||||||
{#each nav as main, i}
|
<img src={logo} class="me-3 h-6 sm:h-9" alt="Flowbite Logo" />
|
||||||
{#if i != 0}
|
<span class="self-center whitespace-nowrap text-xl font-semibold dark:text-stone-200"
|
||||||
{#if !nav[i].subPages[0]}
|
>rannes.dev</span
|
||||||
<li class="nav-item font-jose inline-block">
|
>
|
||||||
<a
|
</NavBrand>
|
||||||
href={main.name}
|
<div class="flex">
|
||||||
class={selected == nav[i]
|
<DarkMode btnClass="block sm:hidden" />
|
||||||
? 'nav-link text-2xl p-3 hover:text-slate-400'
|
<NavHamburger />
|
||||||
: 'nav-link text-2xl p-3 hover:text-slate-400'}
|
|
||||||
on:click={changeComponent}
|
|
||||||
id={i}>{main.name}</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{:else}
|
|
||||||
<li class="nav-item font-jose inline-block hover:text-slate-400">
|
|
||||||
<button
|
|
||||||
class={selected == nav[i] ? 'nav-link text-2xl p-3' : 'nav link text-2xl p-3'}
|
|
||||||
on:click={changeComponent}
|
|
||||||
id={i}>{main.name}</button
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
<hr class="nav-divider max-w-xl border-1 mx-auto rounded border-slate-400" />
|
|
||||||
{#key selected}
|
|
||||||
<ul in:blur={{ delay: 250 }} out:blur={{ duration: 250 }} class="text-center h-4 my-1">
|
|
||||||
{#each selected.subPages as subPage}
|
|
||||||
<li class="sub-nav-item inline-block font-jose hover:text-slate-400">
|
|
||||||
<a class="sub-nav-link text-xl p-2" href={subPage}>{subPage.replace(/_/g, ' ')}</a>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/key}
|
|
||||||
<div class="absolute top-2 md:top-6 right-2 md:right-6">
|
|
||||||
<DarkMode />
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
<NavUl {activeUrl}>
|
||||||
<div class="mt-6 mx-auto max-w-xs sm:max-w-md md:max-w-3xl text-center">
|
<NavLi href="/">home</NavLi>
|
||||||
|
<NavLi href="/about">whoami</NavLi>
|
||||||
|
<NavLi class="cursor-pointer">
|
||||||
|
projects<ChevronDownOutline class="w-6 h-6 text-primary-800 dark:text-stone-200 inline" />
|
||||||
|
</NavLi>
|
||||||
|
<Dropdown class="w-44 z-20">
|
||||||
|
<DropdownItem href="/projects/svelte">svelte</DropdownItem>
|
||||||
|
<DropdownItem href="/projects/nextjs">nextjs</DropdownItem>
|
||||||
|
<DropdownItem href="/projects/homelab">homelab</DropdownItem>
|
||||||
|
</Dropdown>
|
||||||
|
<NavLi href="/contact">contact</NavLi>
|
||||||
|
<NavLi class="hidden sm:block">
|
||||||
|
<DarkMode btnClass="p-0" />
|
||||||
|
</NavLi>
|
||||||
|
</NavUl>
|
||||||
|
</Navbar>
|
||||||
|
<div class="px-2 sm:px-0 mx-auto max-w-xs sm:max-w-md md:max-w-3xl text-center">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<footer class="mt-12">
|
<footer class="mt-12 text-center">
|
||||||
<p class="text-sm text-gray-400">built by christian rannes 2024</p>
|
<p class="text-sm text-stone-400">built by christian rannes 2024</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
<h1>This is the login page</h1>
|
|
||||||
<p>Access denied</p>
|
|
@ -1,10 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { fade } from 'svelte/transition';
|
|
||||||
import Projects from '../../lib/Projects.svelte';
|
|
||||||
const category = 'nextjs';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div in:fade>
|
|
||||||
<h1 class="text-2xl mb-4">Next.js Projects</h1>
|
|
||||||
<Projects {category} />
|
|
||||||
</div>
|
|
12
src/routes/projects/nextjs/+page.svelte
Normal file
12
src/routes/projects/nextjs/+page.svelte
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<script>
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import Projects from '$lib/Projects.svelte';
|
||||||
|
import PageHeader from '$lib/PageHeader.svelte';
|
||||||
|
const category = 'nextjs';
|
||||||
|
const title = 'Next.js Projects';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div in:fade>
|
||||||
|
<PageHeader {title} />
|
||||||
|
<Projects {category} />
|
||||||
|
</div>
|
@ -1,11 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import Projects from '../../lib/Projects.svelte';
|
import Projects from '$lib/Projects.svelte';
|
||||||
|
import PageHeader from '$lib/PageHeader.svelte';
|
||||||
const projectRssUrl = 'https://gitea.rannes.dev/rannes.dev/my-portfolio.rss';
|
const projectRssUrl = 'https://gitea.rannes.dev/rannes.dev/my-portfolio.rss';
|
||||||
const category = 'svelte';
|
const category = 'svelte';
|
||||||
|
const title = 'Svelte Projects';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div in:fade>
|
<div in:fade>
|
||||||
<h1 class="text-2xl mb-4">Svelte Projects</h1>
|
<PageHeader {title} />
|
||||||
<Projects {category} />
|
<Projects {category} />
|
||||||
</div>
|
</div>
|
@ -1,9 +0,0 @@
|
|||||||
<script>
|
|
||||||
|
|
||||||
import { fade } from "svelte/transition";
|
|
||||||
|
|
||||||
</script>
|
|
||||||
<div in:fade>
|
|
||||||
<h1 class="text-xl">Under development</h1>
|
|
||||||
<p>In the meantime have a look at my <a class="underline" href="https://www.linkedin.com/in/christian-rannes/">linkedin</a></p>
|
|
||||||
</div>
|
|
@ -1 +0,0 @@
|
|||||||
<h1>Under development</h1>
|
|
Loading…
Reference in New Issue
Block a user