이정민

Merge branch 'feat/web'

{
"presets": [
"next/babel"
],
"plugins": [
[
"babel-plugin-styled-components",
{
"fileName": true,
"displayName": true,
"pure": true
}
]
]
}
\ No newline at end of file
export const browserslist = ["defaults"];
# Config
!config/default.json
!config/development.json.sample
config/*.json
# Next
.next
out
# Logs
npm-debug.log*
# NPM
node_modules/
# Transpiled code
dist
out
.out
# Dev tools
.DS_Store
.vscode
.idea
*.swp
*.bak
node_modules.nosync/
*.env.*
\ No newline at end of file
{
"editor.formatOnSave": true,
"prettier.semi": false,
"prettier.trailingComma": "all",
"prettier.singleQuote": true,
"prettier.tslintIntegration": true,
"prettier.tabWidth": 2,
"prettier.printWidth": 120
}
node_modules
dist/*
\ No newline at end of file
var blackTheme = {
'common.bi.image': 'https://uicdn.toast.com/toastui/img/tui-image-editor-bi.png',
'common.bisize.width': '251px',
'common.bisize.height': '21px',
'common.backgroundImage': 'none',
'common.backgroundColor': '#1e1e1e',
'common.border': '0px',
// header
'header.backgroundImage': 'none',
'header.backgroundColor': 'transparent',
'header.border': '0px',
// load button
'loadButton.backgroundColor': '#fff',
'loadButton.border': '1px solid #ddd',
'loadButton.color': '#222',
'loadButton.fontFamily': "'Noto Sans', sans-serif",
'loadButton.fontSize': '12px',
// download button
'downloadButton.backgroundColor': '#fdba3b',
'downloadButton.border': '1px solid #fdba3b',
'downloadButton.color': '#fff',
'downloadButton.fontFamily': "'Noto Sans', sans-serif",
'downloadButton.fontSize': '12px',
// main icons
'menu.normalIcon.color': '#8a8a8a',
'menu.activeIcon.color': '#555555',
'menu.disabledIcon.color': '#434343',
'menu.hoverIcon.color': '#e9e9e9',
'menu.iconSize.width': '24px',
'menu.iconSize.height': '24px',
// submenu icons
'submenu.normalIcon.color': '#8a8a8a',
'submenu.activeIcon.color': '#e9e9e9',
'submenu.iconSize.width': '32px',
'submenu.iconSize.height': '32px',
// submenu primary color
'submenu.backgroundColor': '#1e1e1e',
'submenu.partition.color': '#3c3c3c',
// submenu labels
'submenu.normalLabel.color': '#8a8a8a',
'submenu.normalLabel.fontWeight': 'lighter',
'submenu.activeLabel.color': '#fff',
'submenu.activeLabel.fontWeight': 'lighter',
// checkbox style
'checkbox.border': '0px',
'checkbox.backgroundColor': '#fff',
// range style
'range.pointer.color': '#fff',
'range.bar.color': '#666',
'range.subbar.color': '#d1d1d1',
'range.disabledPointer.color': '#414141',
'range.disabledBar.color': '#282828',
'range.disabledSubbar.color': '#414141',
'range.value.color': '#fff',
'range.value.fontWeight': 'lighter',
'range.value.fontSize': '11px',
'range.value.border': '1px solid #353535',
'range.value.backgroundColor': '#151515',
'range.title.color': '#fff',
'range.title.fontWeight': 'lighter',
// colorpicker style
'colorpicker.button.border': '1px solid #1e1e1e',
'colorpicker.title.color': '#fff',
};
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>TUI Example</title>
<link
type="text/css"
href="https://uicdn.toast.com/tui-color-picker/v2.2.6/tui-color-picker.css"
rel="stylesheet"
/>
<link type="text/css" href="./tui-image-editor.css" rel="stylesheet" />
<style>
@import url(http://fonts.googleapis.com/css?family=Noto+Sans);
html,
body {
height: 100%;
margin: 0;
}
</style>
</head>
<body>
<div id="tui-image-editor-container"></div>
<script
type="text/javascript"
src="https://api-storage.cloud.toast.com/v1/AUTH_e18353c4ea5746c097143946d0644e61/toast-ui-cdn/tui-image-editor/v3.11.0/example/fabric-v4.2.0.js"
></script>
<script
type="text/javascript"
src="https://uicdn.toast.com/tui.code-snippet/v1.5.0/tui-code-snippet.min.js"
></script>
<script
type="text/javascript"
src="https://uicdn.toast.com/tui-color-picker/v2.2.6/tui-color-picker.js"
></script>
<script
type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.3/FileSaver.min.js"
></script>
<script type="text/javascript" src="./tui-image-editor.js"></script>
<script type="text/javascript" src="./black-theme.js"></script>
<script type="text/javascript" src="../../dist/gif-generator.js"></script>
<script>
// Image editor
var imageEditor = new tui.ImageEditor("#tui-image-editor-container", {
includeUI: {
loadImage: {
path: "./sampleImage2.png",
name: "SampleImage",
},
theme: blackTheme, // or whiteTheme
initMenu: "filter",
menuBarPosition: "bottom",
},
cssMaxWidth: 700,
cssMaxHeight: 500,
usageStatistics: false,
});
window.onresize = function () {
imageEditor.ui.resizeEditor();
};
let gifGenerator;
setTimeout(function () {
gifGenerator = new GifGenerator(imageEditor._graphics.getCanvas());
}, 1000);
</script>
</body>
</html>
This diff is collapsed. Click to expand it.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
{
"name": "gif-generator",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack -w",
"build": "webpack"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/cli": "^7.13.14",
"@babel/core": "^7.13.15",
"@babel/preset-env": "^7.13.15",
"babel-loader": "^8.2.2",
"fabric": "^4.4.0",
"webpack": "^5.31.0",
"webpack-cli": "^4.6.0"
},
"dependencies": {
"@babel/plugin-proposal-class-properties": "^7.13.0",
"gifencoder": "^2.0.1",
"stream": "0.0.2"
}
}
import GIF from "gifencoder";
class GifGenerator {
constructor(canvas) {
this.canvas = canvas;
this.width = canvas.getWidth();
this.height = canvas.getHeight();
this.gif = new GIF(this.width, this.height);
this.gif.start();
this.gif.setTransparent(null);
this.gif.setRepeat(0);
this.gif.setQuality(10);
}
addFrame(delay = 0) {
this.gif.setDelay(delay);
this.gif.addFrame(this.canvas.getContext());
}
render() {
this.gif.finish();
const byte = new Uint8Array(this.gif.out.data);
return new Blob([byte], { type: "image/gif" });
}
}
window.GifGenerator = GifGenerator;
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: __dirname + '/dist',
filename: 'gif-generator.js',
sourceMapFilename: 'gif-generator.map',
},
module: {
rules: [
{
test: /\.js$/,
include: [
path.resolve(__dirname, 'src/js')
],
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-proposal-class-properties']
}
}
}
]
},
devtool: 'source-map',
mode: 'development'
};
\ No newline at end of file
/// <reference types="next" />
/// <reference types="next/types/global" />
/* eslint-disable @typescript-eslint/no-var-requires */
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
});
module.exports = withBundleAnalyzer({
target: "serverless",
env: {
BASE_URL: process.env.BASE_URL,
},
webpack(conf) {
conf.module.rules.push({
test: /\.svg$/,
use: [
{
loader: "@svgr/webpack",
options: {
svgoConfig: {
plugins: [
{
// Enable figma's wrong mask-type attribute work
removeRasterImages: false,
removeStyleElement: false,
removeUnknownsAndDefaults: false,
// Enable svgr's svg to fill the size
removeViewBox: false,
},
],
},
},
},
],
});
// 절대경로
conf.resolve.modules.push(__dirname);
return conf;
},
});
{
"name": "website",
"version": "0.1.0",
"private": true,
"dependencies": {
"@next/bundle-analyzer": "^10.0.7",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"next": "^10.0.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"styled-components": "^5.2.3",
"styled-reset": "^4.3.4",
"tui-image-editor": "3.14.2",
"@toast-ui/react-image-editor": "3.14.2",
"web-vitals": "^1.0.1"
},
"devDependencies": {
"@babel/core": "^7.13.10",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@svgr/webpack": "^5.5.0",
"@types/node": "^14.14.22",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-window": "^1.8.2",
"@types/styled-components": "^5.1.7",
"@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/eslint-plugin-tslint": "^4.14.1",
"@typescript-eslint/parser": "^4.14.1",
"babel-loader": "^8.2.2",
"eslint": "^7.18.0",
"eslint-config-airbnb-typescript": "^12.3.1",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"prettier": "^2.2.1",
"typescript": "^4.1.3"
},
"scripts": {
"dev": "next",
"debug": "NODE_OPTIONS='--inspect' next dev",
"build": "next build",
"start": "next start",
"export": "next export",
"type-check": "tsc",
"eslint": "eslint .",
"analyze": "ANALYZE=true next build"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
import styled from "styled-components";
const Header = () => {
return <Container>Gif Generator</Container>;
};
const Container = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
padding: 1.5rem;
background-color: white;
box-shadow: ${({ theme }) => theme.boxShadow.normal};
text-align: center;
font-size: 1.6rem;
font-weight: 500;
font-style: italic;
`;
export default Header;
import dynamic from "next/dynamic";
import { useState } from "react";
import styled from "styled-components";
const ToastEditor = dynamic(() => import("components/ToastEditor"), {
ssr: false,
});
const Image = ({ previewURL, setPreviewURL }) => {
const [file, setFile] = useState(undefined);
console.log("previewURL", previewURL);
// const uploadImage = (file) => {
// if (!file) {
// return;
// }
// };
// const selectImg = (e) => {
// const reader = new FileReader();
// const targetFile = e.target.files[0];
// setFile(targetFile);
// // uploadImage(targetFile);
// reader.onloadend = () => {
// setPreviewURL(reader.result);
// };
// reader.readAsDataURL(targetFile);
// };
// const [isEditorOpened, setIsEditorOpened] = useState(false);
// const handleEditor = () => {
// setIsEditorOpened(true);
// };
return (
<>
<Container>
<ImgBox>
{/* <div onClick={handleEditor}>asdf</div> */}
{/* {file === undefined ? ( */}
<>
{/* <div className="sub-flex">
<BlankBox />
<div>Click to add a photo</div>
<input
type="file"
style={{
position: "absolute",
top: 0,
paddingLeft: 0,
zIndex: 0,
width: "90%",
height: "100%",
border: "none",
cursor: "pointer",
outline: "none",
}}
onChange={selectImg}
/>
</div>
<div className="sub-flex">Open Image Editor</div> */}
</>
{/* ) : ( */}
<img
id="image"
alt={""}
style={{
objectFit: "cover",
display: "flex",
maxHeight: "90%",
maxWidth: "90%",
}}
src={previewURL as string}
/>
{/* )} */}
</ImgBox>
{/* <Menu /> */}
</Container>
{/* {isEditorOpened && <ToastEditor {...{ setPreviewURL, setIsImgAdded }} />} */}
</>
);
};
const Menu = () => {
return (
<div style={{ width: "15rem", marginLeft: "2rem" }}>
<Box />
<Box />
<Box />
<Box />
</div>
);
};
const Container = styled.div`
width: 100%;
display: flex;
justify-content: center;
margin-top: 10rem;
`;
const ImgBox = styled.div`
position: relative;
width: 90%;
/* height: 30rem; */
background-color: white;
box-shadow: ${({ theme }) => theme.boxShadow.normal};
border-radius: 2rem;
margin-top: 2rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
display: flex;
/* flex: 0.6; */
padding: 1rem 0;
/* .sub-flex {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
:first-child {
border-right: 1px solid ${({ theme }) => theme.color.gray};
}
} */
`;
const Box = styled.div`
width: 100%;
height: 10rem;
margin-top: 2rem;
border-radius: 1rem;
background-color: white;
box-shadow: ${({ theme }) => theme.boxShadow.normal};
`;
const BlankBox = styled.div`
z-index: 1;
position: absolute;
top: 0;
width: 90%;
height: 50px;
background-color: white;
`;
export default Image;
/// <reference path="react-image-editor.d.ts" />
import ImageEditor from "@toast-ui/react-image-editor";
import { useEffect, useState } from "react";
import styled from "styled-components";
import "tui-image-editor/dist/tui-image-editor.css";
const ToastEditor = ({ setPreviewURL, setIsImgAdded, setIsEditorOpened }) => {
// const [lowerCanvas, setLowerCanvas] = useState<HTMLCanvasElement>();
// const [upperCanvas, setUpperCanvas] = useState<HTMLCanvasElement>();
// // console.log(
// // document.getElementsByClassName("lower-canvas")[0]?.toDataURL("image/png")
// // );
// console.log("s");
// // const [upperCanvas, setUpperCanvas] = useState(
// // document.getElementsByClassName("upper-canvas ")[0]
// // );
// useEffect(() => {
// window?.addEventListener("click", () => {
// setLowerCanvas(
// document.getElementsByClassName("lower-canvas")[0] as HTMLCanvasElement
// );
// setUpperCanvas(
// document.getElementsByClassName("upper-canvas")[0] as HTMLCanvasElement
// );
// });
// }, []);
// useEffect(() => {
// const img = lowerCanvas?.toDataURL("image/png");
// const uploaded = document.getElementById("image");
// console.log(uploaded);
// // let w = window.open();
// // if (w?.window) w.document.body.innerHTML = "<img src='" + img + "'>";
// const image = new Image();
// // image.onload = function () {
// // lowerCanvas.width = uploaded.clientWidth;
// // lowerCanvas.height = uploaded.clientHeight;
// // lowerCanvas?.getContext("2d").drawImage(image, 0, 0);
// // };
// image.src = previewURL;
// console.log("b");
// if (lowerCanvas?.getContext&&upperCanvas?.getContext) {
// image.onload = function () {
// lowerCanvas.width = 1000;
// lowerCanvas.height = 572;
// upperCanvas.width = 1000;
// upperCanvas.height = 572;
// lowerCanvas?.getContext("2d").drawImage(image, 0, 0);
// };
// console.log(lowerCanvas.getContext("2d"));
// }
// }, [lowerCanvas?.toDataURL("image/png")]);
const handleEnd = () => {
const lowerCanvas = document.getElementsByClassName(
"lower-canvas"
)[0] as HTMLCanvasElement;
setPreviewURL(lowerCanvas.toDataURL("image/png"));
console.log("asdf");
setIsImgAdded(true);
setIsEditorOpened(false);
};
return (
<Container>
<div onClick={handleEnd} className="upload">
Upload
</div>
<ImageEditor
includeUI={{
loadImage: {
// path: 'img/sampleImage.jpg',
name: "SampleImage",
},
// theme: myTheme,
menu: ["shape", "filter"],
initMenu: "filter",
uiSize: {
width: "100%",
height: "700px",
},
menuBarPosition: "bottom",
}}
cssMaxHeight={500}
cssMaxWidth={700}
selectionStyle={{
cornerSize: 20,
rotatingPointOffset: 70,
}}
usageStatistics={true}
/>
</Container>
);
};
const Container = styled.div`
position: fixed;
width: 90%;
top: 10rem;
border-radius: 1.5rem;
box-shadow: ${({ theme }) => theme.boxShadow.normal};
display: flex;
flex-direction: column;
align-items: center;
.upload {
font: 800 11.5px Arial;
position: absolute;
right: 0;
top: 0;
width: 120px;
height: 40px;
background: red;
z-index: 10;
border-radius: 20px;
margin: 8px;
background-color: #fdba3b;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.tui-image-editor-container {
border-radius: 1.5rem;
}
.tui-image-editor-container .tui-image-editor-help-menu.top {
top: 2rem;
}
`;
export default ToastEditor;
declare module "@toast-ui/react-image-editor";
import type { AppProps } from "next/app";
import Head from "next/head";
import { ThemeProvider } from "styled-components";
import { GlobalStyle } from "styles/global-style";
import { theme } from "styles/theme";
function MyApp({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Gif Generator</title>
</Head>
<GlobalStyle />
<ThemeProvider theme={theme}>
<Component {...pageProps} />
<div id="modal-root" />
</ThemeProvider>
</>
);
}
export default MyApp;
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
} from "next/document";
import { ServerStyleSheet } from "styled-components";
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
render() {
return (
<Html>
<Head>
<meta charSet="utf-8" />
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<meta property="og:title" content="네이버 예약" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@100;300;400;500;700;900&display=swap"
rel="preload"
as="style"
/>
<link
href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@100;300;400;500;700;900&display=swap"
rel="stylesheet"
/>
<script
type="text/javascript"
src="https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=3ioixdkru3"
/>
</Head>
<body>
<Main />
<NextScript />
<script src="libs/code-snippet.min.js"></script>
<script src="libs/jquery.min.js"></script>
<script src="libs/fabric.min.js"></script>
<script src="js/image-editor.js"></script>
<div id="my-image-editor">
<canvas></canvas>
</div>
</body>
</Html>
);
}
}
export default MyDocument;
import Header from "components/Header";
import Image from "components/Image";
import styled from "styled-components";
import dynamic from "next/dynamic";
import { useState } from "react";
const ToastEditor = dynamic(() => import("components/ToastEditor"), {
ssr: false,
});
const Index = () => {
const [isEditorOpened, setIsEditorOpened] = useState(false);
const [previewURL, setPreviewURL] = useState<string | ArrayBuffer>("");
const [isImgAdded, setIsImgAdded] = useState(false);
return (
<Container>
<Header />
{!isImgAdded ? (
<div
style={{
height: "80vh",
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<button
className="open-button"
onClick={() => setIsEditorOpened(true)}
>
Open Image Editor
</button>
</div>
) : (
!isEditorOpened && (
<>
<div style={{ position: "fixed", top: "5rem" }}>
<button
className="open-button"
onClick={() => setIsEditorOpened(true)}
>
Change Image
</button>
</div>
<Image {...{ previewURL, setPreviewURL }} />
</>
)
)}
{isEditorOpened && (
<ToastEditor {...{ setPreviewURL, setIsImgAdded, setIsEditorOpened }} />
)}
</Container>
);
};
const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
.open-button {
margin-top: 3rem;
padding: 0.5rem 2rem;
display: flex;
align-items: center;
transition: 0.3s;
:hover {
font-size: 1.1rem;
transition: 0.3s;
}
::before {
width: 2.315rem;
content: "+";
font-size: 2rem;
margin-right: 1rem;
box-shadow: ${({ theme }) => theme.boxShadow.normal};
border-radius: 50%;
}
}
`;
export default Index;
import { createGlobalStyle } from "styled-components";
import { reset } from "styled-reset";
import { media } from "./theme";
export const GlobalStyle = createGlobalStyle`
${reset}
:focus {
outline: none;
border: none;
}
div[role="button"] {
cursor: pointer;
}
::-webkit-scrollbar {
display: none;
}
html{
-webkit-text-size-adjust: none;
font-family: -apple-system,BlinkMacSystemFont,helvetica,Apple SD Gothic Neo,sans-serif;
font-display: fallback;
${media.tablet}{
font-size: 10px;
}
-ms-overflow-style: none;
scrollbar-width: none;
}
button {
background: none;
padding: 0;
border: none;
cursor: pointer;
&:disabled {
cursor: default;
fill: #f2f3f4;
}
}
.pc-tablet-only {
display: block;
${media.mobile} {
display: none;
}
}
.tablet-mobile-only{
display: none;
${media.tablet}{
display:block;
}
}
.mobile-only {
display: none;
${media.mobile} {
display: block;
}
}
`;
import "styled-components";
declare module "styled-components" {
export interface DefaultTheme {
color: {
purple: "#8661de";
blue: "#00bac7";
gray: "#f6f6f6";
green: "#07b495";
lightGreen: "#99ecdd";
darkGray: "#54595d";
};
boxShadow: {
normal: "0 3px 8px 0 rgb(0 0 0 / 10%)";
purple: "0 3px 8px 0 #d6c9ff";
blue: "0 3px 8px 0 #b3e2e6";
};
}
}
import { DefaultTheme } from "styled-components";
export const theme: DefaultTheme = {
color: {
purple: "#8661de",
blue: "#00bac7",
gray: "#f6f6f6",
green: "#07b495",
lightGreen: "#99ecdd",
darkGray: "#54595d",
},
boxShadow: {
normal: "0 3px 8px 0 rgb(0 0 0 / 10%)",
purple: "0 3px 8px 0 #d6c9ff",
blue: "0 3px 8px 0 #b3e2e6",
},
};
const customMediaQuery = (maxWidth: number): string =>
`@media (max-width: ${maxWidth}px)`;
export const media = {
custom: customMediaQuery,
pc: customMediaQuery(1440),
tablet: customMediaQuery(768),
mobile: customMediaQuery(576),
};
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": "src",
"rootDir": "src",
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"next.config.js",
"custom.d.ts",
"styled.d.ts",
"src/styles",
"src/pages",
"public",
"**/*.scss",
"src/components",
"resources",
],
"exclude": [
"node_modules",
]
}
\ No newline at end of file
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.