re-init
This commit is contained in:
commit
87b9aa3cc9
|
@ -0,0 +1,12 @@
|
|||
*~
|
||||
._*
|
||||
.DS_Store
|
||||
node_modules
|
||||
package-lock.json
|
||||
|
||||
config.*
|
||||
log.txt
|
||||
output.txt
|
||||
ssl
|
||||
uploads
|
||||
static/vendor
|
|
@ -0,0 +1,339 @@
|
|||
'use strict'
|
||||
|
||||
const http = require( 'http' )
|
||||
, https = require( 'https' )
|
||||
, fs = require( 'fs' )
|
||||
, path = require( 'path' )
|
||||
, express = require( 'express' )
|
||||
, bodyParser = require( 'body-parser' )
|
||||
, formidable = require( 'formidable' )
|
||||
, compression = require( 'compression' )
|
||||
, minify = require( 'express-minify' )
|
||||
, morgan = require( 'morgan' )
|
||||
, range = require( 'range-2018.js' )
|
||||
|
||||
const DB = require( './lib/db.js' )
|
||||
, Photos = require( './lib/photos.js' )
|
||||
, Now = require( './lib/readable-timestamp.js' )
|
||||
, Config = require( './config.js' )
|
||||
|
||||
let app = express( )
|
||||
|
||||
app.all( '*', ( req, res, next ) =>
|
||||
{ if( /:\/\/baidu\.com\//i.test( req.url ) )
|
||||
{ return res.sendStatus( 403 ).end( ) }
|
||||
|
||||
// if( ( 'http' == req.protocol )
|
||||
// || /^www\./i.test( req.hostname )
|
||||
// )
|
||||
// { return res.redirect( 301,
|
||||
// [ 'https://'
|
||||
// , req.hostname.replace( /^(www\.)+/i, '' )
|
||||
// , ':'
|
||||
// , Config.ENV.SSLPORT
|
||||
// , req.url
|
||||
// ].join('') )
|
||||
// }
|
||||
|
||||
next( )
|
||||
})
|
||||
|
||||
|
||||
app.use( bodyParser.json( ) )
|
||||
.use( bodyParser.urlencoded({ extended: false }) )
|
||||
.use( compression( ) )
|
||||
.use( minify( ) )
|
||||
.use( express.static( './static' ) )
|
||||
|
||||
if( Config.ENV.PRODUCTION )
|
||||
{ app.use( morgan
|
||||
( 'combined'
|
||||
, { skip: ( req, res ) =>
|
||||
/\.(jpg|png|svg|ico|css|js)$/i.test( req.url )
|
||||
, stream: fs.createWriteStream
|
||||
( path.join( __dirname, 'log.txt' ), { flags: 'a' } )
|
||||
}
|
||||
) )
|
||||
}
|
||||
|
||||
|
||||
app.get( '/photos/:search', ( req, res ) =>
|
||||
{ res.redirect( 302, '/photos/#'+req.params.search ) })
|
||||
|
||||
|
||||
app.post( '/api/photos/variant', async ( req, res ) =>
|
||||
{ // create a Variant
|
||||
if( !req.query
|
||||
|| !req.query.SKU
|
||||
|| !req.query.Variant
|
||||
)
|
||||
{ res.sendStatus( 400 )
|
||||
.end( 'not enough params to create a variant' )
|
||||
return false
|
||||
}
|
||||
|
||||
let { SKU, Variant, CopyOf } = req.query
|
||||
Photos.createVariant({ SKU, Variant, CopyOf })
|
||||
|
||||
res.sendStatus( 200 )
|
||||
.end( )
|
||||
})
|
||||
|
||||
|
||||
app.put( '/api/photos/setDefaultVariant', ( req, res ) =>
|
||||
{ if( !req.query
|
||||
|| !req.query.SKU
|
||||
|| !req.query.Variant
|
||||
|| !req.query.oldVarinatRenamedTo
|
||||
)
|
||||
{ res.sendStatus( 400 )
|
||||
.end( 'not enough params to set a default variant'
|
||||
+ '(PUT /api/photos/setDefaultVariant)'
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
let { SKU, Variant, oldVarinatRenamedTo } = req.query
|
||||
|
||||
// set Variant as Default
|
||||
Photos.setDefaultVariant({ SKU, Variant, oldVarinatRenamedTo })
|
||||
|
||||
res.sendStatus( 200 )
|
||||
.end( )
|
||||
})
|
||||
|
||||
app.put( '/api/photos/renumber', async ( req, res ) =>
|
||||
{ if( !req.query.SKU
|
||||
|| !req.query.Variant
|
||||
|| !req.query.OldNr
|
||||
|| !req.query.NewNr
|
||||
)
|
||||
{ res.sendStatus( 400 )
|
||||
.end( 'not enough params to renumber photos'
|
||||
+ '(PUT /api/photos/renumber)'
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
// re-arraged the Photos
|
||||
const { SKU, Variant, OldNr, NewNr } = req.query
|
||||
const _success = await Photos.Renumber(
|
||||
{ SKU
|
||||
, Variant
|
||||
, OldNr
|
||||
, NewNr
|
||||
})
|
||||
|
||||
res.sendStatus( _success && 200 || 500 )
|
||||
.end( )
|
||||
})
|
||||
|
||||
|
||||
app.delete( '/api/photos/variant', async ( req, res ) =>
|
||||
{ if( !req.query
|
||||
|| !req.query.SKU
|
||||
|| !req.query.Variant
|
||||
)
|
||||
{ res.sendStatus( 400 )
|
||||
.end( 'not enough parameters to delete variant' )
|
||||
return false
|
||||
}
|
||||
|
||||
const { SKU, Variant } = req.query
|
||||
res.sendStatus( 200 )
|
||||
.end( await Photos.removeVariant({ SKU, Variant }) )
|
||||
})
|
||||
|
||||
app.delete( '/api/photos', async ( req, res ) =>
|
||||
{ if( !req.query
|
||||
|| !req.query.SKU
|
||||
|| !req.query.Variant
|
||||
|| !req.query.Nr
|
||||
)
|
||||
{ res.sendStatus( 400 )
|
||||
.end( 'not enough query parameters to delete a photo' )
|
||||
return false
|
||||
}
|
||||
|
||||
const { SKU, Variant, Hash, Nr } = req.query
|
||||
const _success = await Photos.removeOne(
|
||||
{ SKU
|
||||
, Variant
|
||||
, Nr
|
||||
})
|
||||
|
||||
res.sendStatus( _success && 200 || 500 )
|
||||
.end( )
|
||||
})
|
||||
|
||||
app.post( '/api/photos', async ( req, res ) =>
|
||||
{ if( !req.query
|
||||
|| !req.query.SKU
|
||||
|| !req.query.Variant
|
||||
)
|
||||
{ res.sendStatus( 400 )
|
||||
.end( 'not enough params to upload photo(s)' )
|
||||
return false
|
||||
}
|
||||
|
||||
let { SKU, Variant, Nr } = req.query
|
||||
, UploadingMultiplePhotos = false
|
||||
|
||||
if( 0 == Nr )
|
||||
{ // uploading multiple photos
|
||||
UploadingMultiplePhotos = true
|
||||
|
||||
const CurrentPhotos = await Photos.getHashmap(
|
||||
{ SKU
|
||||
, Variant
|
||||
, arrNumbers : range( 1,10 )
|
||||
})
|
||||
|
||||
if( CurrentPhotos )
|
||||
{ Object.keys( CurrentPhotos ).every( _Photo =>
|
||||
{ if( !CurrentPhotos[ _Photo ] )
|
||||
{ Nr = Number( _Photo )
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
else
|
||||
{ Nr = 1 }
|
||||
}
|
||||
else
|
||||
{ // replacing single photo
|
||||
await Photos.deletePhotoFiles({ SKU, Nr })
|
||||
}
|
||||
|
||||
let form = new formidable.IncomingForm( )
|
||||
form.uploadDir = path.join( __dirname, 'uploads' )
|
||||
form.encoding = 'binary'
|
||||
|
||||
form.addListener( 'file', async ( name, file ) =>
|
||||
{ if( UploadingMultiplePhotos
|
||||
&& ( 10 <= Nr )
|
||||
)
|
||||
{ return false }
|
||||
|
||||
console.info
|
||||
( Now( )
|
||||
, `recieving photo "${SKU}" /`
|
||||
, `"${Variant}" / "${Nr}"`
|
||||
)
|
||||
|
||||
Photos.Process
|
||||
({ FilePath : file.path
|
||||
, SKU
|
||||
, Variant
|
||||
, Nr : Nr++
|
||||
})
|
||||
})
|
||||
|
||||
form.addListener( 'end', ( ) =>
|
||||
{ res.sendStatus( 200 )
|
||||
.end( '<script>window.close( )</script>' )
|
||||
})
|
||||
|
||||
form.parse( req, ( err, fields, files ) =>
|
||||
{ if( err )
|
||||
{ console.error( Now( ), { err, fields, files } ) }
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
app.put( '/api/photos/assignVariantToAccount', async ( req, res ) =>
|
||||
{ // assigning Variant to Account
|
||||
if( !req.query
|
||||
|| !req.query.SKU
|
||||
|| !req.query.Variant
|
||||
|| !req.query.Account
|
||||
)
|
||||
{ res.sendStatus( 400 )
|
||||
.end( 'not enough params to assigning Variant to Account '
|
||||
+ '[POST /api/photos/assignVariantToAccount]'
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
const { SKU, Variant, Account } = req.query
|
||||
const _success = await Photos.assignVariantToAccount(
|
||||
{ SKU
|
||||
, Variant
|
||||
, Account
|
||||
})
|
||||
|
||||
res.sendStatus( _success && 200 || 500 )
|
||||
.end( )
|
||||
})
|
||||
|
||||
|
||||
app.get( '/api/photos', async ( req, res ) =>
|
||||
{ // get Photos_Ordered(since lastUpdated) table
|
||||
// console.log('GET: /api/photos\nQUERY:', req.query )
|
||||
res.send( await Photos.get_Photos_Since( req.query.since ) )
|
||||
})
|
||||
|
||||
|
||||
app.get( '/api/photos/baseURL', async ( req, res ) =>
|
||||
{ // get the httpURL of photos dir
|
||||
res.end( Config.Paths.imgGlobal )
|
||||
})
|
||||
|
||||
app.get( '/api/photos/accounts', async ( req, res ) =>
|
||||
{ // get all the Accounts for the photos
|
||||
res.end( Config.Accounts.join( '\n' ) )
|
||||
})
|
||||
|
||||
|
||||
// http (for redirects only)
|
||||
const httpURL = _ => `\n http://${ _ }:${ Config.ENV.PORT }`
|
||||
|
||||
//console.log( Config, Config.ENV )
|
||||
|
||||
http.createServer( app )
|
||||
.listen( Config.ENV.PORT, Config.ENV.IP )
|
||||
|
||||
if( '::' == Config.ENV.IP )
|
||||
{ console.info( Now( ), httpURL( '[::1]' ) ) }
|
||||
else
|
||||
{ http.createServer( app )
|
||||
.listen( Config.ENV.PORT, Config.ENV.IP6 )
|
||||
|
||||
console.info
|
||||
( Now( )
|
||||
, httpURL( 'localhost' )
|
||||
, httpURL( Config.ENV.HOST )
|
||||
, httpURL( Config.ENV.IP )
|
||||
, httpURL( `[${ Config.ENV.IP6 }]` )
|
||||
)
|
||||
}
|
||||
|
||||
// SSL
|
||||
// const sslURL = _ => `\n https://${ _ }:${ Config.ENV.SSLPORT }`
|
||||
// try
|
||||
// { const SSLCerts =
|
||||
// { cert: fs.readFileSync( path.join( __dirname, 'ssl', 'cert.pem' ) )
|
||||
// , key: fs.readFileSync( path.join( __dirname, 'ssl', 'key.pem' ) )
|
||||
// }
|
||||
|
||||
// https.createServer( SSLCerts, app )
|
||||
// .listen( Config.ENV.SSLPORT, Config.ENV.IP )
|
||||
|
||||
// if( '::' == Config.ENV.IP )
|
||||
// { console.info( Now( ), sslURL( '[::1]' ) ) }
|
||||
// else
|
||||
// { https.createServer( SSLCerts, app )
|
||||
// .listen( Config.ENV.SSLPORT, Config.ENV.IP6 )
|
||||
|
||||
// console.info
|
||||
// ( Now( )
|
||||
// , sslURL( 'localhost' )
|
||||
// , sslURL( Config.ENV.HOST )
|
||||
// , sslURL( Config.ENV.IP )
|
||||
// , sslURL( `[${ Config.ENV.IP6 }]` )
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// catch( err )
|
||||
// { console.warn( Now( ), 'no SSL:', err ) }
|
|
@ -0,0 +1,72 @@
|
|||
'use strict'
|
||||
|
||||
const fs = require( 'fs' )
|
||||
, path = require( 'path' )
|
||||
, URL = require( 'url' ).URL
|
||||
, request = require( 'request' )
|
||||
|
||||
, Now = require( './readable-timestamp.js' )
|
||||
, Config = require( '../config.js' )
|
||||
|
||||
const req
|
||||
= ( http_method, sql_params ) =>
|
||||
new Promise( ( resolve, reject ) =>
|
||||
{ let req_url = new URL( Config.SQL.BaseURL )
|
||||
|
||||
for( const k in sql_params )
|
||||
{ req_url.searchParams.set( k, sql_params[ k ] ) }
|
||||
|
||||
// console.log
|
||||
// ({ url : req_url.toString()
|
||||
// , method : http_method
|
||||
// , auth : Config.SQL.auth
|
||||
// })
|
||||
|
||||
request( { url: req_url.toString()
|
||||
, method: http_method
|
||||
, auth: Config.SQL.auth
|
||||
}
|
||||
, ( err, res, body ) =>
|
||||
{ // console.log({ err, res, body })
|
||||
|
||||
if( err || !res )
|
||||
{ reject( err )
|
||||
return false
|
||||
}
|
||||
if( 400 <= res.statusCode )
|
||||
{ resolve( false ) }
|
||||
|
||||
let _res = ''
|
||||
|
||||
try
|
||||
{ _res = JSON.parse( body ) }
|
||||
catch( err )
|
||||
{ if( err )
|
||||
{ console.error( Now(), '\n', JSON.stringify(
|
||||
{ http_method
|
||||
, sql_params
|
||||
, err
|
||||
, _res
|
||||
}, false, 2 ) )
|
||||
resolve( [] )
|
||||
}
|
||||
}
|
||||
resolve( _res )
|
||||
})
|
||||
|
||||
}).catch( err => console.error
|
||||
( Now()
|
||||
, 'cant make request to mysql,'
|
||||
, err
|
||||
, http_method
|
||||
, sql_params
|
||||
) )
|
||||
|
||||
|
||||
module.exports = exports =
|
||||
{ create: ( _ ) => req( 'POST' , _ )
|
||||
, read: ( _ ) => req( 'GET' , _ )
|
||||
, update: ( _ ) => req( 'PUT' , _ )
|
||||
, delete: ( _ ) => req( 'DELETE' , _ )
|
||||
, raw: ( _ ) => req( 'PATCH' , _ )
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
'use strict'
|
||||
|
||||
module.exports = exports = STR =>
|
||||
STR.replace( /\s+/g, '' )
|
||||
.replace( /[^\w\d\.,_-]/g, '-' )
|
||||
.toUpperCase()
|
|
@ -0,0 +1,682 @@
|
|||
'use strict'
|
||||
|
||||
const fs = require( 'fs' )
|
||||
, md5 = require( 'md5' )
|
||||
, path = require( 'path' )
|
||||
, gm = require( 'gm' )
|
||||
, url = require( 'url' )
|
||||
, range = require( 'range-2018.js' )
|
||||
|
||||
, DB = require( './db.js' )
|
||||
, GetID = require( './get-id.js' )
|
||||
, Now = require( './readable-timestamp.js' )
|
||||
, Config = require( '../config.js' )
|
||||
|
||||
const Sizes =
|
||||
exports.Sizes =
|
||||
{ S : 200
|
||||
, N : 490
|
||||
, M : 800
|
||||
, L : 1600
|
||||
}
|
||||
|
||||
|
||||
const SortAccounts = arrAccounts =>
|
||||
arrAccounts
|
||||
.filter( a => a && a.trim( ) )
|
||||
.sort( ( a,b ) => b[ 0 ] == a[ 0 ]
|
||||
? 'i' == a[ 0xA ]
|
||||
: 'r' == a[ 0 ]
|
||||
)
|
||||
|
||||
const getHashFromPhotoURL =
|
||||
exports.getHashFromPhotoURL = PhotoURL =>
|
||||
PhotoURL.match( /([^\/_]+)_?L\.jpg$/ )[ 1 ]
|
||||
|
||||
const PhotoHashmapToSQL =
|
||||
exports.PhotoHashmapToSQL = PhotoHashmap =>
|
||||
Object.keys( PhotoHashmap )
|
||||
.map( k => `Photo_${ k } = "${ PhotoHashmap[ k ] }"` )
|
||||
.join( ',' )
|
||||
|
||||
const SmallerSizes =
|
||||
exports.SmallerSizes =
|
||||
Object.keys( Sizes )
|
||||
.filter( k => 'L' != k )
|
||||
|
||||
const HashOrNr =
|
||||
exports.HashOrNr = Hash =>
|
||||
( Hash
|
||||
&& Hash.length
|
||||
&& ( Hash.length > 2 )
|
||||
)
|
||||
? Hash + '_'
|
||||
: Hash
|
||||
|
||||
const getURL =
|
||||
exports.getURL = ({ SKU, Hash, Size, EXT }) =>
|
||||
url.resolve
|
||||
( Config.Paths.imgGlobal
|
||||
, path.join
|
||||
( './'
|
||||
, GetID( SKU )
|
||||
, HashOrNr( Hash ) + Size + ( EXT || '.jpg' )
|
||||
)
|
||||
)
|
||||
|
||||
const getFilepath =
|
||||
exports.getFilepath = ({ SKU, Hash, Size, EXT }) =>
|
||||
path.join
|
||||
( Config.Paths.imgLocal
|
||||
, GetID( SKU )
|
||||
, HashOrNr( Hash ) + Size + ( EXT || '.jpg' )
|
||||
)
|
||||
|
||||
const Resize =
|
||||
exports.Resize = ({ SKU, Hash }) =>
|
||||
new Promise( ( resolve, reject ) =>
|
||||
{ SmallerSizes.forEach( ( Size, i, a ) =>
|
||||
{ gm( getFilepath({ SKU, Hash, Size : 'L' }) )
|
||||
.resize( Sizes[ Size ], Sizes[ Size ] )
|
||||
.write( getFilepath({ SKU, Hash, Size }), err =>
|
||||
{ if( err )
|
||||
{ reject( err ) }
|
||||
if( i == a.length - 1 )
|
||||
{ resolve( true ) }
|
||||
})
|
||||
})
|
||||
}).catch( err => console.error
|
||||
( Now( )
|
||||
, 'error resizing'
|
||||
, '(Photos.Resize)'
|
||||
, err
|
||||
) )
|
||||
|
||||
const Convert_toBW = path =>
|
||||
new Promise( ( resolve, reject ) =>
|
||||
{ gm( path )
|
||||
.colorspace( 'GRAY' )
|
||||
.write( path, err =>
|
||||
{ if( err )
|
||||
{ console.error
|
||||
( Now( )
|
||||
, 'error converting #10 to BW'
|
||||
, path
|
||||
, err
|
||||
)
|
||||
reject( false )
|
||||
}
|
||||
resolve( true )
|
||||
})
|
||||
|
||||
}).catch( err => console.error
|
||||
( Now( )
|
||||
, 'error converting #10 to BW'
|
||||
, '(Photos.Convert_toBW)'
|
||||
, err
|
||||
) )
|
||||
|
||||
const Process =
|
||||
exports.Process = ({ FilePath, SKU, Variant, Nr }) =>
|
||||
new Promise( async ( resolve, reject ) =>
|
||||
{ if( 10 == Nr )
|
||||
{ console.info( Now( ), 'converting to BW...\n' )
|
||||
await Convert_toBW( FilePath )
|
||||
}
|
||||
|
||||
const Hash = md5( fs.readFileSync( FilePath ) )
|
||||
try
|
||||
{ fs.mkdirSync( path.join( Config.Paths.imgLocal, GetID( SKU ) ) ) }
|
||||
catch( err )
|
||||
{ console.info( 'dir', path.join( Config.Paths.imgLocal, GetID( SKU ) ), 'exist' ) }
|
||||
fs.renameSync( FilePath, getFilepath({ SKU, Hash, Size : 'L' }) )
|
||||
|
||||
const Varinat_Exist = await DB.read(
|
||||
{ table : 'Photos'
|
||||
, where : `( SKU = "${ SKU }"
|
||||
AND Variant = "${ Variant }"
|
||||
)`
|
||||
})
|
||||
|
||||
if( !Varinat_Exist || !Varinat_Exist.length )
|
||||
{ await createVariant({ SKU, Variant }) }
|
||||
|
||||
await DB.update(
|
||||
{ table : 'Photos'
|
||||
, set : ` Photo_${ Nr } = "${ getURL({ SKU, Hash, Size : 'L' }) }"
|
||||
, Need_update = TRUE
|
||||
`
|
||||
, where : `( SKU = "${ SKU }"
|
||||
AND Variant = "${ Variant }"
|
||||
)`
|
||||
})
|
||||
|
||||
await Resize({ SKU, Hash })
|
||||
|
||||
resolve( true )
|
||||
|
||||
}).catch( err => console.error
|
||||
( Now( )
|
||||
, 'error processing'
|
||||
, '(Photos.Process)'
|
||||
, err
|
||||
) )
|
||||
|
||||
const Renumber =
|
||||
exports.Renumber = ({ SKU, Variant, OldNr, NewNr }) =>
|
||||
new Promise( async ( resolve, reject ) =>
|
||||
{ const OldHashmap = await getHashmap(
|
||||
{ SKU
|
||||
, Variant
|
||||
, arrNumbers : range( 1,10 )
|
||||
})
|
||||
const Photo_toShift = OldHashmap[ OldNr ]
|
||||
|
||||
let newArr = Object
|
||||
.keys( OldHashmap )
|
||||
.map( k => OldHashmap[ k ] )
|
||||
newArr.unshift( null )
|
||||
newArr.splice( OldNr, 1 )
|
||||
newArr.splice( NewNr, 0, Photo_toShift )
|
||||
|
||||
let NewHashmap = { }
|
||||
range( 1,10 ).map( Nr =>
|
||||
{ NewHashmap[ Nr ] = newArr[ Nr ] })
|
||||
|
||||
console.info
|
||||
( Now( )
|
||||
, 'Renumbering\n from :'
|
||||
, OldHashmap
|
||||
, 'to :'
|
||||
, NewHashmap
|
||||
)
|
||||
|
||||
const _res = await DB.update(
|
||||
{ table : 'Photos'
|
||||
, set : PhotoHashmapToSQL( NewHashmap )
|
||||
+ ', Need_update = TRUE'
|
||||
, where : `( SKU = "${ SKU }"
|
||||
AND Variant = "${ Variant }"
|
||||
)`
|
||||
})
|
||||
|
||||
if( _res && _res.length )
|
||||
{ resolve( true ) }
|
||||
else
|
||||
{ reject( _res ) }
|
||||
|
||||
}).catch( err => console.error
|
||||
( Now( )
|
||||
, 'error renumbering'
|
||||
, '(Photos.Renumber)'
|
||||
, err
|
||||
) )
|
||||
|
||||
|
||||
const URLoccurrences =
|
||||
exports.URLoccurrences = ({ SKU, PhotoURL }) =>
|
||||
new Promise( async ( resolve, reject ) =>
|
||||
{ const Photos_EQ_URL = range( 1,11 )
|
||||
.map( Nr => `Photo_${ Nr } = "${ PhotoURL }"` )
|
||||
.join( ' OR ' )
|
||||
|
||||
const _req = await DB.read(
|
||||
{ table : 'Photos'
|
||||
, where : `( SKU = "${ SKU }"
|
||||
AND ( ${ Photos_EQ_URL } )
|
||||
)`
|
||||
})
|
||||
|
||||
if( typeof _req == 'object' )
|
||||
{ let Occurrences = 0
|
||||
_req.forEach( V =>
|
||||
{ for( Param in V )
|
||||
{ Occurrences += ( V[ Param ] == PhotoURL ) }
|
||||
})
|
||||
resolve( Occurrences )
|
||||
}
|
||||
reject( false )
|
||||
|
||||
}).catch( err => console.error
|
||||
( Now( )
|
||||
, 'error getting finding occurrences of an URL'
|
||||
, '(Photos.URLoccurrences)'
|
||||
, err
|
||||
) )
|
||||
|
||||
const deletePhotoFiles =
|
||||
exports.deletePhotoFiles = ({ SKU, Hash }) =>
|
||||
new Promise( async ( resolve, reject ) =>
|
||||
{ if( !Hash )
|
||||
{ console.error
|
||||
( Now( )
|
||||
, 'cant delete file, no Hash in params:'
|
||||
, { SKU, Hash }
|
||||
)
|
||||
resolve( false )
|
||||
return false
|
||||
}
|
||||
// check if filename exist anywhere else
|
||||
if( 1 < ( await URLoccurrences(
|
||||
{ SKU
|
||||
, PhotoURL : getURL({ SKU, Hash })
|
||||
})
|
||||
)
|
||||
)
|
||||
{ resolve( false )
|
||||
return false
|
||||
}
|
||||
|
||||
console.log( 'removing file:', SKU + '/' + Hash )
|
||||
Object.keys( Sizes ).map
|
||||
( Size => fs.unlinkSync( getFilepath({ SKU, Hash, Size }) ) )
|
||||
|
||||
resolve( true )
|
||||
|
||||
}).catch( err => console.error
|
||||
( Now( )
|
||||
, 'error deleting files'
|
||||
, '(Photos.deletePhotoFiles)'
|
||||
, err
|
||||
) )
|
||||
|
||||
const removeOne =
|
||||
exports.removeOne = ({ SKU, Variant, Nr }) =>
|
||||
new Promise( async ( resolve, reject ) =>
|
||||
{ const HashMap_req = await getHashmap(
|
||||
{ SKU
|
||||
, Variant
|
||||
, arrNumbers : [ Nr ]
|
||||
})
|
||||
|
||||
const PhotoURL = HashMap_req[ Nr ]
|
||||
, Hash = getHashFromPhotoURL( PhotoURL )
|
||||
|
||||
console.log({ SKU, Variant, Nr, PhotoURL, Hash })
|
||||
|
||||
console.log
|
||||
( Now( )
|
||||
, 'removing photo'
|
||||
, '(Photos.removeOne):\n'
|
||||
, { SKU, Variant, Nr }
|
||||
)
|
||||
|
||||
if( 10 === Number( Nr ) )
|
||||
{ DB.update(
|
||||
{ table : 'Photos'
|
||||
, set : `Photo_10 = ""`
|
||||
, where : `( SKU = "${SKU}"
|
||||
AND Variant = "${Variant}"
|
||||
)`
|
||||
})
|
||||
}
|
||||
else
|
||||
{ const OldHashmap = await getHashmap(
|
||||
{ SKU
|
||||
, Variant
|
||||
, arrNumbers : range( 1,10 ).filter( _Nr => _Nr >= Nr )
|
||||
})
|
||||
|
||||
let NewHashmap = { }
|
||||
Object.keys( OldHashmap )
|
||||
.filter( _Nr => OldHashmap[ _Nr ] )
|
||||
.forEach( _Nr =>
|
||||
{ NewHashmap[ _Nr ] =
|
||||
OldHashmap[ Number( _Nr ) + 1 ] || ''
|
||||
})
|
||||
|
||||
await DB.update(
|
||||
{ table : 'Photos'
|
||||
, set : PhotoHashmapToSQL( NewHashmap )
|
||||
, where : `( SKU = "${SKU}"
|
||||
AND Variant = "${Variant}"
|
||||
)`
|
||||
})
|
||||
}
|
||||
resolve( await deletePhotoFiles({ SKU, Hash }) )
|
||||
|
||||
}).catch( err => console.error
|
||||
( Now( )
|
||||
, 'error removing one'
|
||||
, '(Photos.removeOne)'
|
||||
, err
|
||||
) )
|
||||
|
||||
const getHashmap =
|
||||
exports.getHashmap = ({ SKU, Variant, arrNumbers }) =>
|
||||
new Promise( async ( resolve, reject ) =>
|
||||
{ const _req = await DB.read(
|
||||
{ table : 'Photos'
|
||||
, columns : ( arrNumbers || range( 1,11 ) )
|
||||
.map( Nr => `Photo_${ Nr } AS "${ Nr }"` )
|
||||
.join( ',' )
|
||||
, where : `( SKU="${SKU}"
|
||||
AND Variant="${Variant}"
|
||||
)`
|
||||
})
|
||||
|
||||
if( _req.length && typeof _req == 'object' )
|
||||
{ resolve( _req[ 0 ] ) }
|
||||
reject( false )
|
||||
|
||||
}).catch( err => console.error
|
||||
( Now( )
|
||||
, 'error getting hashmap'
|
||||
, '(Photos.getHashmap)'
|
||||
, err
|
||||
) )
|
||||
|
||||
|
||||
const setDefaultVariant =
|
||||
exports.setDefaultVariant = async ({ SKU, Variant, oldVarinatRenamedTo }) =>
|
||||
{ const _req = await DB.read(
|
||||
{ table : 'Photos'
|
||||
, columns : 'SKU, Variant, Accounts'
|
||||
, where : `SKU = "${ SKU }"`
|
||||
})
|
||||
|
||||
if( !_req.length )
|
||||
{ console.error
|
||||
( Now( )
|
||||
, 'no records of SKU'
|
||||
, SKU
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
let oldDefault = { Accounts :'' }
|
||||
, newDefault = { Accounts :'' }
|
||||
|
||||
_req.forEach( V =>
|
||||
{ if( '_default' == V.Variant )
|
||||
{ oldDefault = V }
|
||||
if( Variant == V.Variant )
|
||||
{ newDefault = V }
|
||||
})
|
||||
|
||||
newDefault.Accounts =
|
||||
[ ...newDefault.Accounts.split( /[\r\n]+/g )
|
||||
, ...oldDefault.Accounts.split( /[\r\n]+/g )
|
||||
]
|
||||
|
||||
newDefault.Accounts = SortAccounts( newDefault.Accounts )
|
||||
|
||||
if( oldDefault.SKU )
|
||||
{ await DB.update(
|
||||
{ table : 'Photos'
|
||||
, set : ` Variant = "${ oldVarinatRenamedTo }"
|
||||
, Accounts = ""
|
||||
`
|
||||
, where : `( SKU = "${ SKU }"
|
||||
AND Variant = "_default"
|
||||
)`
|
||||
})
|
||||
}
|
||||
|
||||
await DB.update(
|
||||
{ table : 'Photos'
|
||||
, set : ` Variant = "_default"
|
||||
, Accounts = "${ newDefault.Accounts.join('\n') }"
|
||||
, Need_update = TRUE
|
||||
`
|
||||
, where : `( SKU = "${ SKU }"
|
||||
AND Variant = "${ Variant }"
|
||||
)`
|
||||
})
|
||||
|
||||
console.log
|
||||
( Now( )
|
||||
, `variant "${ Variant }"`
|
||||
, `of "${ SKU }" is now the default`
|
||||
, '(Photos.setDefaultVariant)'
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
const createVariant =
|
||||
exports.createVariant = ({ SKU, Variant, CopyOf }) =>
|
||||
new Promise( async ( resolve, reject ) =>
|
||||
{ const _test = await DB.read(
|
||||
{ table : 'Photos'
|
||||
, where : `( SKU = "${ SKU }"
|
||||
AND Variant = "${ Variant }"
|
||||
)`
|
||||
})
|
||||
if( _test.length )
|
||||
{ resolve( true )
|
||||
return true
|
||||
}
|
||||
|
||||
const _req = await DB.read(
|
||||
{ table : 'Photos'
|
||||
, where : `( SKU = "${ SKU }"
|
||||
AND Variant = "${ CopyOf || '_default' }"
|
||||
)`
|
||||
})
|
||||
|
||||
console.log( _req && _req.length && _req[ 0 ]
|
||||
|| 'err DB.read [Photos.createVariant]'
|
||||
)
|
||||
|
||||
const Accounts
|
||||
= '_default' == Variant
|
||||
? Config.Accounts.join( '\n' )
|
||||
: ''
|
||||
|
||||
await DB.create(
|
||||
{ table : 'Photos'
|
||||
, set :
|
||||
` SKU = "${ SKU }"
|
||||
, Variant = "${ Variant }"
|
||||
, Need_update = TRUE
|
||||
, Accounts = "${ Accounts }"
|
||||
`
|
||||
+ ( _req && _req.length
|
||||
? `, Photo_10 = "${ _req[ 0 ].Photo_10 }"`
|
||||
+ ( CopyOf
|
||||
? `
|
||||
, Photo_1 = "${ _req[ 0 ].Photo_1 }"
|
||||
, Photo_2 = "${ _req[ 0 ].Photo_2 }"
|
||||
, Photo_3 = "${ _req[ 0 ].Photo_3 }"
|
||||
, Photo_4 = "${ _req[ 0 ].Photo_4 }"
|
||||
, Photo_5 = "${ _req[ 0 ].Photo_5 }"
|
||||
, Photo_6 = "${ _req[ 0 ].Photo_6 }"
|
||||
, Photo_7 = "${ _req[ 0 ].Photo_7 }"
|
||||
, Photo_8 = "${ _req[ 0 ].Photo_8 }"
|
||||
, Photo_9 = "${ _req[ 0 ].Photo_9 }"
|
||||
`
|
||||
: '' )
|
||||
: '' )
|
||||
})
|
||||
|
||||
resolve( true )
|
||||
|
||||
}).catch( err => console.error
|
||||
( Now( )
|
||||
, 'error creating Variant'
|
||||
, '(Photos.createVariant)'
|
||||
, err
|
||||
) )
|
||||
|
||||
|
||||
const assignVariantToAccount =
|
||||
exports.assignVariantToAccount = ({ SKU, Account, Variant }) =>
|
||||
new Promise( async ( resolve, reject ) =>
|
||||
{ const AccountsBefore = await DB.read(
|
||||
{ table : 'Photos'
|
||||
, columns : 'Variant, Accounts'
|
||||
, where : `( SKU = "${ SKU }"
|
||||
AND Accounts LIKE "%${ Account }%"
|
||||
)`
|
||||
})
|
||||
|
||||
if( AccountsBefore && AccountsBefore.length )
|
||||
{ const PrevVariant = AccountsBefore[ 0 ].Variant
|
||||
let arrAccountsToStay = AccountsBefore[ 0 ].Accounts.split(/[\r\n]+/g)
|
||||
arrAccountsToStay.splice( arrAccountsToStay.indexOf(Account), 1)
|
||||
|
||||
console.log({ PrevVariant, arrAccountsToStay })
|
||||
|
||||
await DB.update(
|
||||
{ table : 'Photos'
|
||||
, set : `Accounts = "${ arrAccountsToStay.join('\n') }"`
|
||||
, where : `( SKU = "${ SKU }"
|
||||
AND Variant = "${ PrevVariant }"
|
||||
)`
|
||||
})
|
||||
}
|
||||
else
|
||||
{ console.warn(Account, 'was unset before') }
|
||||
|
||||
const AccountsInsertBefore = await DB.read(
|
||||
{ table : 'Photos'
|
||||
, columns : 'Accounts'
|
||||
, set : `Accounts = "${ Account }"`
|
||||
, where : `( SKU = "${ SKU }"
|
||||
AND Variant = "${ Variant }"
|
||||
)`
|
||||
})
|
||||
|
||||
if( AccountsInsertBefore && AccountsInsertBefore.length )
|
||||
{ let arrUpdatedAccountsInsert =
|
||||
AccountsInsertBefore[ 0 ]
|
||||
.Accounts
|
||||
.split(/[\r\n]+/g)
|
||||
|
||||
arrUpdatedAccountsInsert.push(Account)
|
||||
arrUpdatedAccountsInsert = SortAccounts( arrUpdatedAccountsInsert )
|
||||
|
||||
DB.update(
|
||||
{ table : 'Photos'
|
||||
, set : ` Accounts = "${ arrUpdatedAccountsInsert.join('\n') }"
|
||||
, Need_update = TRUE
|
||||
`
|
||||
, where : `( SKU = "${ SKU }"
|
||||
AND Variant = "${ Variant }"
|
||||
)`
|
||||
})
|
||||
|
||||
resolve( true )
|
||||
}
|
||||
else
|
||||
{ reject( 'error getting AccountsInsertBefore' ) }
|
||||
|
||||
}).catch( err => console.error
|
||||
( Now( )
|
||||
, 'cant assign Variant to Account'
|
||||
, '(Photos.assignVariantToAccount)'
|
||||
, err
|
||||
) )
|
||||
|
||||
const removeVariant =
|
||||
exports.removeVariant = ({ SKU, Variant }) =>
|
||||
new Promise( async ( resolve, reject ) =>
|
||||
{ const VariantToDelete = await DB.read(
|
||||
{ table : 'Photos'
|
||||
, where : `( SKU = "${ SKU }"
|
||||
AND Variant = "${ Variant }"
|
||||
)`
|
||||
})
|
||||
if( !VariantToDelete.length )
|
||||
{ resolve( 'variant does not exist' )
|
||||
return true
|
||||
}
|
||||
|
||||
if( VariantToDelete[ 0 ].Accounts.trim( ) )
|
||||
{ const VariantsMigrateTo = await DB.read(
|
||||
{ table : 'Photos'
|
||||
, where : `( SKU = "${ SKU }"
|
||||
AND Variant != "${ Variant }"
|
||||
)`
|
||||
})
|
||||
if( VariantsMigrateTo.length )
|
||||
{ let AccountsToMigrate = [...new Set
|
||||
( [ ...VariantToDelete[ 0 ].Accounts.split( /[\r\n]+/g )
|
||||
, ...VariantsMigrateTo[ 0 ].Accounts.split( /[\r\n]+/g )
|
||||
]
|
||||
)]
|
||||
|
||||
console.log(
|
||||
{ VariantToDelete
|
||||
, VariantsMigrateTo
|
||||
, AccountsToMigrate
|
||||
})
|
||||
|
||||
await DB.update(
|
||||
{ table : 'Photos'
|
||||
, set : `Accounts = "${ AccountsToMigrate.join('\n') }"`
|
||||
, where : `( SKU = "${ SKU }"
|
||||
AND Variant = "${ VariantsMigrateTo[ 0 ].Variant }"
|
||||
)`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
DB.delete(
|
||||
{ table : 'Photos'
|
||||
, where : `( SKU = "${ SKU }"
|
||||
AND Variant = "${ Variant }"
|
||||
)`
|
||||
})
|
||||
|
||||
let Photos_toDelete = await getHashmap({ SKU, Variant })
|
||||
if( Photos_toDelete )
|
||||
{ for( let i=1; i<11; i++ )
|
||||
{ if( Photos_toDelete[ i ] )
|
||||
{ await deletePhotoFiles(
|
||||
{ SKU
|
||||
, Hash : Photos_toDelete[ i ]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolve( true )
|
||||
|
||||
}).catch( err => console.error
|
||||
( Now( )
|
||||
, 'error removing variant'
|
||||
, '(Photos.removeVariant)'
|
||||
, err
|
||||
) )
|
||||
|
||||
const readTable =
|
||||
exports.readTable = table =>
|
||||
new Promise( async ( resolve, reject ) =>
|
||||
resolve( await DB.read({ table }) )
|
||||
).catch( err => console.error
|
||||
( Now( )
|
||||
, 'error removing variant'
|
||||
, '(Photos.readTable)'
|
||||
, err
|
||||
) )
|
||||
|
||||
|
||||
const get_Photos_Since =
|
||||
exports.get_Photos_Since = ( since = 0 ) =>
|
||||
new Promise( async ( resolve, reject ) =>
|
||||
{ const res = await DB.read(
|
||||
{ table : 'Photos_Ordered'
|
||||
, where : ( 0 != since )
|
||||
&& `( Updated > "${ since }"
|
||||
AND Updated IS NOT NULL
|
||||
)`
|
||||
|| '1'
|
||||
, order_by : 'id'
|
||||
}).catch( reject )
|
||||
|
||||
// console.log( 'get_Photos_Since:', since )
|
||||
// console.log( 'res:', res )
|
||||
resolve( res )
|
||||
|
||||
}).catch( err => console.error
|
||||
( Now( )
|
||||
, 'error removing variant'
|
||||
, '(Photos.readTable)'
|
||||
, err
|
||||
) )
|
||||
|
||||
|
||||
module.exports = exports
|
|
@ -0,0 +1,10 @@
|
|||
'use strict'
|
||||
|
||||
// readable timestamp
|
||||
module.exports = exports = ( ) =>
|
||||
'\x1b[32m'
|
||||
+ ( new Date )
|
||||
.toJSON( )
|
||||
.slice( 0, 23 )
|
||||
.replace( 'T', ' ' )
|
||||
+ '\x1b[0m'
|
|
@ -0,0 +1,26 @@
|
|||
{ "name" : "photo-manager"
|
||||
, "description" : "Upload and Management of Photos (node.js, 2016)"
|
||||
, "version" : "0.0.1"
|
||||
, "author" : "Dym Sohin <re@dym.sh>"
|
||||
, "license" : "MIT"
|
||||
, "main" : "app.js"
|
||||
, "engines" : { "node" : ">=0.8.0" }
|
||||
, "dependencies" :
|
||||
{ "body-parser" : "^1.18.3"
|
||||
, "compression" : "^1.7.1"
|
||||
, "express" : "^4.16.2"
|
||||
, "express-minify" : "^0.2.0"
|
||||
, "formidable" : "^1.1.1"
|
||||
, "gm" : "^1.23.0"
|
||||
, "md5" : "^2.2.1"
|
||||
, "morgan" : "^1.9.0"
|
||||
, "multer" : "^1.3.0"
|
||||
, "range-2018.js": "0.0.4"
|
||||
, "request" : "^2.87.0"
|
||||
}
|
||||
, "scripts" :
|
||||
{ "start" : "npm i --no-save && pm2 update && pm2 start app.js -i 1 --name 'photos' --no-daemon --watch --ignore-watch 'uploads static node_modules .git output.txt log.txt' --attach -l output.txt"
|
||||
, "production" : "ENV=PRODUCTION pm2 start app.js -i 1 --name 'photos' --watch --ignore-watch 'uploads static node_modules .git output.txt log.txt' --attach -l output.txt"
|
||||
, "dev" : "nodemon app.js --ignore static/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
'use strict'
|
||||
|
||||
const os = require( 'os' )
|
||||
, path = require( 'path' )
|
||||
|
||||
const PRODUCTION = ( 'PRODUCTION' == process.env.ENV )
|
||||
|| ( '<DOMAIN.TLD>' == os.hostname() )
|
||||
|
||||
module.exports = exports =
|
||||
{ ENV:
|
||||
{ PRODUCTION
|
||||
, IP : process.env.IP
|
||||
|| ( PRODUCTION && '<IP.4.ADDR>' )
|
||||
|| '::'
|
||||
, IP6 : process.env.IP6
|
||||
|| ( PRODUCTION && '<IP:6:ADDR>' )
|
||||
|| '::'
|
||||
, PORT : process.env.PORT || 8080
|
||||
, SSLPORT : process.env.SSLPORT || 4443
|
||||
, HOST : '<DOMAIN.TLD>'
|
||||
}
|
||||
|
||||
, Paths:
|
||||
{ imgGlobal : 'https://<PHOTOS.URL>'
|
||||
, imgLocal : '~/photos/'
|
||||
, imgFTP : '~/ftp/'
|
||||
}
|
||||
|
||||
, SQL:
|
||||
{ BaseURL : 'http://<DB-API.URL>'
|
||||
, auth :
|
||||
{ user : 'user'
|
||||
, pass : 'pass'
|
||||
}
|
||||
}
|
||||
, Accounts :
|
||||
[ 'account1'
|
||||
, 'account2'
|
||||
]
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square70x70logo src="/icon.png"/>
|
||||
<square150x150logo src="/icon.png"/>
|
||||
<square310x310logo src="/icon.png"/>
|
||||
<TileColor>#ffffff</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,4 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset='utf-8'>
|
||||
<title>PHOTOS</title>
|
||||
<h1>PHOTOS</h1>
|
|
@ -0,0 +1,9 @@
|
|||
{ "name": "App",
|
||||
"icons":
|
||||
[ { "src": "icon.png"
|
||||
, "sizes": "500x500"
|
||||
, "type": "image\/png"
|
||||
, "density": "4.0"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset='utf-8'>
|
||||
<title>Photos</title>
|
||||
|
||||
<style id='ZoomedStyle'></style>
|
||||
<iframe name='uploading' id='uploading' style='display:none'></iframe>
|
||||
|
||||
<nav>
|
||||
<form id='searchForm' name='searchForm' action='#'>
|
||||
<input type='search' id='search' name='search' placeholder='search..' tabindex='1' list='product_types'>
|
||||
<input type='submit' id='searchButton' name='searchButton' value='🔎'>
|
||||
<input type='checkbox' id='ShowOnlyAvaliable' name='ShowOnlyAvaliable'>
|
||||
<label for='ShowOnlyAvaliable'>ShowOnlyAvaliable</label>
|
||||
<span id='CurrentCount'></span>
|
||||
<button id='ShowAll' name='ShowAll' title='Show All'>
|
||||
<i class='fa fa-globe'></i>
|
||||
</button>
|
||||
<button id='ShowWithoutPhotos' name='ShowWithoutPhotos' title='No Photos'>
|
||||
<i class='fa fa-question'></i>
|
||||
</button>
|
||||
<ProgressBarElement></ProgressBarElement>
|
||||
</form>
|
||||
</nav>
|
||||
|
||||
<main id='photos'></main>
|
||||
|
||||
<datalist id='product_types'></datalist>
|
||||
|
||||
<template id='ProductTemplate'>
|
||||
<ProductElement>
|
||||
<ProductDescription>
|
||||
<NewVariant title='Add Variant..'>
|
||||
<button class='button' id='add_new_variant'>
|
||||
<i class='fa fa-plus' aria-hidden='true'></i>
|
||||
</button>
|
||||
<ConfirmationDialog class='hidden'>
|
||||
<input type='text' name='variant_name' placeholder='Variant name'>
|
||||
<ConfirmNewVariant title='Add Variant' class='button top'>
|
||||
<i class='fa fa-plus' aria-hidden='true'></i>
|
||||
</ConfirmNewVariant>
|
||||
<AbortNewVariant title='Abort' class='button'>
|
||||
<i class='fa fa-times' aria-hidden='true'></i>
|
||||
</AbortNewVariant>
|
||||
</ConfirmationDialog>
|
||||
</NewVariant>
|
||||
<ProductSKU>
|
||||
<a href='#'></a>
|
||||
</ProductSKU>
|
||||
<ProductTitle></ProductTitle>
|
||||
<ProductAmount></ProductAmount>
|
||||
</ProductDescription>
|
||||
<Variants></Variants>
|
||||
<hr>
|
||||
<Accounts></Accounts>
|
||||
</ProductElement>
|
||||
</template>
|
||||
|
||||
<template id='ThumbnailsRowTemplate'>
|
||||
<ThumbnailsRow>
|
||||
<RowDescription>
|
||||
<Copy title='Copy Variant' class='button top'>
|
||||
<Icon>
|
||||
<i class='fa fa-copy' aria-hidden='true'></i>
|
||||
</Icon>
|
||||
</button>
|
||||
</Copy>
|
||||
<Remove title='Remove Variant..' class='button top left'>
|
||||
<Icon>
|
||||
<i class='fa fa-times' aria-hidden='true'></i>
|
||||
</Icon>
|
||||
<ConfirmationDialog class='hidden'>
|
||||
<AbortRemoval title='Abort' class='button top left'>
|
||||
<i class='fa fa-times' aria-hidden='true'></i>
|
||||
</AbortRemoval>
|
||||
<ConfirmRemoval title='Remove' class='button top right'>
|
||||
<i class='fa fa-times' aria-hidden='true'></i>
|
||||
</ConfirmRemoval>
|
||||
</ConfirmationDialog>
|
||||
</Remove>
|
||||
<AddPhotos title='Add Photos'>
|
||||
<Upload class='button top left'>
|
||||
<i class='fa fa-cloud-upload' aria-hidden='true'></i>
|
||||
<form target='uploading' enctype='multipart/form-data' method='post' onsubmit='void 0; return false'>
|
||||
<input name='files[]' type='file' class='transparent' accept='image/*' multiple>
|
||||
</form>
|
||||
</Upload>
|
||||
</AddPhotos>
|
||||
<input type='checkbox' name='is_default' title='Set as Default'>
|
||||
<RowTitle></RowTitle>
|
||||
<select name='VariantSelector'></select>
|
||||
</RowDescription>
|
||||
</ThumbnailsRow>
|
||||
</template>
|
||||
|
||||
<template id='ThumbnailTemplate'>
|
||||
<Thumbnail>
|
||||
<Prev title='Prev' class='hidden button arrow left'>
|
||||
<i class='fa fa-arrow-left' aria-hidden='true'></i>
|
||||
</Prev>
|
||||
|
||||
<WrongDimensions></WrongDimensions>
|
||||
<img src='../placeholder.svg'>
|
||||
<ThumbnailCaption></ThumbnailCaption>
|
||||
|
||||
<Upload title='Upload' class='button top left'>
|
||||
<i class='fa fa-cloud-upload' aria-hidden='true'></i>
|
||||
<form target='uploading' enctype='multipart/form-data' method='post' onsubmit='void 0; return false'>
|
||||
<input name='files[]' class='top left transparent' type='file' accept='image/*'>
|
||||
</form>
|
||||
</Upload>
|
||||
<Remove title='Remove..' class='button top right'>
|
||||
<Icon>
|
||||
<i class='fa fa-times' aria-hidden='true'></i>
|
||||
</Icon>
|
||||
<ConfirmationDialog class='hidden'>
|
||||
<AbortRemoval title='Abort' class='button top left'>
|
||||
<i class='fa fa-times' aria-hidden='true'></i>
|
||||
</AbortRemoval>
|
||||
<ConfirmRemoval title='Remove' class='button top right'>
|
||||
<i class='fa fa-times' aria-hidden='true'></i>
|
||||
</ConfirmRemoval>
|
||||
</ConfirmationDialog>
|
||||
</Remove>
|
||||
<Move title='Move' class='button bottom left'>
|
||||
<i class='fa fa-arrows' aria-hidden='true'></i>
|
||||
</Move>
|
||||
<Zoom title='Zoom' class='button bottom right'>
|
||||
<i class='fa fa-search-plus' aria-hidden='true'></i>
|
||||
</Zoom>
|
||||
|
||||
<Next title='Next' class='hidden button arrow right'>
|
||||
<i class='fa fa-arrow-right' aria-hidden='true'></i>
|
||||
</Next>
|
||||
|
||||
<MovePlaceholder title='' class='hidden'></MovePlaceholder>
|
||||
</Thumbnail>
|
||||
</template>
|
||||
|
||||
<link rel='stylesheet' href='../vendor/fa.css'>
|
||||
<link rel='stylesheet' href='../reset.css'>
|
||||
|
||||
<link rel='stylesheet' href='style.css'>
|
||||
<script async defer src='script.js'></script>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,590 @@
|
|||
:root
|
||||
{ --cloud-blue: #0080dd
|
||||
; --border-blue: #0af
|
||||
; --super-light-gray: #f8f8f8
|
||||
; --cancel-red: #c00
|
||||
; --confirm-green: #4b0
|
||||
}
|
||||
|
||||
Thumbnail
|
||||
{ background: url('placeholder.svg') 50% 50% no-repeat
|
||||
; background-size: 100% 100%
|
||||
; margin: 0 1rem 1rem 0
|
||||
; width: 5rem
|
||||
; height: 5rem
|
||||
; position: relative
|
||||
; display: block
|
||||
; float: left
|
||||
}
|
||||
|
||||
Thumbnail img
|
||||
{ width: 5rem
|
||||
; height: 5rem
|
||||
; display: inline-block
|
||||
; position: absolute
|
||||
; border: 0.2rem solid lightgray
|
||||
; background: lightgray
|
||||
}
|
||||
|
||||
Thumbnail[data-nr='10'] img
|
||||
{ border-color: var(--border-blue) }
|
||||
|
||||
Thumbnail ThumbnailCaption
|
||||
{ display: inline-block
|
||||
; position: absolute
|
||||
; background: lightgray
|
||||
; font-weight: bold
|
||||
; padding: 0.15rem 0.3rem
|
||||
; bottom: -0.4rem
|
||||
; right: 0.35rem
|
||||
; font-size: 0.5rem
|
||||
; color: #555
|
||||
; border-radius: 0.6rem
|
||||
}
|
||||
|
||||
Thumbnail .button
|
||||
{ display: none
|
||||
; position: absolute
|
||||
; font-size: 1.1rem
|
||||
; padding: 0 0.35rem 0.2rem 0.35rem
|
||||
; margin: 0.25rem
|
||||
}
|
||||
|
||||
Thumbnail:hover .button
|
||||
{ display: inline-block }
|
||||
|
||||
Thumbnail .button .fa
|
||||
{ text-shadow: 1.5pt 1.5pt 1pt white
|
||||
, -1.5pt -1.5pt 1pt white
|
||||
, -1.5pt 1.5pt 1pt white
|
||||
, 1.5pt -1.5pt 1pt white
|
||||
}
|
||||
|
||||
Thumbnail .button .fa
|
||||
, .button Icon
|
||||
{ pointer-events: none }
|
||||
|
||||
Thumbnail .arrow
|
||||
{
|
||||
; margin: -1.5rem
|
||||
; position: absolute
|
||||
; top: 50%
|
||||
; display: none
|
||||
}
|
||||
|
||||
Upload
|
||||
{ position: relative
|
||||
}
|
||||
|
||||
Upload .fa
|
||||
{ color: var(--cloud-blue)
|
||||
}
|
||||
|
||||
Upload input[type='file']
|
||||
{ top: 0
|
||||
; left: 0
|
||||
; position: absolute
|
||||
; display: block
|
||||
; width: 2.1rem
|
||||
; height: 2.1rem
|
||||
; overflow: hidden
|
||||
; cursor: pointer
|
||||
}
|
||||
|
||||
Copy .fa
|
||||
{ color: var(--confirm-green)
|
||||
; font-size: 0.66em
|
||||
}
|
||||
Remove .fa
|
||||
{ color: var(--cancel-red) }
|
||||
|
||||
AbortRemoval .fa
|
||||
{ color: #333 }
|
||||
|
||||
Move .fa
|
||||
{ color: #333 }
|
||||
|
||||
Thumbnail[data-nr='10'] Move
|
||||
{ display: none !important }
|
||||
|
||||
Zoom .fa
|
||||
{ color: #333 }
|
||||
|
||||
Thumbnail.moving
|
||||
{ position: absolute !important
|
||||
; pointer-events: none
|
||||
; z-index: 1
|
||||
; margin-left: 0 !important
|
||||
}
|
||||
|
||||
MovePlaceholder
|
||||
{ border: 0.25rem solid white
|
||||
; cursor: move
|
||||
; background: lightgray
|
||||
; margin-top: -1rem
|
||||
; margin-left: 1rem
|
||||
; height: 7rem
|
||||
; width: 3rem
|
||||
; float: right
|
||||
; color: gray
|
||||
; text-align: center
|
||||
; line-height: 6.5rem
|
||||
; font-size: 2rem
|
||||
}
|
||||
|
||||
MovePlaceholder:hover
|
||||
{ background: #8f0 }
|
||||
|
||||
.has_moving Thumbnail
|
||||
{ margin-right: 3rem }
|
||||
|
||||
.has_moving Thumbnail MovePlaceholder
|
||||
{ margin-right: -3rem }
|
||||
|
||||
.has_moving Thumbnail .button
|
||||
{ display: none }
|
||||
|
||||
@keyframes flash_border_red
|
||||
{ from { border-color: lightgray }
|
||||
to { border-color: #c44 }
|
||||
}
|
||||
|
||||
.failed
|
||||
{ border-width: 0.25rem
|
||||
; animation-duration: 0.3s
|
||||
; animation-name: flash_border_red
|
||||
; animation-direction: alternate
|
||||
; animation-iteration-count: 2
|
||||
}
|
||||
|
||||
@keyframes flash_border_orange
|
||||
{ from { border-color: lightgray }
|
||||
to { border-color: #f52 }
|
||||
}
|
||||
|
||||
.removed
|
||||
{ animation-duration: 0.3s
|
||||
; animation-name: flash_border_orange
|
||||
; animation-direction: alternate
|
||||
; animation-iteration-count: 2
|
||||
}
|
||||
|
||||
@keyframes flash_border_green
|
||||
{ from { border-color: lightgray }
|
||||
to { border-color: #8f0 }
|
||||
}
|
||||
|
||||
.updated
|
||||
{ animation-duration: 0.3s
|
||||
; animation-name: flash_border_green
|
||||
; animation-direction: alternate
|
||||
; animation-iteration-count: 2
|
||||
}
|
||||
|
||||
@keyframes flash_background_green
|
||||
{ from { background-color: lightgray }
|
||||
to { background-color: #8f0 }
|
||||
}
|
||||
|
||||
.changed_numeration Thumbnail:not([data-nr='10']) ThumbnailCaption
|
||||
{ animation-duration: 0.3s
|
||||
; animation-name: flash_background_green
|
||||
; animation-direction: alternate
|
||||
; animation-iteration-count: 2
|
||||
}
|
||||
|
||||
ProductElement
|
||||
{ display: inline-block
|
||||
; width: 100%
|
||||
; min-height: 7rem
|
||||
; clear: both
|
||||
; float: none
|
||||
; padding: 0.5rem
|
||||
}
|
||||
|
||||
ProductElement ProductDescription
|
||||
{ float: left
|
||||
; padding: 0 1rem
|
||||
; width: 10rem
|
||||
; display: block
|
||||
}
|
||||
|
||||
ProductDescription ProductTitle
|
||||
{ font-size: 0.6rem
|
||||
; display: block
|
||||
; text-overflow: hidden
|
||||
; clear: both
|
||||
}
|
||||
|
||||
ProductDescription ProductAmount
|
||||
{ color: gray
|
||||
; display: block
|
||||
}
|
||||
|
||||
ThumbnailsRow
|
||||
{ display: block
|
||||
; margin-left: 10rem
|
||||
; min-height: 5rem
|
||||
}
|
||||
|
||||
ThumbnailsRow RowDescription
|
||||
{ font-size: 0.75rem
|
||||
; clear: both
|
||||
; text-align: right
|
||||
; display: inline-block
|
||||
; min-width: 6rem
|
||||
; margin-left: -7rem
|
||||
; float: left
|
||||
; position: relative
|
||||
}
|
||||
|
||||
RowDescription > Remove
|
||||
{ display: inline-block
|
||||
; clear: none
|
||||
; position: absolute
|
||||
}
|
||||
|
||||
RowDescription > Copy
|
||||
{ display: inline-block
|
||||
; left: 2rem
|
||||
; position: absolute
|
||||
}
|
||||
|
||||
RowDescription RowTitle
|
||||
{ width: 8rem
|
||||
; display: block
|
||||
; margin-left: -2rem
|
||||
; clear: both
|
||||
}
|
||||
|
||||
RowDescription input[type='checkbox']
|
||||
{ display: inline-block
|
||||
; width: auto
|
||||
; float: right
|
||||
}
|
||||
|
||||
AddPhotos
|
||||
{ display: block
|
||||
; position: relative
|
||||
; clear: both
|
||||
; width: 1.5rem
|
||||
; height: 1.5rem
|
||||
; float: right
|
||||
}
|
||||
|
||||
AddPhotos .fa
|
||||
{ color: gray }
|
||||
|
||||
AddPhotos:hover .fa
|
||||
{ color: #0080dd }
|
||||
|
||||
|
||||
Remove ConfirmationDialog
|
||||
{ position: relative
|
||||
; width: 3rem
|
||||
; height: 1.45rem
|
||||
; display: inline-block
|
||||
; top: 0.15rem
|
||||
; right: -1.85rem
|
||||
; background: lightgray
|
||||
; border-radius: 1rem
|
||||
; z-index: 2
|
||||
; box-shadow: 1.5pt 1.5pt 1pt red
|
||||
, -1.5pt -1.5pt 1pt red
|
||||
, -1.5pt 1.5pt 1pt red
|
||||
, 1.5pt -1.5pt 1pt red
|
||||
}
|
||||
|
||||
Remove ConfirmationDialog .button
|
||||
{ margin: 0
|
||||
; display: inline-block
|
||||
; float: left
|
||||
; height: 1.5rem
|
||||
; width: 1.5rem
|
||||
; font-size: 1rem
|
||||
; line-height: 1.4rem
|
||||
; position: relative
|
||||
}
|
||||
|
||||
Remove ConfirmationDialog .button .fa
|
||||
{ text-shadow: none }
|
||||
|
||||
Remove AbortRemoval:hover:after
|
||||
{ content: ''
|
||||
; display: inline-block
|
||||
; position: absolute
|
||||
; top: 0.1rem
|
||||
; left: 0.1rem
|
||||
; height: 1.25rem
|
||||
; width: 1.25rem
|
||||
; border-radius: 1rem
|
||||
; background: white
|
||||
; opacity: 0.5
|
||||
}
|
||||
|
||||
Remove ConfirmRemoval:hover:after
|
||||
{ display: inline-block
|
||||
; position: absolute
|
||||
; content: ''
|
||||
; top: 0.1rem
|
||||
; left: 0.1rem
|
||||
; height: 1.25rem
|
||||
; width: 1.25rem
|
||||
; border-radius: 1rem
|
||||
; background: red
|
||||
; opacity: 0.25
|
||||
}
|
||||
|
||||
.button
|
||||
, select
|
||||
{ cursor: pointer }
|
||||
|
||||
.top { top: 0 }
|
||||
.left { left: 0 }
|
||||
.right { right: 0 }
|
||||
.bottom { bottom: 0 }
|
||||
|
||||
select
|
||||
, button
|
||||
, input:not([type='file'])
|
||||
{ border: 0.1rem solid lightgray
|
||||
; font-size: 0.75rem
|
||||
; width: 6rem
|
||||
; height: 1.5rem
|
||||
; -webkit-outline: 0
|
||||
; -moz-outline: 0
|
||||
; outline: 0
|
||||
; outline-width: 0
|
||||
; display: block
|
||||
; padding: 0.15rem
|
||||
}
|
||||
|
||||
hr
|
||||
{ border-top: 1rem solid var(--super-light-gray)
|
||||
; padding-top: 1rem
|
||||
; clear: both
|
||||
; display: block
|
||||
; width: 20rem
|
||||
}
|
||||
|
||||
ProductTitle hr
|
||||
{ border: 0
|
||||
; padding: 0
|
||||
; width: auto
|
||||
}
|
||||
|
||||
NewVariant
|
||||
{ float: left
|
||||
; position: relative
|
||||
; left: 9rem
|
||||
; width: 13.95rem
|
||||
}
|
||||
|
||||
NewVariant ConfirmationDialog input[name='variant_name']
|
||||
{ width: 10.95rem /* visual bug: if set to 11 seems TOO wide */
|
||||
; float: left
|
||||
; margin-right: 0.75rem
|
||||
}
|
||||
|
||||
NewVariant button
|
||||
{ width: 1.5rem }
|
||||
|
||||
NewVariant ConfirmationDialog
|
||||
{ position: relative }
|
||||
|
||||
NewVariant AbortNewVariant
|
||||
{ float: right }
|
||||
|
||||
AbortNewVariant .fa
|
||||
{ color: var(--cancel-red) }
|
||||
|
||||
ConfirmNewVariant
|
||||
{ float: left }
|
||||
|
||||
ConfirmNewVariant .fa
|
||||
{ color: var(--confirm-green) }
|
||||
|
||||
.hidden
|
||||
, .zero
|
||||
, ProductElement:not(.targeted) Upload
|
||||
, ProductElement:not(.targeted) Move
|
||||
, ProductElement:not(.targeted) Remove
|
||||
, ProductElement:not(.targeted) > hr
|
||||
, ProductElement:not(.targeted) ThumbnailsRow:not(:first-of-type)
|
||||
, ProductElement:not(.targeted) Accounts
|
||||
, ProductElement:not(.targeted) NewVariant
|
||||
, ProductElement:not(.targeted) RowDescription
|
||||
, Variants select
|
||||
, Accounts .button
|
||||
, Accounts AddPhotos
|
||||
, ThumbnailsRow Thumbnail:first-of-type Remove
|
||||
{ display: none !important
|
||||
}
|
||||
|
||||
ProductElement.targeted
|
||||
{ border-top: 2rem solid var(--super-light-gray)
|
||||
; border-bottom: 2rem solid var(--super-light-gray)
|
||||
; padding-top: 3rem
|
||||
; padding-bottom: 3rem
|
||||
; margin-bottom: 4rem
|
||||
}
|
||||
|
||||
ProductElement.targeted:last-of-type
|
||||
{ margin-bottom: -0.4rem }
|
||||
|
||||
ProductElement:first-of-type
|
||||
{ margin-top: 0 }
|
||||
|
||||
form
|
||||
{ display: inline-block
|
||||
; left: 0
|
||||
}
|
||||
|
||||
Thumbnail.zoomed
|
||||
{ position: fixed
|
||||
; top: 2rem
|
||||
; left: 2rem
|
||||
; z-index: 1
|
||||
; width: 800px
|
||||
; height: 800px
|
||||
}
|
||||
|
||||
Thumbnail.zoomed .button
|
||||
{ display: none }
|
||||
|
||||
Thumbnail.zoomed Zoom
|
||||
, Thumbnail.zoomed .arrow
|
||||
{ display: inline-block !important
|
||||
; z-index: 3
|
||||
}
|
||||
|
||||
Thumbnail.zoomed img
|
||||
{ width: 800px
|
||||
; height: 800px
|
||||
}
|
||||
|
||||
ProgressBarElement
|
||||
{ display: block
|
||||
; width: 0
|
||||
; height: 2pt
|
||||
; margin-top: -2pt
|
||||
; background: var(--border-blue)
|
||||
; opacity: 0.25
|
||||
; pointer-events: none
|
||||
; transition-duration: 0.25s
|
||||
; transition-timing-function: ease-out
|
||||
}
|
||||
|
||||
.transparent
|
||||
{ filter: alpha(opacity=0)
|
||||
; opacity: 0
|
||||
}
|
||||
|
||||
main
|
||||
{ margin-top: 2rem }
|
||||
|
||||
nav
|
||||
{ position: fixed
|
||||
; z-index: 10
|
||||
; display: inline-block
|
||||
; top: 0
|
||||
; left: 0
|
||||
; height: 2rem
|
||||
; min-width: 10rem
|
||||
; width: 100%
|
||||
; background: lightgray
|
||||
; box-shadow: 0 2pt 5pt lightgray
|
||||
}
|
||||
|
||||
nav form
|
||||
{ display: block
|
||||
; min-width: 10rem
|
||||
; width: 100%
|
||||
}
|
||||
|
||||
nav input
|
||||
{ font-size: 1.3rem !important
|
||||
; height: 2rem !important
|
||||
; float: left
|
||||
}
|
||||
nav input[type='search']
|
||||
{ display: block
|
||||
; background: var(--super-light-gray) !important
|
||||
; border-color: var(--super-light-gray) !important
|
||||
; min-width: 10rem
|
||||
; width: 50% !important
|
||||
}
|
||||
|
||||
nav input[type='checkbox']
|
||||
{ display: none }
|
||||
|
||||
nav input[type='checkbox'] + label
|
||||
{ float: right
|
||||
; background: var(--super-light-gray)
|
||||
; border: 0
|
||||
; min-width: 5rem
|
||||
; display: block
|
||||
; padding: 0.25rem 0.5rem
|
||||
; height: 2rem
|
||||
; float: left
|
||||
}
|
||||
nav input[type='checkbox']:checked + label
|
||||
{ background: #ad0
|
||||
}
|
||||
|
||||
nav input[type='submit']
|
||||
{ background: var(--super-light-gray) !important
|
||||
; width: 2rem
|
||||
; text-align: center
|
||||
; border: 0
|
||||
; line-height: 1.3rem
|
||||
; font-size: 1rem
|
||||
}
|
||||
|
||||
nav span
|
||||
{ float: left
|
||||
; border: 0
|
||||
; min-width: 3rem
|
||||
; display: block
|
||||
; padding: 0.25rem 0.5rem
|
||||
; height: 2rem
|
||||
; float: left
|
||||
; text-align: center
|
||||
}
|
||||
|
||||
nav button
|
||||
{ display: block
|
||||
; background: var(--super-light-gray) !important
|
||||
; float: right
|
||||
; width: 2rem
|
||||
; height: 2rem
|
||||
; text-align: center
|
||||
; border: 0
|
||||
}
|
||||
|
||||
nav button .fa
|
||||
{ font-size: 1rem
|
||||
; line-height: 1.5rem
|
||||
}
|
||||
|
||||
RowDescription Remove ConfirmationDialog
|
||||
{ right: auto
|
||||
; left: -0.45rem
|
||||
}
|
||||
|
||||
RowDescription Remove ConfirmationDialog .button i
|
||||
{ left: -0.35rem
|
||||
; position: relative
|
||||
}
|
||||
|
||||
Thumbnail WrongDimensions
|
||||
{ background: var(--cancel-red)
|
||||
; color: white
|
||||
; position: absolute
|
||||
; display: block
|
||||
; z-index: 20
|
||||
; text-align: center
|
||||
; width: 5rem
|
||||
; top: -1.15rem
|
||||
; font-size: 0.75rem
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 141.53383 200.16929" height="38.042" width="38.042">
|
||||
<g fill="none" stroke="gray" stroke-opacity=".3">
|
||||
<path style="isolation:auto;mix-blend-mode:normal" color="#000" overflow="visible" stroke-width="10" d="M-24.318 195.17h190.17V5h-190.17z"/>
|
||||
<path stroke-width="3.577" d="M36.756 134.095h68.021v-68.02h-68.02z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 392 B |
|
@ -0,0 +1,37 @@
|
|||
@font-face
|
||||
{ font-family: 'PT Sans Caption'
|
||||
; font-style: normal
|
||||
; font-weight: 400
|
||||
; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC
|
||||
, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000
|
||||
; src: local( 'PT Sans Caption' )
|
||||
, local( 'PTSans-Caption' )
|
||||
, url( 'https://fonts.gstatic.com/s/ptsanscaption/v9/OXYTDOzBcXU8MTNBvBHeSebG2bpC97vgFIKC7TC5f4U.eot' )
|
||||
format( 'embedded-opentype' )
|
||||
, url( 'https://fonts.gstatic.com/s/ptsanscaption/v9/OXYTDOzBcXU8MTNBvBHeSVu3pQpJXC1E_Hw7zMp8vJM.woff2' )
|
||||
format( 'woff2' )
|
||||
, url( 'https://fonts.gstatic.com/s/ptsanscaption/v9/OXYTDOzBcXU8MTNBvBHeSQRW432DtwGNER78eOJ0i0s.woff' )
|
||||
format( 'woff' )
|
||||
, url( 'https://fonts.gstatic.com/s/ptsanscaption/v9/OXYTDOzBcXU8MTNBvBHeSVQX5OToqP8MI8UfeMWsWEY.ttf' )
|
||||
format( 'truetype' )
|
||||
, url( 'https://fonts.gstatic.com/l/font?kit=OXYTDOzBcXU8MTNBvBHeSUGdxaNEzIe9U2myqkmfmAQ&skey=3b791ca205af4a6f&v=v9#PTSansCaption' )
|
||||
format( 'svg' )
|
||||
}
|
||||
|
||||
* { line-height: 1.5em
|
||||
; padding: 0
|
||||
; margin: 0
|
||||
; background: none
|
||||
; color: black
|
||||
; border: none
|
||||
; text-decoration: none
|
||||
; list-style: none
|
||||
; box-sizing: border-box
|
||||
; font: normal 16pt 'PT Sans Caption', sans-serif
|
||||
}
|
||||
|
||||
nav
|
||||
{ list-style: none }
|
||||
|
||||
::-webkit-file-upload-button
|
||||
{ cursor: pointer }
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
Loading…
Reference in New Issue